AlgoDaily

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 Ownership Snapshot

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.

Composition Lifecycle Map

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 Reference Snapshot

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.

Aggregation Survival Test

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.

Aggregation vs Composition vs 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 A disappears, what happens to object B?

  • Association: A only knows about or uses B. Their lifetimes are independent.
  • Aggregation: A has B, but B can still exist without A.
  • Composition: A owns B. If A is destroyed, B should 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.

Ownership Quick Check

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:

  1. Upcasting: When you cast an object to one of its parent class types (i.e., from Rectangle to Shape).
  2. Downcasting: When you cast an object to one of its subtypes (i.e., from Shape to Rectangle)

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.

Reference vs 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.

Casting Direction Guide

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:

  1. public final Class getClass()
  2. public int hashCode()
  3. public boolean equals(Object obj)
  4. protected Object clone()
  5. public String toString()
  6. public final void notify()
  7. public final void notifyAll()
  8. public final void wait(long timeout) throws InterruptedException
  9. public final void wait(long timeout,int nanos) throws InterruptedException
  10. public final void wait() throws InterruptedException
  11. protected 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.

  1. Objects: Objects are the building blocks of UML. Typically, objects define classes and interfaces in the UML diagram.
  2. 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:

UML

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.

UML Relationships

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:

Association

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.

Aggregation

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.

Composition

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.

Generalization

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.

Realization

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 programming terms 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 Object class in Java is the parent of all classes and provides various useful methods such as getClass(), hashCode(), equals() and toString() that are inherited by all classes and can be referred to by a reference variable even if the type of the object is unknown.
  • waitForAll is not a method that is provided within Java's Object class.
  • 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 Diagrams visually, the latter having underlinings under the Object's name.
  • Relationships in UML are represented with lines and arrows that define the interactivity of objects and classes, such as association, inheritance, aggregation and composition.
  • The Player and SaveFile classes can be linked by an association relationship, 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 shape in UML diagrams, dubbed the composition arrow.
  • Using generalization to show inheritance between classes, we can use a simple arrow to represent the relationship between Toyota Corolla and Ford Explorer as children of a more general Car class.
  • A class implements an interface to realize the functions defined by the interface, as shown by the example of Car implementing Drivable.
  • UML diagrams can provide a valuable tool when deciding between aggregation and composition when using OOP, helping to visualize and understand relationships in code.
Open interactive view →