Association, Aggregation, Composition and Casting in OOP
Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.
Association, Aggregation, and Composition in Object-Oriented Programming
Association, Aggregation, and Composition in Object-Oriented Programming
In today's lesson, we will learn relational concepts in object-oriented programming. You can find several definitions of association, composition, and aggregation on the Internet but most of them have convoluted explanations with no practical applications.
These three terms describe the different relationships between classes. We will go through each of them one by one and by the end of this lesson, you will have a clear understanding of these relationships. Afterward, we will learn how to create diagrams that represent each of these relations among classes.
Association
Association
In object-oriented general software design, the relationship between one object's functionality and another's is known as an association. Note that an association between two objects is not the same thing as inheritance between two classes. Association means that one object uses another object or a function/method in that other object. In other words, association is defined as the relationship between objects when one object has one or more references to other objects.
Below are the core differences between association and inheritance:
Inheritance implies that two objects are the same type of object. One object just happens to be either a more generalized or more specific version of the other object. Association occurs between two different objects.
Inheritance is said to be an IS-A relationship whereas association is known as a HAS-A relationship.
The modality of inheritance depends on the programming language features. For example, Java does not support multiple inheritance, but C++ does. On the other hand, any language can have one-to-one, one-to-many, and many-to-many associations between objects.
There are two types of associations between objects: composition and aggregation. Let's understand what those are.
Composition
Composition
Composition is a form of association that occurs when an object's life is tightly bound to another object's life. When the main object dies (i.e., is deleted), all the objects that are associated with that object also die. This means that the referenced object is solely contained by the referring object.
There are a lot of real-life examples of composition in OOP. We are going to present the most used composition example here: the Vehicle example.
// Vehicle.java
public class Vehicle {
private Wheel[4] wheels;
private Door[4] doors;
private Seat[4] seats;
// These objects are created only with their containing class instantiations.
public Vehicle() {
for(int i = 0; i<4; i++) {
wheels[i] = new Wheel();
doors[i] = new Door();
seats[i] = new Seat();
}
}
public void mode(Place a, Place b) {
// Moving
}
}
// Main.java
public class Main {
public static void main(String[] args) {
// All the Wheel, Door and Seat will be created with Vehicle
Vehicle vehicle = new Vehicle();
// Do something with Vehicle
}
// The Wheel, Door, and Seat will be destroyed with the Vehicle when out of scope
}
// Vehicle.js
class Vehicle {
constructor() {
this.wheels = Array(4).fill(new Wheel());
this.doors = Array(4).fill(new Door());
this.seats = Array(4).fill(new Seat());
}
mode(a, b) {
// Moving
}
}
// index.js
const vehicle = new Vehicle();
// Do something with Vehicle
// The Wheel, Door, and seats will be garbage collected when they're no longer in use
# Vehicle.py
class Vehicle:
def __init__(self):
self.wheels = [Wheel() for _ in range(4)]
self.doors = [Door() for _ in range(4)]
self.seats = [Seat() for _ in range(4)]
def mode(self, a, b):
# Moving
# main.py
vehicle = Vehicle()
# Do something with Vehicle
# The Wheel, Door, and Seat will be destroyed with the Vehicle when out of scope
// Vehicle.cpp
class Vehicle {
Wheel wheels[4];
Door doors[4];
Seat seats[4];
public:
Vehicle() {
for(int i = 0; i < 4; i++) {
wheels[i] = Wheel();
doors[i] = Door();
seats[i] = Seat();
}
}
void mode(Place a, Place b) {
// Moving
}
};
// main.cpp
int main() {
// All the Wheel, Door and Seat will be created with Vehicle
Vehicle vehicle;
// Do something with Vehicle
// The Wheel, Door, and Seat will be destroyed with the Vehicle when out of scope
return 0;
}
// Vehicle.cs
public class Vehicle {
private Wheel[] wheels = new Wheel[4];
private Door[] doors = new Door[4];
private Seat[] seats = new Seat[4];
public Vehicle() {
for(int i = 0; i<4; i++) {
wheels[i] = new Wheel();
doors[i] = new Door();
seats[i] = new Seat();
}
}
public void Mode(Place a, Place b) {
// Moving
}
}
// Program.cs
public class Program {
public static void Main(string[] args) {
// All the Wheel, Door and Seat will be created with Vehicle
Vehicle vehicle = new Vehicle();
// Do something with Vehicle
}
// The Wheel, Door, and Seat will be destroyed with the Vehicle when out of scope
}
// vehicle.go
type Vehicle struct {
wheels [4]Wheel
doors [4]Door
seats [4]Seat
}
func NewVehicle() *Vehicle {
v := new(Vehicle)
for i := range v.wheels {
v.wheels[i] = Wheel{}
v.doors[i] = Door{}
v.seats[i] = Seat{}
}
return v
}
func (v *Vehicle) Mode(a, b Place) {
// Moving
}
// main.go
func main() {
vehicle := NewVehicle()
// Do something with Vehicle
// The Wheel, Door, and Seat will be destroyed with the Vehicle when not in use
}
Note that you cannot keep the Door or any other object alive after destroying the Vehicle object. This is why it's called composition: the Vehicle class/object is composed of Doors, Wheels, and Seats.
Composition Ownership Snapshot
Before moving on, lock in the key composition idea: the containing object is responsible for creating and owning its parts.
In the visual, notice that Vehicle is not just connected to Wheel, Door, and Seat — it owns them. If the Vehicle instance goes away, those part objects go with it.

