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.










