Menu

Advanced OOP Concepts in Java

Advanced OOP Concepts

With classes, inheritance, polymorphism, and abstraction in hand, you already have the core toolkit. This final chapter rounds out the picture with a handful of concepts that explain how Java works under the hood, how objects relate to each other beyond simple inheritance, and a set of design guidelines professional developers lean on to keep OOP code healthy as it grows

Object Class in Java

Every class in Java, whether you realise it or not, ultimately inherits from a single class: java.lang.Object. If a class does not explicitly extend anything else, Java silently makes it extend Object. This is why even a brand-new, empty class already has methods like toString(), equals(), and hashCode() available.

class Book {

String title = "Java Basics";

}

Book b = new Book();

System.out.println(b.toString());   // something like Book@1b6d3586

System.out.println(b.equals(b));  // true — same object reference

Method

Purpose

toString()

Returns a String representation of the object; commonly overridden for readable output

equals(Object o)

Defines what it means for two objects to be considered equal; defaults to reference comparison

hashCode()

Returns an integer used by hash-based collections like HashMap and HashSet

getClass()

Returns runtime type information about the object

Without overriding it, equals() simply checks whether two references point to the exact same object in memory, the same default behaviour as the == operator on objects, which is why classes that represent meaningful values, like a custom Point or Money class, typically override both equals() and hashCode() together.

Dynamic Binding

Dynamic binding is the mechanism behind runtime polymorphism, introduced in Chapter 6. It refers to the JVM's process of deciding, while the program is actually running, which overridden method implementation to invoke, based on the object's real type rather than the type of the reference variable.

class Shape {

void draw() {

System.out.println("Drawing a generic shape");

}

}

class Circle extends Shape {

@Override

void draw() {

System.out.println("Drawing a circle");

}

}

Shape s = new Circle();

s.draw();   // Drawing a circle  bound at runtime, not compile time

This stands in contrast to static binding, used for overloaded methods, private methods, and static methods, where the compiler can determine exactly which method to call just by reading the code, without waiting to see what happens at runtime.

Message Passing

Objects in an OOP system do not modify each other's internal data directly; instead, they communicate by sending messages, which in Java simply means calling one another's public methods. One object 'asks' another to do something, or to report some information, without needing to know how that something gets done internally.

class Light {

private boolean isOn = false;

void turnOn() {

isOn = true;

System.out.println("Light is now ON");

}

}

class Switch {

void press(Light light) {

light.turnOn();   // Switch sends a message to Light

}

}

Switch never touches Light's isOn field directly, it cannot, since the field is private. It simply sends the message turnOn() and trusts Light to handle the details. This message-passing style is what makes encapsulated objects safe to combine into larger systems.

Association

Association describes a general relationship between two otherwise independent classes, where objects of one class use or interact with objects of another, but neither owns the other and both can exist perfectly well on their own. A Teacher and a Student are associated, a teacher teaches students, but deleting one does not delete the other.

class Student {

String name;

Student(String n) { name = n; }

}

class Teacher {

String name;

Teacher(String n) { name = n; }

void teach(Student student) {

System.out.println(name + " is teaching " + student.name);

}

}

Association is the broadest of these three relationship concepts. Aggregation and composition, covered next, are both more specific, stronger forms of association.

Aggregation

Aggregation is a 'has-a' relationship where one class holds a reference to another, but the referenced object has its own independent lifecycle; it can exist before, after, and entirely without the class holding the reference. A Department has Teachers, but a Teacher continues to exist even if the Department is dissolved.

class Teacher {

String name;

Teacher(String n) { name = n; }

}

class Department {

String deptName;

Teacher teacher;   // Department HAS-A Teacher, but doesn't own its lifecycle

Department(String dept, Teacher t) {

deptName = dept;

teacher = t;

}

}

Teacher t = new Teacher("Mr. Rao");

Department d = new Department("Mathematics", t);

// Even if 'd' is discarded, 't' (the Teacher) still exists independently

Composition

Composition is a stronger 'has-a' relationship where the contained object's lifecycle is entirely tied to the containing object. If the parent is destroyed, the parts it is composed of are destroyed along with it, because they have no independent existence outside the parent. A House is composed of Rooms; the rooms do not exist as meaningful objects floating outside any house.

class Room {

String type;

Room(String t) { type = t; }

}

class House {

private Room livingRoom;   // House owns and creates its own Room

House() {

livingRoom = new Room("Living Room");   // created inside, not passed in

}

}

The key difference from aggregation lies in who controls the object's creation and destruction. In aggregation, the contained object is typically created outside and merely handed in, as the Teacher was in section 10.5. In composition, the containing class creates and owns it directly, and the two live and die together.

Relationship

Strength

Lifecycle

Example

Association

General, loose

Fully independent on both sides

Teacher and Student

Aggregation

Weak 'has-a'

Part can outlive the whole

Department and Teacher

Composition

Strong 'has-a'

Part cannot outlive the whole

House and Room

Introduction to SOLID Principles

SOLID is a set of five guidelines for writing OOP code that stays flexible and maintainable as it grows, rather than becoming brittle and difficult to change. The five letters each stand for a separate principle, and together they describe what well-structured object-oriented design tends to look like in practice.

Letter

Principle

Core Idea

S

Single Responsibility Principle

A class should have exactly one reason to change, one clear job

O

Open/Closed Principle

Classes should be open for extension but closed for modification of existing code

L

Liskov Substitution Principle

A subclass object should be usable anywhere its superclass is expected, without breaking behaviour

I

Interface Segregation Principle

Prefer several small, specific interfaces over one large, general-purpose one

D

Dependency Inversion Principle

Depend on abstractions (interfaces), not on concrete implementations

A quick example of the first principle: a Report class that both formats a report and saves it to a database is doing two jobs, formatting and persistence, each of which might need to change for entirely unrelated reasons. Splitting it into a ReportFormatter and a ReportRepository gives each class a single, focused responsibility.

OOP Design Best Practices

  • Favour composition over inheritance when you simply need to reuse code; reserve inheritance for genuine 'is-a' relationships, since deep inheritance chains are harder to change safely later.
  • Keep fields private by default, and expose only the methods a class genuinely needs to share with the outside world.
  • Design to interfaces where possible, so that code depending on a class can work with any implementation that fulfils the same contract, not just one specific class.
  • Keep each class focused on a single responsibility; if you struggle to describe what a class does in one sentence without using the word 'and', it likely needs to be split.
  • Use meaningful, consistent naming for classes, fields, and methods, since well-named OOP code often reads almost like plain English describing the real-world entities it models.

Did You Know?

Many of these best practices, especially favouring composition over inheritance and depending on abstractions rather than concrete classes, are not just stylistic preferences. They directly reduce how much code breaks when requirements change later, which is the entire point of choosing OOP in the first place.