Composition Lifecycle Map
Before moving on, lock in the key idea: composition is ownership with shared lifetime.
In the Vehicle example, the Vehicle creates its Wheel, Door, and Seat objects internally. If the Vehicle disappears, those parts do not continue as independent objects in the model.

Aggregation
Aggregation
Aggregation is the other form of association and is similar to composition. In aggregation, a container object again has several references to other objects. But, Aggregation is looser than composition. The objects' life cycles are not bound to each other in aggregation. Thus, the referring object may get destroyed before/after the referenced object.
Let's add some more properties to the Vehicle class to demonstrate aggregation.
// Vehicle.java
public class Vehicle {
private Wheel[4] wheels;
private Door[4] doors;
private Seat[4] seats;
// Now these objects will be Aggregated to the Vehicle object
private List<Person> passengers;
// We will not create a passenger while creating a car lol
public Vehicle() {
for(int i = 0; i<4; i++) {
wheels[i] = new Wheel();
doors[i] = new Door();
seats[i] = new Seat();
}
passenger = new ArrayList<Person>();
}
// Add a Passenger who is living somewhere else
public getIn(Person person) {
passengers.add(person);
}
// Passenger gets removed, but not destroyed
public getOut(Person person) {
passengers.remove(person);
}
public void mode(Place a, Place b) {
// Moving
}
}
// Vehicle.js
class Vehicle {
constructor() {
this.wheels = Array(4).fill(new Wheel());
this.doors = Array(4).fill(new Door());
this.seats = Array(4).fill(new Seat());
this.passengers = [];
}
getIn(person) {
this.passengers.push(person);
}
getOut(person) {
let index = this.passengers.indexOf(person);
if (index > -1) {
this.passengers.splice(index, 1);
}
}
move(a, b) {
// Moving
}
}
# Vehicle.py
class Vehicle:
def __init__(self):
self.wheels = [Wheel() for _ in range(4)]
self.doors = [Door() for _ in range(4)]
self.seats = [Seat() for _ in range(4)]
self.passengers = []
def get_in(self, person):
self.passengers.append(person)
def get_out(self, person):
self.passengers.remove(person)
def move(self, a, b):
# Moving
// vehicle.cpp
class Vehicle {
private:
Wheel wheels[4];
Door doors[4];
Seat seats[4];
std::vector<Person> passengers;
public:
Vehicle() {
for(int i = 0; i < 4; i++) {
wheels[i] = Wheel();
doors[i] = Door();
seats[i] = Seat();
}
}
void getIn(Person person) {
passengers.push_back(person);
}
void getOut(Person person) {
passengers.erase(person);
}
void move(Place a, Place b) {
// Moving
}
};
// Vehicle.cs
public class Vehicle
{
Wheel[] wheels = new Wheel[4];
Door[] doors = new Door[4];
Seat[] seats = new Seat[4];
List<Person> passengers = new List<Person>();
public Vehicle() {
for(int i = 0; i<4; i++) {
wheels[i] = new Wheel();
doors[i] = new Door();
seats[i] = new Seat();
}
}
public void GetIn(Person person) {
passengers.Add(person);
}
public void GetOut(Person person) {
passengers.Remove(person);
}
public void Move(Place a, Place b) {
// Moving
}
}
// vehicle.go
type Vehicle struct {
wheels [4]Wheel
doors [4]Door
seats [4]Seat
passengers []Person
}
func NewVehicle() Vehicle {
veh := Vehicle{}
for i := 0; i < 4; i++ {
veh.wheels[i] = Wheel{}
veh.doors[i] = Door{}
veh.seats[i] = Seat{}
}
return veh
}
func (v *Vehicle) GetIn(p Person) {
v.passengers = append(v.passengers, p)
}
func (v *Vehicle) GetOut(p Person) {
for index, val := range v.passengers {
if val == p {
v.passengers = append(v.passengers[:index], v.passengers[index+1:]...)
break
}
}
}
func (v *Vehicle) Move(a, b Place) {
// Moving
}
In the above example, each Person object that is inside the Vehicle is not being created with the Vehicle. When they get out of the Vehicle, they are not destroyed. This is possible since there are other classes that also reference the Person object. Let's see the main method here.
// Main.java
public class Main {
public static void main(String[] args) {
// Person being created before Vehicle
Person person1 = new Person();
Vehicle vehicle = new Vehicle(); // All the Composition objects (door, wheel) being created with Vehicle
// Aggregation happens here
vehicle.getIn(person1);
// Person being created after Vehicle
Person person2 = new Person();
// Another person object aggregating
vehicle.getIn(person2);
vehicle.getOut(person1);
transport(new Place('A'), new Place('B'), person1);
// Everything gets destroyed here
}
// Suppose we will transport a person somewhere
public static void transport(Place a, Place b, Person person) {
Vehicle vehicle = new Vehicle();
vehicle.getIn(person);
vehicle.move(a, b);
// Going out of scope, vehicle gets destroyed
// But person object is still alive
}
}
// Main.js
class Main {
static transport(a, b, person) {
let vehicle = new Vehicle();
vehicle.getIn(person);
vehicle.move(a, b);
// Getting out of scope, vehicle gets destroyed
// But person object is still alive
}
main() {
// Person getting created before Vehicle
let person1 = new Person();
let vehicle = new Vehicle(); // All the Composition objects (door, wheel) getting created with Vehicle
// Aggregation happening here
vehicle.getIn(person1);
// Person getting created after Vehicle
let person2 = new Person();
// Another person object aggregating
vehicle.getIn(person2);
vehicle.getOut(person1);
Main.transport(new Place('A'), new Place('B'), person1);
// Everything getting destroyed here
}
}
# Main.py
class Main:
@staticmethod
def transport(a, b, person):
vehicle = Vehicle()
vehicle.get_in(person)
vehicle.move(a, b)
# Going out of scope, vehicle gets destroyed
# But person object is still alive
def main(self):
# Person being created before Vehicle
person1 = Person()
vehicle = Vehicle() # All the Composition objects (door, wheel) being created with Vehicle
# Aggregation happens here
vehicle.get_in(person1)
# Person being created after Vehicle
person2 = Person()
# Another person object aggregating
vehicle.get_in(person2)
vehicle.get_out(person1)
Main.transport(Place('A'), Place('B'), person1)
# Everything gets destroyed here
// Main.cs
public class Main
{
// Suppose we will transport a person somewhere
public static void Transport(Place a, Place b, Person person)
{
Vehicle vehicle = new Vehicle();
vehicle.GetIn(person);
vehicle.Move(a, b);
// Going out of scope, vehicle gets destroyed
// But person object is still alive
}
public void Main()
{
// Person being created before Vehicle
Person person1 = new Person();
Vehicle vehicle = new Vehicle(); // All the Composition objects (door, wheel) being created with Vehicle
// Aggregation happens here
vehicle.GetIn(person1);
// Person being created after Vehicle
Person person2 = new Person();
// Another person object aggregating
vehicle.GetIn(person2);
vehicle.GetOut(person1);
Transport(new Place('A'), new Place('B'), person1);
// Everything gets destroyed here
}
}
// Main.cpp
class Main {
public:
// Suppose we will transport a person somewhere
static void transport(Place& a, Place& b, Person& person) {
Vehicle vehicle;
vehicle.getIn(person);
vehicle.move(a, b);
// Going out of scope, vehicle gets destroyed
// But person object is still alive
}
void main() {
// Person being created before Vehicle
Person person1;
Vehicle vehicle; // All the Composition objects (door, wheel) being created with Vehicle
// Aggregation happens here
vehicle.getIn(person1);
// Person being created after Vehicle
Person person2;
// Another person object aggregating
vehicle.getIn(person2);
vehicle.getOut(person1);
transport(Place('A'), Place('B'), person1);
// Everything gets destroyed here
}
};
// Main.go
type Main struct{}
func (m Main) transport(a, b Place, person Person) {
vehicle := Vehicle{}
vehicle.getIn(person)
vehicle.move(a, b)
// Going out of scope, vehicle gets destroyed
// But person object is still alive
}
func main() {
main := Main{}
// Person being created before Vehicle
person1 := Person{}
vehicle := Vehicle{} // All the Composition objects (door, wheel) being created with Vehicle
// Aggregation happens here
vehicle.getIn(person1)
// Person being created after Vehicle
person2 := Person{}
// Another person object aggregating
vehicle.getIn(person2)
vehicle.getOut(person1)
main.transport(Place{'A'}, Place{'B'}, person1)
// Everything gets destroyed here
}
In the above example, we can see in the main method that the Person objects can be created regardless of whether Vehicle instantiation occurs. In the transport method, we can see that even if the Vehicle object is created and then deleted, the Person object is alive in the method and after that.
Aggregation Reference Snapshot
Aggregation looks similar in code because one object still stores references to other objects. The difference is ownership.
In the visual, Vehicle points to Person objects, but it does not create or destroy them. Passengers can enter, leave, and continue existing outside the vehicle.

