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.
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 all the 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. Then you use access specifiers, specifically the private keyword, to make sure nobody outside the class can directly touch the data. The only way to read or change the data is through the methods the class provides.
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:
public means accessible from anywhere in the entire program. private means accessible only within the same class, nowhere else. protected means accessible within the same package and also in any subclass. The default, when you write no keyword at all, means accessible only within the same package.
For encapsulation, fields are typically private and the methods that expose them are public.
Pillar 3: Inheritance
A child class inherits every variable and every method from its parent class. It can use them exactly as they are, or it can override them to provide its own different behavior. The keyword in Java is extends.
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 in the parent. 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
}
}Java 8 introduced default methods in interfaces, which do have bodies. If two interfaces both provide a default implementation of the same method and a class implements both, the compiler forces the class to override the conflicting method. The class always wins. If you refuse to override, it is a compile error.
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 exact same signature. Same method name, same return type, same parameters. The only thing that can differ is what the method actually does internally.
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 |
| Access modifier | Can change freely | Cannot be more restrictive than parent |
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
}has a relationships 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 School {
// 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 School {
// Room objects are created inside the School and are part of it
// Destroying this School object also destroys these Room objects
private List<Room> rooms;
}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 destroying one object instance in memory causes another object instance to also be destroyed. 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.
Interview Quick Reference
When an interviewer says "explain OOP to me without jargon," here is the one sentence per pillar:
Abstraction: you press the brake pedal and the car stops, you have no idea about the hydraulics and you do not need to.
Encapsulation: a medicine capsule holds the medicine inside a protective coating, you cannot reach in and grab the medicine directly.
Inheritance: a child inherits traits from their parent and also develops their own unique ones.
Polymorphism: the word "run" means something completely different when a person runs, a program runs, and a river runs.
When asked about compile time vs runtime polymorphism, remember: compile time means the decision is made before the program runs, based on the arguments in the code. Runtime means the JVM makes the decision while the program is executing, based on the actual object in memory.
When asked about the diamond problem, remember: it is the ambiguity that arises when two parent classes define the same method and a child tries to inherit from both. Java forbids multiple class inheritance to prevent this. Interfaces work because they only declare methods without implementing them, so the implementing class is forced to write exactly one implementation with no ambiguity.
When asked about aggregation vs composition, remember: both are has a relationships. Aggregation is weak: both objects live and die independently. Composition is strong: the contained object dies when the container dies.
These are the building blocks of every Java program you will ever write. Every design pattern, every framework, every system you build in Java rests on these four pillars. Get them into your bones and everything else will click into place.