Appearance
Object Oriented Programming: The Four Pillars of Java
Object oriented programming, or OOP, is one of those topics that sounds abstract until you understand what problem it actually solves. Before you can appreciate the solution, you need to feel the pain of the problem it replaced.
Why OOP Exists: The Problem with Procedural Programming
Before OOP there was procedural programming. The classic example is the C language. In C, your entire program is a collection of functions. Data moves freely between them. Any function that receives a piece of data can do absolutely anything to it. The function that originally created that data has no way to stop this. There is no protection, no rules, no control.
This creates what is called tight coupling. There is no concept of data hiding. Functions matter more than the data they operate on. Your program is just a long sequence of instructions calling other instructions, and your data is exposed to everything.
| Procedural programming | OOP |
|---|---|
| Program is divided into functions | Program is divided into objects |
| No way to hide data; data moves freely between functions | Objects hide their data |
| Gives importance to functions | Gives importance to data |
| No overloading, no inheritance, no code reusability | Overloading, inheritance and code reusability are possible |
| Examples: C, Pascal, FORTRAN | Examples: Java, C++, Python |
OOP was invented to fix this. The core idea is that you model your program as a collection of real world entities called objects. A dog. A car. A bank account. A student. Every single object has exactly two things: properties, which are the observable things about it, and behavior, which is what it can do. A dog has a color, a breed, and an age. A dog can bark, sleep, and eat. Those properties are called data variables and those behaviors are called methods or data methods.
A class is the blueprint where you write those two things together. From one class you can create many independent objects, each carrying its own separate copy of the data. Because the data and the methods that operate on it live together inside one class, the class has complete control over its own state. No outside code can reach in and corrupt it unless you allow it. That single idea is the seed from which all four pillars grow.
Objects and Classes: The Foundation
Before covering the pillars, you need to be absolutely comfortable with what an object and a class are.
A class is a template. It describes what data an object of that type will hold and what methods it will have. An object is a live instance of that class, created at runtime and stored in memory on the Heap.
Here is the minimal example that captures this:
java
public class Student {
int age;
String address; // data variables: describe the state of a student
void updateAddress(String a) { this.address = a; } // data method
int getAge() { return age; }
void setAge(int age) { this.age = age; }
}
// One class can produce many completely independent objects
Student engineering = new Student();
engineering.setAge(23);
Student mba = new Student();
mba.setAge(25);
// engineering.age and mba.age are completely separate memory locations
// changing one does not affect the otherWhen you write new Student(), the JVM allocates fresh memory on the Heap for one object. Call new Student() a second time and you get a second completely independent object. The engineering student's age of 23 has absolutely nothing to do with the MBA student's age of 25. They share the class definition but each object owns its own copy of the data.
This is the most important thing to internalize. A class defines the shape. Objects are the real things that live in memory.
The Dog Example: Properties and Behavior
Consider a dog class. What observable things does a dog have? Color, breed, and age. Those are the properties, and they become data variables in the class. What can a dog do? It can bark, sleep, and eat. Those are the behaviors, and they become methods.
java
class Dog {
String color; // property
String breed; // property
int age; // property
void bark() { // behavior
System.out.println("Woof!");
}
void sleep() { // behavior
System.out.println("Zzz...");
}
void eat() { // behavior
System.out.println("Nom nom...");
}
}The Dog class is the blueprint. When you create a new Dog(), you get an actual dog object in memory. You can create a thousand dog objects from this single class, and each one has its own color, breed, and age values.
Now you understand the foundation. Let us cover the four pillars.
The Four Pillars at a Glance
Java has four fundamental concepts that make OOP what it is. Every interview on OOP will touch all four of these. They are abstraction, encapsulation, inheritance, and polymorphism.
| Pillar | Core idea | Real world analogy | How Java does it |
|---|---|---|---|
| Abstraction | Hide the how, expose only the what | Brake pedal: press it, car stops, you never see the hydraulics | interface and abstract class |
| Encapsulation | Bundle data and methods together, control who touches what | Medicine capsule: medicine inside, protective coating outside | class with private fields and public methods |
| Inheritance | Child class gets the accessible (non-private) variables and methods of the parent | A child inherits traits from a parent and adds their own | extends keyword |
| Polymorphism | Same method name, different behavior depending on context | Water is solid, liquid, or gas: same substance, many forms | Method overloading and method overriding |
Pillar 1: Abstraction
Think about pressing the brake pedal in a car. What do you know about what happens next? The car slows down and stops. That is all you know, and honestly that is all you need to know. You have no idea about the hydraulic pressure, the brake calipers, the pads squeezing the rotor. All of that is hidden from you.
Think about dialing a number on your phone and pressing the green call button. The call connects. How the signal travels through cell towers, gets converted, routed, and arrives at the other phone is entirely invisible to you. You see the input and the output. The middle is hidden.
That is abstraction. You expose only the essential feature that the user needs to interact with, and you hide every internal implementation step.
In Java, abstraction is achieved through interfaces and abstract classes. The interface defines the short list of things a user of the class can call. The implementing class is where the actual messy steps live, completely invisible to the outside world.
java
// This interface is what the user of a car interacts with
// It defines what a car CAN DO, not how it does it
interface Car {
void applyBrake();
void pressAccelerator();
void pressHorn();
}
// The implementation class fills in the hidden steps
class Sedan implements Car {
@Override
public void applyBrake() {
// These internal steps are hidden from anyone using this class
dropHydraulicPressure();
engageCallipers();
reduceWheelSpeed();
}
@Override
public void pressAccelerator() {
// internal fuel injection logic hidden here
}
@Override
public void pressHorn() {
// internal horn circuit logic hidden here
}
// These private methods are invisible to anyone outside this class
private void dropHydraulicPressure() { }
private void engageCallipers() { }
private void reduceWheelSpeed() { }
}The person driving the car writes myCar.applyBrake(). They get exactly the behavior they need without being bombarded with three internal methods they never asked about. If you upgrade the braking technology tomorrow from hydraulic to electromagnetic, the driver does not change a single line of their code. The interface stays the same. The internals change freely.
Why Does Abstraction Matter? The Interview Answer
Interviewers often ask why you would hide implementation details. There are three reasons worth knowing:
First, confidentiality. You decide which methods are visible to the outside world. The internal helper methods stay private. Second, simpler client code. The person using your class only sees what they actually need. They are not overwhelmed by details they did not ask for. Third, safety. If callers cannot reach the internal methods, they cannot accidentally break them.
Pillar 2: Encapsulation
Abstraction is the concept of hiding. Encapsulation is the mechanical way you actually achieve that hiding in Java.
Think about a medicine capsule. Inside the capsule is the medicine. The capsule has a protective coating around it. You cannot reach directly into the capsule and pull out the medicine. You have to take the whole capsule. The protective coating ensures the medicine inside is handled correctly.
Encapsulation works the same way. You bundle the data variables and the methods that operate on them together inside a single class. It is also called data hiding. Two steps achieve it:
- Declare the variables of the class
private. - Provide
publicgetter and setter methods to view and modify them.
The only way to read or change the data is then through the methods the class provides. The advantages are loosely coupled code and better access control and security.
java
class Dog {
// This field is private: nobody outside Dog can read or write it directly
private String color;
// Controlled read: the outside world can ask for the color through this method
public String getDogColor() {
return color;
}
// Controlled write: the outside world must go through this method to change color
public void setDogColor(String c) {
// The Dog class can validate, log, or reject the input before accepting it
if (c == null || c.isBlank()) {
throw new IllegalArgumentException("Color cannot be empty");
}
this.color = c;
}
}Now see what happens when you try to bypass the method:
java
Dog rottweiler = new Dog();
rottweiler.color = "black"; // COMPILE ERROR: color is private, cannot touch it
rottweiler.setDogColor("black"); // OK: goes through the controlled methodBecause color is private, every single read and every single write goes through a method. The Dog class gets to validate, log, or transform before accepting any change. If color were public, any code anywhere in your entire project could write rottweiler.color = null and the object would be in a corrupt invalid state with absolutely nothing to catch it.
The Getter Question: Does a Getter Break Encapsulation?
This is a classic interview trap. The interviewer asks: "If you make a field private but then give it a public getter, are you not just exposing the field anyway? Does that not break encapsulation?"
The answer is no. Reading data is perfectly fine. No class is fully useful without sharing some information with the outside world. Encapsulation does not mean nobody can ever see any data. It means only the Dog class owns its fields and only the Dog class maintains them. Everyone else must go through Dog's methods. Making the field private and providing a public getter keeps the class in full control because the getter is a method. The class controls what it returns. It could compute a transformed version, log the access, or apply any logic it wants. Direct field access gives you none of that control.
Access Specifiers
Encapsulation relies on access specifiers to define who can reach what:
Java has four: private, public, protected, and default (no keyword). For now two matter: public means anyone can access it, and private means only code inside the same class can. The other two are covered in the methods chapter.
For encapsulation, fields are typically private and the methods that expose them are public.
Pillar 3: Inheritance
A child class inherits the non-private fields and methods of its parent class. It can use them exactly as they are, or it can override the methods to provide its own different behavior. The keyword in Java is extends (a class can also inherit from an interface with implements, covered later).
Two exceptions: private members are not inherited, and constructors are never inherited. The child object still contains the parent's private fields; it just cannot use them directly and must go through the parent's methods.
Think about this in real life. A child inherits traits from their parents. Eye color, height, certain abilities. But the child also has their own unique personality, their own skills that the parent does not have. The parent does not inherit anything from the child. It flows one direction: parent to child.
java
// Vehicle is the parent class (also called superclass or base class)
class Vehicle {
boolean hasEngine;
boolean getEngine() {
return hasEngine;
}
}
// Car is the child class (also called subclass or derived class)
// It extends Vehicle, meaning it inherits everything Vehicle has
class Car extends Vehicle {
String carType;
String getCarType() {
return carType;
}
}
Car swift = new Car();
swift.getEngine(); // works: Car inherited this from Vehicle
swift.getCarType(); // works: Car's own method
Vehicle v = new Vehicle();
v.getCarType(); // COMPILE ERROR: parent cannot see child's methodsThe direction is strictly one way. The child class sees everything the parent makes accessible to it. The parent class sees nothing in the child. If you think of it as a family tree, properties flow downward, never upward.
Types of Inheritance in Java
Java supports several forms of inheritance, but not all of them:
Single inheritance means one class extends exactly one other class. Class B extends class A. This is the basic and most common form.
Multilevel inheritance means a chain of classes. Class C extends class B, and class B extends class A. The properties flow all the way down the chain.
Hierarchical inheritance means multiple classes all extend the same parent. Class B extends class A and class C also extends class A. Both children share the same parent.
Multiple class inheritance, where one class tries to extend two different classes simultaneously, is not allowed in Java. This is because of the diamond problem, and it is one of the most frequently asked interview questions on inheritance.
The Diamond Problem: Why Multiple Class Inheritance Is Forbidden
This is a hot interview topic. You need to know this cold.
Imagine class A defines a method called getEngine(). Now imagine class B also defines a method called getEngine() with the same signature but different behavior inside. If Java allowed class C to extend both class A and class B at the same time, then when you write c.getEngine(), the JVM would have no idea which version to call. Is it A's version or B's version? There is no way to resolve this ambiguity. Java's designers decided the simplest fix is to simply forbid it.
java
// This is NOT allowed in Java
// class C extends A, B { } // COMPILE ERROR
// This is the diamond problem: two parents define the same method
// Java cannot decide which one C should inheritWhy Interfaces Solve the Diamond Problem
Here is the follow up question interviewers always ask: "If multiple class inheritance is not allowed, why can a class implement multiple interfaces?"
The answer comes down to a simple rule about interfaces. An interface only defines a method signature. Historically, an interface cannot contain the actual implementation of a method. It just says "this method exists." Because there is no implementation in an interface, there is nothing to be ambiguous about. When class C implements both interface A and interface B, and both interfaces declare getEngine(), the compiler forces class C to write its own single implementation. There is exactly one getEngine() in class C and it belongs entirely to class C.
java
interface A {
boolean getEngine(); // just a declaration, no body
}
interface B {
boolean getEngine(); // just a declaration, no body
}
// This is allowed because C must write its own implementation
// There is no ambiguity: C provides the one and only implementation
class C implements A, B {
@Override
public boolean getEngine() {
return true; // C's own implementation, no conflict
}
}The Advantage of Inheritance
When interviewers ask what the advantage of inheritance is, the two key answers are code reusability and achieving polymorphism. If a parent class has logic you want to use, you do not rewrite it. You inherit it and use it directly. And as you will see with polymorphism, inheritance is what makes runtime polymorphism possible.
Pillar 4: Polymorphism
The word polymorphism comes from Greek. Poly means many. Morphism means form. Same thing, many forms.
Think about water. Water can be solid ice, liquid water, or steam gas. It is the same substance behaving in completely different ways depending on context. Think about a person. A person can be a father when they are with their children, a husband when they are with their spouse, and an employee when they are at work. Same individual, different roles, different behavior depending on the situation.
In Java, polymorphism means the same method name behaves differently in different situations.
Java has two completely different kinds of polymorphism. Many candidates confuse them because they share the same concept name. They are resolved at entirely different points in time and they work through completely different mechanisms.
Method Overloading: Compile Time Polymorphism
Method overloading is also called static polymorphism or compile time polymorphism. All three names refer to exactly the same thing. In an interview, if someone asks you to explain static polymorphism, or compile time polymorphism, or method overloading, they are asking for the same explanation.
Here is what overloading is. Within the same class, you can have multiple methods that share the same name, as long as their parameter lists differ. The difference can be in the number of parameters, or in the types of the parameters. The compiler figures out which method to call at compile time, before the program even runs, based on the arguments you pass at the call site.
java
class Sum {
// Three methods with the same name: doSum
// Each differs in its parameter list
int doSum(int a, int b) {
return a + b; // adds two integers
}
String doSum(String a, String b) {
return a + b; // concatenates two strings
}
int doSum(int a, int b, int c) {
return a + b + c; // adds three integers
}
}Now when you use this class:
java
Sum calc = new Sum();
calc.doSum(5, 2); // compiler picks the first method: two integers
calc.doSum("hi", "there"); // compiler picks the second method: two strings
calc.doSum(3, 4, 2); // compiler picks the third method: three integersAt compile time, Java already has all the information it needs. It looks at what arguments you are passing and picks the matching method. The method name is the same for all three, but the arguments distinguish them.
The Overloading Trap: You Cannot Overload by Return Type Alone
This is a very frequently asked interview question. Can you overload a method by changing only the return type?
java
class Sum {
int doSum(int x, int y) {
return x + y;
}
// Is this valid overloading? Parameters are the same, only return type changed
String doSum(int x, int y) { // COMPILE ERROR: not valid overloading
return x + " plus " + y;
}
}This is not valid. You cannot overload on the basis of return type alone. Here is why. Think about a caller that writes calc.doSum(5, 2) and does not even bother to capture the return value. They just call it and move on. The compiler has to decide which method to bind to that call. It looks at the arguments: two integers. Now it sees two methods that both accept two integers. Which one should it call? It cannot use the return type to decide because the caller is not using the return value at all. There is no information available to resolve the ambiguity. So Java simply does not allow it. Return type is irrelevant to method overloading. Only the parameters matter.
Similarly, just renaming the parameters does not create a new overload. The parameter names are invisible to the caller. Only the types and count of parameters matter.
Method Overriding: Runtime Polymorphism
Method overriding is also called dynamic polymorphism or runtime polymorphism. Again, all three names mean exactly the same thing.
Overriding happens when a child class reimplements a method from its parent class with the same method name and the same parameters. The return type must be the same, or a subtype of the parent's return type. Only the body, what the method actually does, differs.
The key difference from overloading is when the decision is made. With overloading, the compiler decides at compile time. With overriding, the JVM decides at runtime based on the actual object type sitting in memory.
java
class A {
boolean getEngine() {
return true; // A's version of getEngine
}
}
class B extends A {
@Override
boolean getEngine() {
return false; // B overrides it with its own behavior
}
}Now consider what happens when you call getEngine():
java
// If you create a B object and call getEngine, it calls B's version
B bObj = new B();
bObj.getEngine(); // returns false: B's version runs
// If you create an A object and call getEngine, it calls A's version
A aObj = new A();
aObj.getEngine(); // returns true: A's version runsThe runtime polymorphism part is most powerful when you use a parent type variable to hold a child type object:
java
// The variable is declared as type A
// But the actual object in memory is a B
A ref = new B();
// Which getEngine runs?
// The JVM looks at the actual object on the Heap, not the variable type
// The object is a B, so B's getEngine runs
ref.getEngine(); // returns false: B's version runs at runtimeThis is called dynamic dispatch. The variable type says A, but the JVM ignores that at runtime and looks at the real object. The real object is a B, so B's method runs. The decision is made at runtime, not at compile time. That is why it is called runtime polymorphism.
How the JVM Finds a Method: The Lookup Rule
When you call a method on an object, the JVM follows a specific order. It first checks the actual runtime class of the object. If the method is present there, it calls it. If the method is not found in the child class, the JVM walks up to the parent class and checks there. This is why when you create a B object and call a method that only exists in A, it still works. The JVM did not find it in B, so it walked up to A.
This lookup rule is also why overriding works. If the child has its own version, that version is found first and the parent's version is never reached.
The Power of Runtime Polymorphism in Practice
Here is why this matters in real programs:
java
class Animal {
void makeSound() {
System.out.println("...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bark!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow!");
}
}
// A shelter holds a list of Animals
// At runtime, the actual objects are Dogs and Cats
List<Animal> shelter = new ArrayList<>();
shelter.add(new Dog());
shelter.add(new Cat());
shelter.add(new Dog());
// This loop does not care what the specific type is
// Each object knows its own version of makeSound
for (Animal a : shelter) {
a.makeSound(); // Dog says Bark, Cat says Meow, Dog says Bark
}The loop is written once and never changes. If you add a Parrot class tomorrow that extends Animal and overrides makeSound() with "Squawk!", you just add a new Parrot() to the list. The loop handles it automatically. Zero changes to the loop. This is the practical power of runtime polymorphism.
Overloading vs Overriding: The Full Comparison
This table comes up in nearly every Java interview. Know it:
| Property | Method Overloading | Method Overriding |
|---|---|---|
| Other names | Static polymorphism, compile time polymorphism | Dynamic polymorphism, runtime polymorphism |
| Where it happens | Inside the same class | Parent class and child class |
| Method name | Same | Same |
| Parameters | Must differ in type or count | Must be identical |
| Return type | Ignored for method resolution | Must be identical or covariant |
| When decided | Compile time, based on argument types | Runtime, based on actual object type |
The one line summary: overloading is in the same class, same name, different arguments, decided at compile time. Overriding is parent and child, exactly same signature, different body, decided at runtime based on which object you created.
Relationships: is a and has a
Before wrapping up, there is one more concept that is very frequently tested in interviews, especially low level design interviews. Understanding these two relationship types is what separates candidates who think in objects from those who just know syntax.
is a Relationship
is a means inheritance. When class Car extends class Vehicle, you can say that a car is a vehicle. That is the is a relationship. Dog is an Animal. A student is a person.
In code, is a is expressed through extends. The child class can be used anywhere the parent class is expected. If someone asks you in an interview to describe an is a relationship, they are asking you about inheritance. These two terms are completely interchangeable.
java
class Vehicle { }
// Car IS-A Vehicle
class Car extends Vehicle { }
// Dog IS-A Animal
class Animal { }
class Dog extends Animal { }has a Relationship
has a means that one class contains an object of another class as a data member. This is not inheritance. The class does not extend the other. It just holds an instance of it.
Think about a student and a course. A student has courses. You represent this in code by giving the Student class a variable of type Course or a list of Course objects.
java
class Course {
String courseName;
}
class Student {
String name;
Course course; // Student HAS A Course: one to one
List<Course> courses; // Student HAS A list of Courses: one to many
}A has-a relationship is also called association: a relationship between two different objects. It can be one to one, one to many, or many to many. The student example above shows both. One student can have one course, or one student can have many courses. And for many to many: one student can take many courses, and one course like English can be taken by many students. So you would have a list of courses in Student and a list of students in Course.
Aggregation vs Composition: Weak vs Strong has a
This goes deeper and this absolutely comes up in interviews. has a has two subtypes based on how tightly the lifecycles of the objects are connected.
Aggregation is a weak has a relationship. The two objects can survive independently. If you destroy one object, the other continues to exist just fine.
The classic example is a school and its students. A school has a list of student objects. But if you destroy the school object, the student objects do not get destroyed. The students enrolled somewhere else. The students existed before the school was created and they continue to exist after the school is gone. Their lifecycles are independent. That independent survivability is what makes it aggregation.
Composition is a strong has a relationship. One object cannot exist without the other. If you destroy the container, the contained objects are destroyed with it.
The classic example is a school and its rooms. A school has rooms. But when you create a school object, the room objects are created inside it. When you destroy that school object, the room objects are destroyed along with it. Room 101 of School A has no meaning without School A. The room cannot survive independently. That tight lifecycle coupling is what makes it composition.
java
// Aggregation: School has students, but students exist independently
class SchoolWithStudents {
// Student objects were created elsewhere and passed in
// Destroying a School object does not destroy these Student objects
private List<Student> students;
}
// Composition: School has rooms, rooms do not exist outside the school
class SchoolWithRooms {
// Room objects are created inside the School and never handed out
// Once this School becomes unreachable, its Rooms become unreachable too
private final List<Room> rooms = List.of(new Room("101"), new Room("102"));
}The critical point that interviewers test: aggregation and composition are about object lifetime at runtime, not about class files at compile time. If you delete a class file, yes, you get compilation errors everywhere that class was referenced. That is not the point. The point is whether the end of one object's life ends the other's too. In Java you never destroy objects explicitly; "destroyed" here means "becomes unreachable and eligible for garbage collection". In a composition, nothing outside the owner holds a reference to the parts, so they become unreachable together. Weak relationship means both objects survive independently. Strong relationship means ending one ends the other.
Another common example for aggregation and composition: a bike has an engine. If you model this as composition, destroying the bike destroys the engine. If you model it as aggregation, the engine could be removed and put in another bike. Whether you choose aggregation or composition depends on the real world behavior you are modeling.
Check yourself
- Explain abstraction and encapsulation in one sentence each. How are they related?
- A field is
privatebut has a public getter. Does that break encapsulation? - Why can a class not extend two classes, yet it can implement two interfaces that declare the same method?
- Can you overload a method by changing only its return type? Why?
A ref = new B();whereB extends Aand both definegetEngine(). Which version runs, and when is that decided?- What is the difference between aggregation and composition?
Answers
- Abstraction hides how something works and exposes only what it does (interfaces, abstract classes). Encapsulation is the mechanism: bundle data with its methods and control access with
private. - No. The class still owns the field; the getter is a method, so the class controls what it returns and can validate, compute, or log.
- With two parent classes, a call like
c.getEngine()could not decide which parent's implementation to run (the diamond problem). The interface methods only declaregetEngine(), so the implementing class must write the one and only implementation, and there is nothing to choose between. - No. A call like
calc.doSum(5, 2)gives the compiler only the name and argument types; the return type cannot pick between two methods with the same parameters. B's version, decided at runtime by the actual object type (dynamic dispatch). The variable typeAonly decides which methods you are allowed to call.- Both are "has-a". Aggregation: parts live independently of the owner (a school and its students). Composition: parts are created and owned by the container and become unreachable with it (a school and its rooms).