Aggregation Survival Test
Aggregation is easiest to recognize by asking: can the referenced object still make sense without the container?
A Passenger can get into a Vehicle, get out, and continue existing. The vehicle stores a reference, but it does not own the passenger's lifetime.

Association vs Composition vs Aggregation
Association vs Composition vs Aggregation
The key difference between association, composition, and aggregation is that composition and aggregation are two forms of association.

The key differences between composition and aggregation are given below:
Composition Aggregation 1 Child will co-exist with the container class Child can exist Independently 2 Part-of relationship. Has-a relationship 3 Stronger form of association Weaker form of composition 4 Can only have one-to-one and many-to-one relationship Can have a one-to-one, many-to-one, one-to-many and many-to-many relationship 5 Composited objects cannot be changed from the referrer. Aggregated objects can be removed or replaced by another same type of object.Ownership Quick Check
Ownership Quick Check
A good way to separate these three relationship types is to ask one simple question:
If object
Adisappears, what happens to objectB?
-
Association:
Aonly knows about or usesB. Their lifetimes are independent. -
Aggregation:
AhasB, butBcan still exist withoutA. -
Composition:
AownsB. IfAis destroyed,Bshould not meaningfully exist by itself.
For example, a Library and a Book are usually aggregation. A book can be moved to another library or exist outside the library.
But a House and its Room objects are usually composition. A room is normally created as part of a house and does not exist as a standalone object in the program.

quiz 1
You can delete one object and keep another if the relationship between them is association.
Upcasting and Downcasting
Upcasting and Downcasting
When we cast objects in OOP, we usually talk about two directions:
- Upcasting: treating a child object as its parent type.
- Downcasting: treating a parent reference as a more specific child type.
Upcasting is safe because every child object is also a valid parent object. Downcasting is more dangerous because not every parent reference is actually pointing to that child type.
Let's see this in Java:
```java
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("Dog is barking");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
// Upcasting: Dog is treated as an Animal
Animal animal = dog;
animal.eat();
// Downcasting: Animal reference is treated as a Dog again
Dog sameDog = (Dog) animal;
sameDog.bark();
}
}
```
quiz 3
Which statement about object casting in Java is correct?
Object Typecasting
Object Typecasting
Now let's learn another concept in OOP called typecasting. Typecasting, or type conversion, is the process of converting a primitive data type to another primitive data type. In the OOP world, we can only cast objects to compatible types. In most languages, typecasting is automatically preferred. For example, if we use a float as int or double, it will automatically typecast. See the example below:
// Main.java
public class Main {
public static void main(String[] args) {
Float f1 = 0.5, f2 = 0.6;
// Passing floats will be automatically converted to int
Integer i = add(f1, f2);
System.out.println(i);
}
public static int add(Integer i1, Integer i2) {
return i1 + i2;
}
}
// This javascript code simulates the same scenario as your provided Java
// code snippet, whereby 2 floats f1 and f2 are passed and automatically
// converted to integers before performing addition
let f1 = 0.5, f2 = 0.6;
function add(i1, i2) {
return Math.floor(i1) + Math.floor(i2);
}
let i = add(f1, f2);
console.log(i);
# Python code simulating the scenario of your Java snippet,
# two floating type variables, f1 and f2, are passed and
# automatically converted to integers before addition
f1, f2 = 0.5, 0.6
def add(i1, i2):
return int(i1) + int(i2)
i = add(f1, f2)
print(i)
// This C++ code simulates the same scenario as the Java snippet,
// where f1 and f2 floats are passed and automatically converted
// into integers before adding.
#include <iostream>
using namespace std;
int add(int i1, int i2) {
return i1 + i2;
}
int main() {
float f1 = 0.5, f2 = 0.6;
int i = add((int)f1, (int)f2);
cout << i;
return 0;
}
// This C# code simulates the same scenario as your provided Java
// snippet, you have two float types, f1 and f2, that are passed
// and automatically converted to integers before performing the addition.
using System;
public class Program
{
public static void Main()
{
float f1 = 0.5f, f2 = 0.6f;
int i = Add((int)f1, (int)f2);
Console.WriteLine(i);
}
public static int Add(int i1, int i2)
{
return i1 + i2;
}
}
// Go code simulating the same scenario in the Java snippet with
// two floats f1 and f2, that are converted to integers prior to addition
package main
import "fmt"
func add(i1 int, i2 int) int {
return i1 + i2
}
func main() {
var f1 float64 = 0.5
var f2 float64 = 0.6
i := add(int(f1), int(f2))
fmt.Println(i)
}
The type of conversion above is called automatic type conversion.
Other than the primitive types and built-in classes, user-defined interfaces or classes belonging to a single type hierarchy can be converted or cast into one another. However, if you attempt to convert objects with different types of hierarchy or different parent-child relationships, a compilation error will occur. At runtime, it is also possible to get a ClassCastException when you try to cast classes that are not in the same path in the class hierarchy.
In Java, if we set an object to one of its parents' reference types, then there is no way to call the parent object's methods that are overridden by the subclass. See the example below:
public class Shape {
public void draw() {
System.out.println("Only Draw Shape");
}
}
public class Rectangle {
@Override
public void draw() {
System.out.println("Drawing Rectangle");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
// This is allowed due to polymorphism
Shape shape = new Rectangle();
// This will always call overridden method
shape.draw();
}
}
// Output will be:
// Drawing Rectangle
class Shape {
draw() {
console.log("Only Draw Shape");
}
}
class Rectangle extends Shape {
draw() {
console.log("Drawing Rectangle");
}
}
// main.js
let shape = new Rectangle();
// This will always call overridden method
shape.draw();
// Output will be:
// Drawing Rectangle
class Shape:
def draw(self):
print("Only Draw Shape")
class Rectangle(Shape):
def draw(self):
print("Drawing Rectangle")
# main.py
shape = Rectangle()
# This will always call overridden method
shape.draw()
# Output will be:
# Drawing Rectangle
#include <iostream>
class Shape {
public:
virtual void draw() {
std::cout << "Only Draw Shape" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing Rectangle" << std::endl;
}
};
// main.cpp
int main() {
// This is allowed due to polymorphism
Shape* shape = new Rectangle();
// This will always call overridden method
shape->draw();
delete shape;
return 0;
// Output will be:
// Drawing Rectangle
}
public class Shape {
public virtual void Draw() {
Console.WriteLine("Only Draw Shape");
}
}
public class Rectangle : Shape {
public override void Draw() {
Console.WriteLine("Drawing Rectangle");
}
}
// Program.cs
public sealed class Program {
public static void Main(string[] args) {
// This is allowed due to polymorphism
Shape shape = new Rectangle();
// This will always call overridden method
shape.Draw();
// Output will be:
// Drawing Rectangle
}
}
package main
import "fmt"
type Shape interface {
Draw()
}
type rectangle struct{}
func (r rectangle) Draw() {
fmt.Println("Drawing Rectangle")
}
type anyShape struct{}
func (s anyShape) Draw() {
fmt.Println("Only Draw Shape")
}
func main() {
var shape Shape
// This is allowed due to polymorphism
shape = rectangle{}
// This will always call overridden method
shape.Draw()
// Output will be:
// Drawing Rectangle
}
So how can we call a method of the Shape class? We can do this by first casting the Rectangle to Shape and then calling it.
// Main.java
public class Main {
public static void main(String[] args) {
Shape shape = new Rectangle();
// We cast the rectangle into Shape, and then call methods
((Shape)shape).draw();
}
}
// Output will be:
// Only Draw Shape
// Program.cs
public class Program {
public static void Main(string[] args) {
Shape shape = new Rectangle();
// We cast the rectangle into Shape, and then call methods
((Shape)shape).Draw();
}
}
# main.py
shape = Rectangle()
# We cast the rectangle into Shape, and then call methods
shape.draw()
// main.js
let shape = new Rectangle();
// We cast the rectangle into Shape, and then call methods
shape.draw();
// main.cpp
int main() {
Shape* shape = new Rectangle;
// We cast the rectangle into Shape, and then call methods
shape->draw();
delete shape;
return 0;
}
// main.go
func main() {
var shape Shape = new(Rectangle)
// We cast the rectangle into Shape, and then call methods
shape.Draw()
}
Typecasting can be divided into two types:
-
Upcasting: When you cast an object to one of its parent class types (i.e., from
RectangletoShape). -
Downcasting: When you cast an object to one of its subtypes (i.e., from
ShapetoRectangle)
Reference vs Object
For object casting, separate two ideas: the reference type and the actual object type.
A parent reference can point to a child object during upcasting. Downcasting only works safely when that parent reference is still pointing to the expected child object.

Casting Direction Guide
For object casting, separate two ideas: the reference type you are using and the actual object sitting in memory.
Upcasting moves to a more general reference and is safe. Downcasting moves back to a more specific reference and should only happen when the actual object really is that subtype.

Parent of all classes
Parent of All Classes
In many programming languages like Java, there is always a class that is implicitly the parent of all the built-in and user-defined classes. Java calls it the Object class. The Object class is beneficial if you want to refer to any object whose type you don't know. Notice that the parent class reference variable can refer to the child class object (defined as upcasting in the previous section). We can instantiate an Object object using the following method:
Object obj = getObject();
var obj = getObject();
obj = getObject()
Object obj = getObject();
Object obj = getObject();
obj := getObject()
The Object class has many methods predefined in it, so any user-defined class should have those built-in methods in it without explicitly declaring or defining them. The methods are:
public final Class getClass()public int hashCode()public boolean equals(Object obj)protected Object clone()public String toString()public final void notify()public final void notifyAll()public final void wait(long timeout) throws InterruptedExceptionpublic final void wait(long timeout,int nanos) throws InterruptedExceptionpublic final void wait() throws InterruptedExceptionprotected void finalize() throws Throwable
Using this Object class, we can create an array of Objects of any type. See the code below:
public class Main {
public static void main(String[] args) {
Object[] objects = {Integer(5), Float(5.5)};
System.out.println((Integer)objects[0]);
System.out.println((Float)objects[1]);
}
}
// Output:
// 5
// 5.5
var objects = [5, 5.5];
console.log(objects[0]);
console.log(objects[1]);
// Output:
// 5
// 5.5
objects = [5, 5.5]
print(objects[0])
print(objects[1])
# Output:
# 5
# 5.5
#include<iostream>
int main() {
auto objects = {5, 5.5f};
std::cout << objects[0] << std::endl;
std::cout << objects[1] << std::endl;
return 0;
}
// Output:
// 5
// 5.5
public class MainClass {
public static void Main() {
object[] objects = {5, 5.5};
Console.WriteLine(objects[0]);
Console.WriteLine(objects[1]);
}
}
// Output:
// 5
// 5.5
package main
import (
"fmt"
)
func main() {
objects := []interface{}{5, 5.5}
fmt.Println(objects[0])
fmt.Println(objects[1])
}
// Output:
// 5
// 5.5
Remember that you need to remember the types when you insert them into the containers (arrays) or else there would be no way to know what that Object was previously.
quiz 2
Which of the following methods is not already in a class?
Using UML to Represent Class Hierarchy
Using UML to Represent Class Hierarchy
Object-oriented programming is used to design large systems with a lot of interconnected components. OOP is applied to desktop application design, web app development, and server program management. Top frameworks like .NET Framework, Django, Spring, Unity 3D all use object-oriented designs. All of these frameworks use thousands of classes to make the developers' lives a lot easier. For example, .Net Framework has implementations of 6025 classes!
It is not easy to demonstrate the relationships between thousands of classes, so a Unified Modelling Language (UML) was invented to create and share class relations between collaborators. UML is the standard language for specifying, visualizing, constructing, and documenting the artifacts of software systems. UML is a pictorial language written with XML and has two major components for a diagram.
- Objects: Objects are the building blocks of UML. Typically, objects define classes and interfaces in the UML diagram.
- Relations: Relationships are mostly arrows from and to different objects in the diagram. They typically define inheritance, association, and modality.
UML is not just used to design OOP structures. It actually has many more uses, and there are lots of elements in UML that are not used for object-oriented design. We will leave those for now and only talk about the elements that are used for OOP.
Objects
Objects
UML can be used to create the following two diagrams for OOP design:
- Class Diagram
- Object Diagram
These components are nearly the same visually. The main difference is that Objects have underlinings under their name. Below is the visual representation of them:

Relations
Relations
Without relationships in UML, all the objects would be meaninglessly scattered in the diagram. Relations are depicted using lines and arrows that define the interactivity of different objects and classes. In the picture below, the most common relations are shown along with their meaning.

Now let's look at each one with an example:
Association
Association
Whenever two classes are connected to each other, an association relationship link can be used. You can use a simple name for the relationship close to the line. For example, in a game, a player will have a lot of save files. If we consider Player and SaveFile as classes, then we can create an association link for them like below:

You can also state the multiplicity of the association with a value near the class. See that * near the SaveFile class above? That means that a Player can interact with multiple SaveFile classes. You can also use a number if you know how many save files a player will have prior to runtime (e.g. A Player will always have N save files).
Aggregation
Aggregation
With aggregation, an object will always be referenced by other objects. This can be shown by an open diamond (a diamond without any fill color) in the UML. Previously we described aggregation and composition using the Vehicle class example. We will use the same example for the UML diagrams.

As you can see, a car can have multiple Passengers and one Driver within. Using the blank diamond, we can demonstrate that the lives of the Passengers and Drivers are not bound to the car.
Composition
Composition
If an object only contains one other object such that their lives are bound together, then we can show this relationship with the composition arrow in UML.

For example, a Car will always have 4 Doors and 4 Wheels. Compositions are shown with a filled-in diamond shape.
Generalization
Generalization
Generalization is a synonym of inheritance in the world of OOP. When a class is inherited from another class, then we can show this inheritance relationship with a simple arrow from the child class to the parent class.

Toyota Corolla and Ford Explorer are both popular cars in 2021. Since they are specific to Cars, they can be inherited from a generalization of Cars. Thus, they can be children of the general class Car.
Realization
Realization
Realization is also a type of inheritance but for interfaces. In a realization relationship, one entity (normally an interface) defines a set of functionalities as a contract and the other entity (normally a class) realizes the contract by implementing the functionality defined in the contract.
For example, a car is Drivable (people can drive it), so it should have a drive() function. Let's look at a coded example:
// Drivable.java
public interface Drivable {
public void drive();
}
// Car.java
public class Car implements Drivable {
// Must implements all functions of Drivable
public void drive() {
// Do the driving here
}
}
// Drivable.js
class Drivable {
drive() {
throw new Error("You have to implement the method drive!")
}
}
// Car.js
class Car extends Drivable {
// Must implement all functions of Drivable
drive() {
// Do the driving here
}
}
# Drivable.py
class Drivable:
def drive(self):
raise NotImplementedError("You have to implement the method drive!")
# Car.py
class Car(Drivable):
# Must implement all functions of Drivable
def drive(self):
# Do the driving here
pass
// Drivable.h
class Drivable {
public:
virtual void drive() = 0; // Pure virtual function
};
// Car.h
class Car: public Drivable {
public:
// Must implement all functions of Drivable
void drive() {
// Do the driving here
}
};
// Drivable.cs
public interface Drivable {
void Drive();
}
// Car.cs
public class Car : Drivable {
// Must implement all functions of Drivable
public void Drive() {
// Do the driving here
}
}
// Drivable.go
type Drivable interface {
Drive()
}
// Car.go
type Car struct{}
// Must implement all functions of Drivable
func (c Car) Drive() {
// Do the driving here
}
This code can be presented in UML by the following picture.

Conclusion
Conclusion
Aggregation vs composition is one of the most common questions asked in interviews where the job requires skills in OOP. Whenever you are asked to do an object-oriented design, you must follow the rules of UML diagrams. This makes it easier for others to understand relationships in your code without having to know your notation. UML can save a lot of time during the sketch up of a system. This lesson provides a very basic understanding of some advanced topics of OOP and visualizing the structure of an OOP design.
One Pager Cheat Sheet
One Pager Cheat Sheet
- By the end of this lesson, you will understand key
object-oriented programmingterms like association, aggregation, and composition and have a clear understanding of how to create diagrams representing these relations among classes. - Association is the relationship between two objects, which can be of the same or different type, and is usually represented as either a composition or an aggregation.
- Composition is a form of association in which an object's life is bound to that of its containing object, and all objects inside it die when the main object is deleted.
- - Composition means strong ownership: the part's lifecycle is tied to the whole.
- - Composition means the contained object is created, owned, and destroyed with the container.
- Aggregation allows for the referencing of other objects in a container object, without affecting the life cycle of the referenced objects.
- - Aggregation means loose ownership: the container references objects that can live independently.
- - Aggregation means one object references another, but the referenced object can survive independently.
Composition and aggregation are two forms of association, with the main difference being that composition involves a stronger form of relationship than aggregation, and compositied objects cannot be changed from the referrer, while aggregated objects can be removed or replaced.- The relationship between two objects is
association, meaning that they have some kind of connection with no logical dependency enforced. - Object typecasting is the process of converting one primitive data type to another or one class to another compatible class in the same type hierarchy, which can be done automatically or by manually upcasting or downcasting.
- - Casting changes how you view an object through a reference; it does not magically change the object's real runtime type.
- - Upcasting is safe toward a parent type; downcasting needs runtime compatibility with the actual object.
- The
Objectclass in Java is the parent of all classes and provides various useful methods such asgetClass(),hashCode(),equals()andtoString()that are inherited by all classes and can be referred to by a reference variable even if the type of the object is unknown. -
waitForAllis not a method that is provided within Java'sObjectclass. - UML provides a standard pictorial language for specifying and visualizing the objects and relationships of object-oriented designs, allowing efficient collaboration among software system collaborators.
- UML can be used to create Class Diagrams and
Object Diagramsvisually, the latter having underlinings under the Object's name. -
Relationshipsin UML are represented with lines and arrows that define the interactivity of objects and classes, such as association, inheritance, aggregation and composition. - The
PlayerandSaveFileclasses can be linked by anassociationrelationship, which can be indicated through multiplicity values near the related class. - Aggregation is shown in UML by an open diamond and can be demonstrated with the example of a car with multiple Passengers and one Driver, showing that their lives are not bound to the car.
- Objects can have a bound relationship, which is displayed as a
filled-in diamond shapein UML diagrams, dubbed the composition arrow. - Using
generalizationto show inheritance between classes, we can use a simple arrow to represent the relationship betweenToyota CorollaandFord Exploreras children of a more generalCarclass. - A class
implementsan interface torealizethe functions defined by the interface, as shown by the example ofCarimplementingDrivable. - UML diagrams can provide a valuable tool when deciding between
aggregationandcompositionwhen usingOOP, helping to visualize and understand relationships in code.