OOP in Java — Complete Notes

Object-oriented programming explained from scratch in plain language, then taken down to the details that matter in real code and in interviews.

00. What OOP actually is

Before any syntax: OOP is just a way of organising code so it mirrors how we naturally think about the world — as things that have properties and can do stuff.

Imagine you're describing your car to a friend. You don't list a thousand loose facts. You say: "It's a car. It's red, it's a Honda, it has 40,000 km on it, and it can start, accelerate, and brake." You bundled two things together without thinking about it:

  • Data — what the car is: colour, brand, kilometres driven.
  • Behaviour — what the car can do: start, accelerate, brake.

That bundle — data + the behaviour that works on that data, kept together in one unit — is an object. Object-Oriented Programming is simply the idea of building your whole program out of these self-contained bundles instead of one long list of loose commands.

The old style (procedural programming) is like a recipe: a long top-to-bottom list of steps and a pile of loose ingredients on the counter. OOP is like a kitchen full of appliances: the blender holds its own blades and knows how to blend, the oven holds its own heating element and knows how to bake. You don't manage the blades yourself — you just tell the blender "blend." Each appliance is an object.

Why anyone bothers with it

As programs grow past a few hundred lines, loose code becomes a tangled mess where everything can touch everything. OOP gives you four practical wins, and they map exactly onto the four pillars you'll learn below:

You want to… OOP gives you Pillar
Protect data from being corrupted by accident A wall around each object's internals Encapsulation
Reuse code instead of copy-pasting it New types built on top of existing ones Inheritance
Write one piece of code that works for many types The same call behaving correctly per object Polymorphism
Hide overwhelming complexity behind a simple handle Show the what, hide the how Abstraction
One sentence to remember

OOP = modelling your program as a collection of objects (data + behaviour bundled together) that talk to each other by calling each other's methods.

01. Class vs Object — the most important distinction

Beginners mix these up constantly. Get this one crystal clear and half of OOP clicks into place.

A class is the blueprint for a house. An object is an actual house built from that blueprint. From one blueprint you can build a whole street of houses — same design, but each with its own address, its own paint colour, its own family living inside. The blueprint isn't a house you can live in; it's the plan. The houses are the real things.

So a class defines what something looks like and can do (the template). An object is a concrete, living copy made from that class (also called an instance). "Creating an object" is often called instantiation.

Car — one blueprint, many carsCar.java
// THE CLASS — the blueprint. This is written once.
class Car {
// fields (the data every car has)
String colour;
String brand;
int kilometres;

// a method (behaviour every car can do)
void describe() {
    System.out.println(brand + " in " + colour + ", " + kilometres + " km");
}
}

public class Main {
public static void main(String[] args) {
    // THE OBJECTS — real cars built from the blueprint.
    Car myCar = new Car();      // object #1
    myCar.colour = "red";
    myCar.brand  = "Honda";
    myCar.kilometres = 40000;

    Car herCar = new Car();     // object #2, totally independent
    herCar.colour = "blue";
    herCar.brand  = "Toyota";
    herCar.kilometres = 12000;

    myCar.describe();   // Honda in red, 40000 km
    herCar.describe();  // Toyota in blue, 12000 km
}
}

Notice myCar and herCar share the same class but hold different data. Changing one never touches the other. That independence is exactly why objects are useful.

Vocabulary you'll hear

Instance = object. Instantiate = create an object. Instance variable / field / attribute / property / member variable = the data inside an object (all roughly the same thing). Interviewers switch between these words freely.

02. Fields, methods & constructors

The three things you put inside a class. Together they're called the members of the class.

Fields — the object's memory

Fields hold the object's state (its data). Each object gets its own copy. In the car example, colour, brand and kilometres are fields.

Methods — the object's actions

Methods are functions that live inside a class. They usually work on that object's own fields. describe() above is a method — it reads the object's fields and prints them.

Constructors — the "setup" routine

Setting each field one line at a time (like above) is clumsy. A constructor is a special method that runs automatically the moment an object is born, letting you set everything up in one go.

A constructor is like the hospital admission form filled in the second a baby is born — name, date, weight recorded immediately so the baby never exists in an "unfilled" state. Every object gets one at birth.

A constructor cleans this right upCar.java
class Car {
String colour;
String brand;
int kilometres;

// CONSTRUCTOR: same name as the class, no return type.
Car(String colour, String brand, int kilometres) {
    this.colour = colour;          // "this.colour" = the field
    this.brand = brand;            // "colour" (right side) = the parameter
    this.kilometres = kilometres;
}

void describe() {
    System.out.println(brand + " in " + colour + ", " + kilometres + " km");
}
}

// Now creating a fully-set-up car is a single clean line:
Car myCar = new Car("red", "Honda", 40000);
Constructor rules

Its name must match the class name exactly. It has no return type (not even void). If you write no constructor at all, Java quietly gives you a free empty one (the default constructor). The moment you write any constructor yourself, that free one disappears.

03. The new and this keywords

new — the "build it" command

new Car(...) does two jobs: it carves out memory for a fresh object, then runs the constructor to fill it in. It hands you back a reference — think of it as the object's address or a remote control pointing at that object. The variable myCar doesn't hold the car itself; it holds a pointer to where the car lives.

A reference variable is a TV remote. The remote isn't the TV — it just points at one. If two remotes are paired to the same TV, pressing volume-up on either changes the same screen. Likewise, if two variables reference the same object, changing it through one is visible through the other.

The classic beginner trap: == vs .equals()

== asks "are these two remotes pointing at the same TV?" (same object in memory). .equals() asks "do these two objects look the same?" (same contents). For text, always compare with a.equals(b), never a == b. More on this in the Object class section.

this — "the current object"

Inside a method or constructor, this means "the specific object I'm running on right now." Its two everyday uses:

  • Disambiguation: when a parameter has the same name as a field, this.colour = colour means "set my field to the value passed in." Without this, Java would think you're assigning the parameter to itself.
  • Constructor chaining: this(...) lets one constructor call another in the same class (covered under constructor chaining).
The Four Pillars

04. The Four Pillars of OOP

Everything else in OOP is built on these four ideas. If someone asks you "explain OOP," this is the answer. Memory hook: A PIE — Abstraction, Polymorphism, Inheritance, Encapsulation.

PILLAR 1

Encapsulation

Wrap data and its methods together, and hide the internals behind a controlled interface. "Keep your private stuff private."

PILLAR 2

Inheritance

Build a new class on top of an existing one, reusing its code. "A child gets the parent's traits, then adds its own."

PILLAR 3

Polymorphism

One name, many behaviours — the same call does the right thing for whatever object it lands on. "One interface, many forms."

PILLAR 4

Abstraction

Expose only the essentials and hide the messy detail. "You drive with a steering wheel, not the engine internals."

Encapsulation and abstraction sound similar and beginners blur them, so here's the clean split: encapsulation is about hiding data (a how-question: bundle and lock the internals). Abstraction is about hiding complexity (a what-question: show a simple surface, hide the mechanism). We'll nail the difference when we reach abstraction.

Pillar 1 · Encapsulation

A medicine capsule wraps the powder inside a shell — you swallow it without touching the contents, and you can't accidentally spill or tamper with the powder. Or think of an ATM: you press buttons on the outside, but the cash, the vault mechanism, and the account database are sealed away. You get a clean, safe interface; the dangerous internals are locked.

Encapsulation = bundling an object's data together with the methods that operate on it, then hiding the data behind those methods so nobody can reach in and mess with it directly. In Java you do it with two moves:

  • Mark the fields private so outside code cannot touch them.
  • Expose controlled public methods — getters (read) and setters (write) — that guard access and can enforce rules.
A bank account that can't go negativeBankAccount.java
class BankAccount {
private double balance;          // hidden — no one outside can touch it directly

public BankAccount(double opening) {
    this.balance = Math.max(0, opening);
}

// GETTER: read-only window into the data
public double getBalance() {
    return balance;
}

// SETTER-style methods that ENFORCE THE RULES
public void deposit(double amount) {
    if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
    balance += amount;
}

public void withdraw(double amount) {
    if (amount > balance) throw new IllegalArgumentException("Insufficient funds");
    balance -= amount;
}
}

// Outside code MUST go through the safe methods:
BankAccount acc = new BankAccount(500);
acc.deposit(200);           // ok  -> 700
acc.withdraw(1000);         // blocked: throws "Insufficient funds"
// acc.balance = -9999;     // COMPILE ERROR: balance is private. Corruption impossible.

Because balance is private, there is no way for buggy or malicious code to set it to a nonsense value. Every change funnels through deposit/withdraw, which enforce the rules. That's the whole point: you control every entry into your object's state.

✓ Why this is good
  • Invalid states become impossible to create.
  • You can change how balance is stored later without breaking anyone's code — they only ever saw the methods.
  • Validation, logging, limits all live in one place.
✗ Without encapsulation
  • A public double balance can be set to anything, anywhere.
  • One typo in some far-off file silently corrupts your data.
  • No single place to add a rule later.
Interview line

"Encapsulation is data hiding: I make fields private and expose public getters/setters so the class fully controls its own state and can enforce invariants." Bonus: mention it decouples the internal representation from the public API.

Pillar 2 · Inheritance

Traits pass down a family tree. A child inherits eye colour and height from a parent, then develops their own personality on top. In code, a new class can inherit all the fields and methods of an existing class, keep what fits, and add or change the rest — so you write the shared parts once.

Inheritance lets a class (the subclass / child) take on the members of another class (the superclass / parent) using the extends keyword. The relationship it models is IS-A: a Dog IS-A Animal, so Dog extends Animal.

Shared code lives once in the parentAnimals.java
// PARENT / superclass
class Animal {
protected String name;               // protected: visible to subclasses

Animal(String name) { this.name = name; }

void eat()  { System.out.println(name + " is eating"); }
void sleep(){ System.out.println(name + " is sleeping"); }
}

// CHILD / subclass — gets eat() and sleep() for free, adds bark()
class Dog extends Animal {
Dog(String name) {
    super(name);                     // call the parent's constructor first
}
void bark() { System.out.println(name + " says woof"); }
}

Dog d = new Dog("Bruno");
d.eat();    // inherited from Animal  -> Bruno is eating
d.sleep();  // inherited from Animal  -> Bruno is sleeping
d.bark();   // Dog's own method       -> Bruno says woof

Dog never re-declares eat() or sleep() — it just gets them. Write the common behaviour once in Animal; every animal type reuses it. That's the reuse win.

The super keyword

super means "the parent." super(name) calls the parent's constructor; super.eat() calls the parent's version of a method you may have overridden. A subclass constructor must call a parent constructor first — if you don't write super(...), Java inserts a silent super() for you.

Types of inheritance in Java

Type Shape Allowed in Java?
Single B extends A Yes
Multilevel C extends B extends A Yes
Hierarchical B and C both extend A Yes
Multiple (via classes) D extends B and C No — banned
Multiple (via interfaces) D implements B, C Yes
Why Java bans multiple inheritance of classes — the Diamond Problem

If D could extend both B and C, and both provided a method greet() with different code, then d.greet() would be ambiguous — which one runs? This is the "diamond problem." Java sidesteps it: a class extends at most one class, but can implement many interfaces (which historically carried no conflicting code). You get the flexibility without the ambiguity.

Don't overuse inheritance

Inheritance couples the child tightly to the parent — a change in the parent can silently break every child. A widely-taught rule is "favour composition over inheritance": often it's cleaner for a class to hold another object (HAS-A) than to extend it (IS-A). Only use extends when the child genuinely is a kind of the parent. See IS-A vs HAS-A.

Pillar 3 · Polymorphism

Polymorphism literally means "many forms." One method call, written once, produces the right behaviour depending on the actual object it runs against. It comes in two flavours.

Press the "play" button on any remote — a music player plays a song, a video player plays a movie, a slideshow advances a slide. One button, many outcomes, each correct for the device. That's polymorphism: same instruction, appropriate response per object.

a) Compile-time polymorphism — Method Overloading

Same method name, different parameter lists, in the same class. The compiler picks which one to run based on the arguments you pass. Decided while compiling → "compile-time" (also "static") polymorphism.

One name, several signaturesCalculator.java
class Calculator {
int add(int a, int b)            { return a + b; }
int add(int a, int b, int c)     { return a + b + c; }   // different count
double add(double a, double b)   { return a + b; }       // different types
}

Calculator c = new Calculator();
c.add(2, 3);        // 5     -> picks add(int,int)
c.add(2, 3, 4);     // 9     -> picks add(int,int,int)
c.add(2.5, 1.5);    // 4.0   -> picks add(double,double)
Overloading rule

The methods must differ in number or type or order of parameters. Changing only the return type is not enough — that won't compile. The parameter list is what distinguishes overloads.

b) Runtime polymorphism — Method Overriding

A subclass replaces a method it inherited, giving it new behaviour with the same signature. Which version runs is decided at runtime, based on the object's actual type — a mechanism called dynamic method dispatch.

Same call, different sound — chosen at runtimeSounds.java
class Animal {
void speak() { System.out.println("Some generic sound"); }
}
class Dog extends Animal {
@Override
void speak() { System.out.println("Woof"); }   // overrides parent
}
class Cat extends Animal {
@Override
void speak() { System.out.println("Meow"); }
}

// The magic: a parent-typed variable can hold any child object,
// and the CHILD'S method runs.
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.speak();   // Woof   (not "Some generic sound")
a2.speak();   // Meow

// This is what makes the loop below "just work" for any future animal:
Animal[] zoo = { new Dog(), new Cat(), new Animal() };
for (Animal a : zoo) a.speak();   // Woof, Meow, Some generic sound

The loop is written once against Animal, yet each object responds in its own way. Add a Cow class tomorrow and the loop needs zero changes. That is the power interviewers are testing when they ask about polymorphism.

Always write @Override

It's an annotation that tells the compiler "I intend to override a parent method." If you mistype the name or get the parameters wrong, the compiler catches it instead of silently creating a brand-new unrelated method. It's optional but every professional uses it.

Overloading Overriding
Where Same class Parent → child class
Signature Must differ Must be identical
Resolved Compile time (static) Runtime (dynamic)
Also called Static / early binding Dynamic / late binding

Pillar 4 · Abstraction

You drive a car with a steering wheel, pedals, and a gear stick. You have no idea (and don't need to know) how fuel injection, spark timing, or the transmission actually work. The complexity is hidden behind a simple, stable set of controls. That's abstraction: show what you can do, hide how it's done.

Abstraction = exposing only the essential features of something and hiding the messy implementation. You define what operations exist without committing to how they work. Java gives you two tools for this: abstract classes and interfaces.

Abstract classes

An abstract class is a half-finished blueprint. It can declare abstract methods — methods with a name but no body, a promise that "every real subclass must supply this." You cannot create an object of an abstract class directly; you must extend it and fill in the blanks.

"Every shape has an area — but each computes it differently"Shapes.java
abstract class Shape {
abstract double area();          // no body: a promise, not an implementation

void printArea() {               // a normal, fully-written method (shared)
    System.out.println("Area = " + area());
}
}

class Circle extends Shape {
double r;
Circle(double r) { this.r = r; }
@Override double area() { return Math.PI * r * r; }   // fills in the blank
}

class Rectangle extends Shape {
double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
@Override double area() { return w * h; }
}

// Shape s = new Shape();        // COMPILE ERROR: can't instantiate an abstract class
Shape s = new Circle(5);         // fine — Circle is concrete
s.printArea();                   // Area = 78.53... (uses Circle's area via polymorphism)

Shape guarantees every shape has an area() and gives them all a free printArea(), but refuses to guess how area is calculated — that's each subclass's job. Callers work with the simple idea "a shape has an area" and never see the formulas.

Encapsulation vs Abstraction — the clean split (asked constantly)

Encapsulation hides data (the state) using private + getters/setters — it's about protection. Abstraction hides implementation complexity using abstract classes/interfaces — it's about simplicity of the interface. Encapsulation = "how do I lock my data away." Abstraction = "how do I present a simple surface and hide the mechanism." They often work together.

05. Abstract class vs Interface

Both let you achieve abstraction, and interviewers love asking you to compare them. Here's the difference plus when to pick which.

An interface is a pure contract: a list of method signatures a class promises to provide. A class implements an interface, then must supply the actual code for every method (unless it's abstract too). Think of it as a job description — "to be a Printer, you must be able to print() and scan()" — with no say in how you do them.

A capability contractPayments.java
interface PaymentMethod {
void pay(double amount);         // implicitly public + abstract
boolean isAvailable();
}

class UpiPayment implements PaymentMethod {
@Override public void pay(double amount) { System.out.println("Paid " + amount + " via UPI"); }
@Override public boolean isAvailable() { return true; }
}

class CardPayment implements PaymentMethod {
@Override public void pay(double amount) { System.out.println("Charged " + amount + " to card"); }
@Override public boolean isAvailable() { return true; }
}

// Code depends only on the CONTRACT, so it works with any payment type — even
// ones written next year. This is "programming to an interface".
void checkout(PaymentMethod method, double total) {
if (method.isAvailable()) method.pay(total);
}

A class can implements as many interfaces as it likes — that's how Java delivers the benefit of multiple inheritance safely.

Aspect Abstract class Interface
Keyword to use it extends implements
How many at once Only one parent Many at once
Method bodies Can mix abstract + concrete Traditionally none; since Java 8 allows default/static methods
Fields Normal instance fields, any state Only public static final constants
Constructor Yes No
Access modifiers on methods Any (public/protected/private) Public by default
Models An "is-a" with shared code/state A "can-do" capability
Which one should I use?

Pick an abstract class when your subclasses are close relatives that share real code and state (Circle, Rectangle both are Shapes and share printArea()). Pick an interface when unrelated classes just need to share a capability (a Bird, a Plane, and a Drone can all Flyable.fly() without being related). Modern Java design leans heavily on interfaces.

Java 8+ changed interfaces

Interfaces can now have default methods (with a body, so you can add new methods to an interface without breaking existing implementers) and static methods. This blurred the old line a bit — but the core distinction (state + single inheritance vs capability + multiple inheritance) still holds.

Essential mechanics

06. Overloading vs Overriding — settle it forever

The single most confused pair in Java interviews. You already saw both under polymorphism; here's the side-by-side you can recite.

Different names, one letter apart, opposite meanings:

  • Overload = same class, same method name, different parameters. It's about offering convenient variants of one operation. Chosen by the compiler.
  • Override = child class replaces a parent's method with the same signature. It's about changing inherited behaviour. Chosen at runtime by the object's real type.
Mnemonic

Overload → "load up more parameters" (same class, more variants). Override → "ride over the parent's version" (replace inherited behaviour).

07. The static and final keywords

static — belongs to the class, not to any object

A normal (instance) field gives every object its own copy. A static field is shared by all objects — there's exactly one copy living on the class itself.

Think of a school. Each student has their own roll number (instance field — one per object). But the school's name is the same for everyone (static field — one shared copy). You'd also count total students once for the whole school, not per student — a perfect static counter.

Shared vs per-objectStudent.java
class Student {
static String school = "Delhi Public School";  // shared by ALL students
static int totalStudents = 0;                   // one shared counter
String name;                                    // each student their own

Student(String name) {
    this.name = name;
    totalStudents++;                            // bumps the single shared count
}

static int getTotal() { return totalStudents; } // static method: no object needed
}

new Student("Asha");
new Student("Ravi");
System.out.println(Student.getTotal());   // 2   -> called on the CLASS, not an object
System.out.println(Student.school);       // Delhi Public School
Static method limitation

A static method belongs to the class, so it has no this — it can't touch instance fields directly (there's no specific object to read them from). That's why main is static: the JVM runs it before any object exists. Use static for utility functions and shared data; use instance methods for per-object behaviour.

final — "this cannot change"

final on a… Meaning
variable Value can be assigned once, then never reassigned → a constant. E.g. final double PI = 3.14159;
method Subclasses cannot override it. Locks the behaviour.
class Nobody can extend it. E.g. Java's String is final — that's part of why it's safe and unchangeable.

08. super, this & constructor chaining

How objects get built up through an inheritance chain — and how constructors call each other.

this(...) calls another constructor in the same class. super(...) calls the parent's constructor. Both must be the very first statement in a constructor (so you can only use one of them, at the top).

Chaining, top to bottomChaining.java
class Vehicle {
String type;
Vehicle(String type) {
    this.type = type;
    System.out.println("Vehicle built: " + type);
}
}

class Car extends Vehicle {
int wheels;

Car() {
    this("Sedan");                 // calls the OTHER Car constructor below
}
Car(String model) {
    super("Car");                  // calls Vehicle's constructor FIRST
    this.wheels = 4;
    System.out.println("Car built: " + model);
}
}

new Car();
// Output order shows the chain runs parent-first:
//   Vehicle built: Car
//   Car built: Sedan
Order of construction

When you build a child, the parent is always fully constructed first (top of the family tree downward). That's why a super(...) call — implicit or explicit — always happens before the child's own constructor body runs.

09. Access modifiers — who can see what

These four keywords control the visibility of every field, method, and class. They're the enforcement mechanism behind encapsulation.

Modifier Same class Same package Subclass (other pkg) Anywhere
private
(default / package-private)
protected
public

Plain-English version, from most locked to most open:

  • private — only inside this class. Your default for fields.
  • default (write nothing) — visible within the same package only.
  • protected — same package, plus subclasses anywhere. Good for members meant for child classes.
  • public — visible to the entire world. Your default for the methods that form the class's public API.
Rule of thumb

Make fields private and methods as private as they can be while still doing their job. Expose the minimum. It's far easier to open access up later than to take it away once other code depends on it.

10. The Object class — the ancestor of everything

Every class in Java secretly extends Object. So every object you ever make inherits a handful of methods worth knowing — and two you'll frequently override.

Even if you write class Car {} with no extends, Java treats it as class Car extends Object. That's why every object already has methods like toString(), equals(), and hashCode(). Three that matter in daily code:

toString() — a readable text version of your object

By default it prints something ugly like Car@1b6d3586 (class name + memory hash). Override it to print something useful.

equals() and hashCode() — "are two objects meaningfully equal?"

By default equals() behaves like == (same object in memory). Usually you want "same contents count as equal" — two Point(2,3) objects should be equal even though they're separate objects. You override equals() to define that.

Value equality, done properlyPoint.java
import java.util.Objects;

class Point {
private final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }

@Override
public String toString() {           // readable output
    return "Point(" + x + ", " + y + ")";
}

@Override
public boolean equals(Object o) {    // same contents => equal
    if (this == o) return true;
    if (!(o instanceof Point)) return false;
    Point p = (Point) o;
    return x == p.x && y == p.y;
}

@Override
public int hashCode() {              // MUST match equals (see rule below)
    return Objects.hash(x, y);
}
}

Point a = new Point(2, 3);
Point b = new Point(2, 3);
System.out.println(a == b);        // false  -> different objects in memory
System.out.println(a.equals(b));   // true   -> same contents
System.out.println(a);             // Point(2, 3)  -> toString() kicks in
The equals/hashCode contract (a favourite trick question)

If you override equals(), you must override hashCode() too, and the rule is: equal objects must have equal hash codes. Break this and hash-based collections (HashMap, HashSet) misbehave — your object can "vanish" inside a HashSet because it's looked up by hash first. Always override both together.

11. IS-A vs HAS-A — inheritance vs composition

Objects relate to each other in two fundamental ways. Knowing which to reach for is a real design skill, not just trivia.

Relationship Question Java tool Example
IS-A Is X a kind of Y? Inheritance (extends/implements) A Dog IS-A Animal
HAS-A Does X contain/use a Y? Composition (a field) A Car HAS-A Engine

HAS-A (composition) means one object holds another as a field. It splits into two shades:

  • Aggregation — a "has-a" where the part can live on its own. A Team has Players, but players still exist if the team disbands. Loosely bound.
  • Composition — a strong "owns-a" where the part can't meaningfully exist without the whole. A House is composed of Rooms; destroy the house and the rooms go with it. Tightly bound.
Composition: a Car HAS-A EngineComposition.java
class Engine {
void start() { System.out.println("Engine roars to life"); }
}

class Car {
private final Engine engine = new Engine();   // Car HAS-A Engine

void drive() {
    engine.start();                           // delegate to the part it owns
    System.out.println("Car moves");
}
}
// Car reuses Engine's behaviour WITHOUT inheriting from it. Swap in a different
// Engine later and nothing else breaks. This flexibility is why designers say:
"Favour composition over inheritance"

Inheritance is rigid — the child is permanently welded to the parent's design, and deep hierarchies get brittle. Composition is flexible — you assemble behaviour from swappable parts. Reach for inheritance only when there's a true IS-A relationship; otherwise prefer HAS-A. This is one of the most repeated pieces of design wisdom in OOP.

12. SOLID — the five design principles

Once you're comfortable with the pillars, SOLID is the next level: five guidelines for writing OOP code that stays clean as it grows. Interviewers for anything beyond junior level expect at least a one-line grasp of each.

Letter Principle Plain meaning
S Single Responsibility A class should do one job and have one reason to change. Don't make a class that parses files and emails users and talks to the database.
O Open/Closed Open for extension, closed for modification. Add new behaviour by adding new code (new subclass/implementation), not by editing working code.
L Liskov Substitution A subclass must be usable anywhere its parent is, without surprises. If Square extends Rectangle breaks code that expected a Rectangle, you've violated it.
I Interface Segregation Prefer many small, focused interfaces over one giant one. Don't force a class to implement methods it doesn't need.
D Dependency Inversion Depend on abstractions (interfaces), not concrete classes. Your checkout() should take a PaymentMethod, not a specific UpiPayment.

You don't need to master these on day one — but recognising the names and giving a one-sentence explanation each will noticeably lift how senior you sound.

Where Java surprises you

13. Gotchas — OOP behaving unintuitively in Java

Everything above is OOP as a concept. This section is OOP as Java actually implements it — the eight places where the rules bend and where "what does this print?" questions come from. This is the gap between having read a tutorial and having written Java.

1 · Java is always pass-by-value — even for objects

People say "Java passes objects by reference." That is wrong, and the confusion causes real bugs. Java always passes by value. The subtlety: for an object, the value being copied is the reference (the remote control), not the object itself.

So the method gets its own copy of the remote. It can use that remote to change the TV (visible to the caller). But if it swaps its remote for a different TV, the caller's remote still points at the original.

Mutate: yes. Reassign: no.
class Box { int value; }

static void mutate(Box b)   { b.value = 99; }      // changes the SAME object
static void reassign(Box b) { b = new Box(); b.value = 42; }  // points local copy elsewhere
static void bump(int n)     { n = 100; }           // primitive: pure copy

Box box = new Box();
box.value = 1;
int num = 1;

mutate(box);    System.out.println(box.value);  // 99  -> caller SEES the change
reassign(box);  System.out.println(box.value);  // 99  -> NOT 42. reassignment was local.
bump(num);      System.out.println(num);        // 1   -> primitives never change
How to say it in an interview

"Java is strictly pass-by-value. For reference types, the reference itself is copied — so a method can mutate the object it points to, but reassigning the parameter has no effect on the caller."

2 · Fields are NOT polymorphic (field hiding)

Methods are dispatched at runtime based on the object's real type. Fields are not — they are resolved at compile time based on the reference type. Redeclaring a field in a subclass doesn't override it; it hides it, and both copies exist.

Same object, two answers, depending on the reference type
class Parent {
String name = "Parent";
String getName() { return name; }
}
class Child extends Parent {
String name = "Child";                      // HIDES, does not override
@Override String getName() { return name; } // this DOES override
}

Parent p = new Child();     // parent reference, child object
System.out.println(p.name);        // "Parent"  <- field: reference type wins
System.out.println(p.getName());   // "Child"   <- method: object type wins
System.out.println(((Child) p).name); // "Child" <- cast changes which field you see
Practical takeaway

Never redeclare a parent's field in a child. It's legal, it's confusing, and it is almost always a bug. Keep fields private and this problem disappears entirely.

3 · static, private and final methods cannot be overridden

Only instance methods participate in runtime polymorphism. Three exceptions:

  • static methods — redeclaring one in a child is method hiding, not overriding. Which one runs is decided by the reference type at compile time.
  • private methods — invisible to the subclass, so a same-named method there is just a brand-new unrelated method.
  • final methods — explicitly locked; the compiler rejects any attempt to override.
Method hiding vs method overriding, side by side
class Parent {
static void hello()    { System.out.println("static Parent"); }
void greet()           { System.out.println("instance Parent"); }
}
class Child extends Parent {
static void hello()    { System.out.println("static Child"); }   // HIDES
@Override void greet() { System.out.println("instance Child"); } // OVERRIDES
}

Parent p = new Child();
p.hello();   // "static Parent"    <- hiding: chosen by the REFERENCE type
p.greet();   // "instance Child"   <- overriding: chosen by the OBJECT type

4 · The full overriding rules (not just "same signature")

An override must satisfy more than a matching name and parameters:

Rule Detail
Same signature Name and parameter list must match exactly. Different params = overloading, not overriding.
Return type Same, or a subtype — this is called a covariant return type and is allowed. Animal reproduce() in the parent can become Dog reproduce() in the child.
Access modifier Cannot be more restrictive. A public method cannot become protected. It can get wider.
Checked exceptions Cannot throw new or broader checked exceptions. Narrower or fewer is fine. Unchecked exceptions are unrestricted.
Static / final / private Cannot be overridden at all (see above).

The logic behind all of these is one idea: a subclass must never break code written against the parent. If the parent promised public, the child can't take that away. That's the Liskov Substitution Principle enforced by the compiler.

5 · Upcasting, downcasting and ClassCastException

Upcasting (child → parent) is always safe and automatic — a Dog is an Animal. Downcasting (parent → child) is you telling the compiler "trust me, this really is a Dog." If you're wrong, it blows up at runtime.

Cast defensively with instanceof
Animal a = new Dog();      // UPCAST — implicit, always safe

// a.bark();               // won't compile: Animal has no bark()
Dog d = (Dog) a;           // DOWNCAST — explicit, works because it really is a Dog
d.bark();                  // now fine

Animal cat = new Cat();
Dog wrong = (Dog) cat;     // compiles, but throws ClassCastException at RUNTIME

// The safe way — check first:
if (cat instanceof Dog) {
((Dog) cat).bark();
}

// Java 16+ pattern matching does the check and cast in one step:
if (cat instanceof Dog dog) {
dog.bark();            // "dog" is already typed and ready
}
Why upcasting is the useful one

Upcasting is what makes polymorphism work — Animal a = new Dog() lets you write code against the general type. Needing to downcast frequently is often a design smell: it means you're fighting your own abstraction.

6 · Initialization order (the classic "what does this print?")

When an object is created, things run in a strict, non-obvious order. Statics run once, ever, when the class first loads. Everything else runs per object, parent-first.

Trace the output — this is the whole puzzle
class Parent {
static { System.out.println("1. Parent static block"); }
{ System.out.println("3. Parent instance block"); }
Parent() { System.out.println("4. Parent constructor"); }
}
class Child extends Parent {
static { System.out.println("2. Child static block"); }
{ System.out.println("5. Child instance block"); }
Child() { System.out.println("6. Child constructor"); }
}

new Child();
new Child();   // second time: statics do NOT run again

Output:

Output
1. Parent static block      <-- statics first, parent before child, ONCE only
2. Child static block
3. Parent instance block    <-- then per-object: parent fully built first
4. Parent constructor
5. Child instance block
6. Child constructor
3. Parent instance block    <-- second object: no static blocks this time
4. Parent constructor
5. Child instance block
6. Child constructor
The rule in one line

Static blocks (parent → child, once) → then for each object: instance blocks + constructor, parent fully completed before the child's body runs.

7 · Building a genuinely immutable class

An immutable object can never change after construction. This makes it automatically thread-safe, safe to share, and safe as a HashMap key. String is the famous example — that's why s.toUpperCase() returns a new string rather than modifying s.

Making one is a recipe with a hidden step people miss:

  • Mark the class final so nobody can subclass it and add mutable behaviour.
  • Make every field private final.
  • Provide no setters — only getters.
  • The step people forget: if a field is itself mutable (a List, a Date, an array), take a defensive copy both when storing it and when returning it. Otherwise the caller still holds a reference to your internals and can mutate them behind your back.
Defensive copies are what make it truly immutable
import java.util.*;

final class Person {                          // 1. final class
private final String name;                // 2. private final fields
private final List<String> hobbies;

Person(String name, List<String> hobbies) {
    this.name = name;
    this.hobbies = new ArrayList<>(hobbies);   // 4a. copy IN
}

public String getName() { return name; }       // 3. getters only, no setters

public List<String> getHobbies() {
    return new ArrayList<>(hobbies);           // 4b. copy OUT
    // Or: return Collections.unmodifiableList(hobbies);
}
}

List<String> list = new ArrayList<>(List.of("chess"));
Person p = new Person("Asha", list);

list.add("hacking");             // caller mutates their original list
p.getHobbies().add("hacking");   // caller mutates the copy they were handed
System.out.println(p.getHobbies());   // [chess]  -> the Person is untouched. 
Without the defensive copies

If the constructor did this.hobbies = hobbies; and the getter did return hobbies;, both lines above would have corrupted the "immutable" object. The final keyword only stops the reference from being reassigned — it does nothing to stop the contents of the list from being changed.

8 · Shallow vs deep copy

Directly related to the above. When you copy an object that holds references to other objects, the question is whether you copied the references or the things they point to.

Shallow copy Deep copy
What's duplicated The outer object only; nested objects are shared The outer object and everything it references, recursively
Effect Mutating a nested object through one copy is visible in the other The two copies are fully independent
In Java Default clone() behaviour; a copy-constructor that just assigns references You must copy each nested object yourself
The bug shallow copies cause
class Address { String city; Address(String c) { city = c; } }

class User {
String name;
Address address;

User shallowCopy() {
    User u = new User();
    u.name = this.name;
    u.address = this.address;              // SHARED reference — danger
    return u;
}

User deepCopy() {
    User u = new User();
    u.name = this.name;
    u.address = new Address(this.address.city);   // brand-new Address
    return u;
}
}

User original = new User();
original.name = "Asha";
original.address = new Address("Ranchi");

User shallow = original.shallowCopy();
shallow.address.city = "Delhi";
System.out.println(original.address.city);   // "Delhi"  <- the original CHANGED!

User deep = original.deepCopy();
deep.address.city = "Mumbai";
System.out.println(original.address.city);   // "Delhi"  <- original safe
Note on clone()

Java's built-in Object.clone() is shallow by default and widely considered a poorly designed API (you must implement Cloneable, handle a checked exception, and cast the result). Most modern Java code avoids it in favour of a copy constructor (new User(existingUser)) or a static factory. If asked, know what clone() does — but say you'd prefer a copy constructor.

Gotcha summary

Trap The truth
"Objects are passed by reference" No — always pass-by-value; the reference is what gets copied.
"Redeclaring a field overrides it" No — it hides it. Fields resolve by reference type, methods by object type.
"Any method can be overridden" No — static (hidden), private (invisible), final (locked).
"Override just needs a matching signature" Also: covariant return allowed, access can't shrink, no broader checked exceptions.
"A cast that compiles will work" No — downcasting fails at runtime with ClassCastException. Guard with instanceof.
"Constructors run first" No — static blocks, then instance blocks, then constructor; parent fully before child.
"final makes it immutable" No — final locks the reference, not the contents. Defensive copies are required.
"A copy is a copy" No — a shallow copy shares nested objects. Deep-copy when independence matters.
Lock it in

14. Interview Q&A & common gotchas

Rapid-fire answers to the questions that come up again and again.

What are the four pillars of OOP?

Abstraction, Polymorphism, Inheritance, Encapsulation (A PIE). Be ready to define each in one sentence and give a code example.

Difference between encapsulation and abstraction?

Encapsulation hides data (private fields + getters/setters, about protection). Abstraction hides implementation complexity (abstract classes/interfaces, about a simple interface).

Abstract class vs interface — when do you use which?

Abstract class for closely related types that share code and state (single inheritance). Interface for a capability that unrelated classes can share (multiple inheritance). Since Java 8 interfaces can have default methods.

Can Java do multiple inheritance?

Not with classes (diamond problem). Yes with interfaces — a class can implement many. That's the safe way to get the benefit.

Overloading vs overriding?

Overloading: same class, same name, different parameters, resolved at compile time. Overriding: child replaces parent's method with the same signature, resolved at runtime (dynamic dispatch).

Why must main be static?

So the JVM can call it without first creating an object of the class — static members belong to the class itself.

== vs .equals()?

== compares references (same object in memory). .equals() compares contents (once overridden). For objects, use .equals().

If you override equals(), what else must you do?

Override hashCode() too, keeping them consistent — equal objects must return equal hash codes, or hash-based collections break.

Can a constructor be inherited?

No. Constructors are not inherited, but a subclass constructor calls a parent constructor via super(...).

What is dynamic method dispatch?

The runtime mechanism where a call on a parent-typed reference runs the actual object's overridden method — the engine behind runtime polymorphism.

Inheritance or composition — which to prefer?

Favour composition (HAS-A) unless there's a true IS-A relationship. Composition is more flexible and less tightly coupled.

Is Java pass-by-value or pass-by-reference?

Always pass-by-value. For objects, the reference is copied — so a method can mutate the object, but reassigning the parameter doesn't affect the caller.

Can you override a static method?

No. Redeclaring it in a subclass is method hiding — resolved by the reference type at compile time, not the object type. Same goes for private (invisible) and final (locked).

Are fields polymorphic?

No. Methods dispatch on the object's real type; fields resolve on the reference type. Redeclaring a field hides it rather than overriding it.

What's a covariant return type?

An override may return a subtype of what the parent returned. That's legal. Access can't be narrowed, and you can't throw broader checked exceptions.

Does final make an object immutable?

No — it only stops the reference from being reassigned. The object's contents can still change. True immutability needs private final fields, no setters, a final class, and defensive copies of any mutable fields.

Shallow vs deep copy?

A shallow copy duplicates the outer object but shares its nested objects; a deep copy duplicates everything recursively. Default clone() is shallow — prefer a copy constructor.

What runs first when you create an object?

Static blocks (parent then child, once ever), then per object: instance blocks and constructor, with the parent fully built before the child's constructor body runs.

15. One-screen cheat sheet

The whole guide, compressed. Skim this the morning of an interview.

Concept One-liner
Class Blueprint. Defines fields + methods.
Object A real instance built from a class with new.
Constructor Runs at birth; same name as class; no return type.
Encapsulation Hide data: private fields + getters/setters.
Inheritance Reuse via extends; IS-A; use super for parent.
Polymorphism One call, many forms. Overload (compile) + override (runtime).
Abstraction Hide complexity via abstract classes / interfaces.
abstract class Half-built; can't instantiate; mixes abstract + real methods; one parent.
interface Pure contract of capabilities; implements; many at once.
this / super Current object / parent object.
static Belongs to the class, one shared copy, no this.
final Var = constant, method = no override, class = no extend.
Access private < default < protected < public.
== vs equals() Same object vs same contents.
Composition HAS-A; prefer over inheritance when there's no true IS-A.
SOLID Single-responsibility, Open/closed, Liskov, Interface-segregation, Dependency-inversion.
Pass-by-value Always. The reference gets copied — mutate yes, reassign no.
Field hiding Fields resolve by reference type; methods by object type.
Not overridable static (hidden), private (invisible), final (locked).
Init order Static blocks (once) → instance blocks → constructor; parent before child.
Downcasting Compiles but can throw ClassCastException. Guard with instanceof.
Immutability final class + private final fields + no setters + defensive copies.
If you remember only three things

1) Class is the blueprint, object is the built thing. 2) The four pillars are A PIE — Abstraction, Polymorphism, Inheritance, Encapsulation. 3) Overload = same class/different params/compile-time; Override = child replaces parent/same signature/runtime.

OOP in Java — The Complete Blueprint · a self-contained study & interview reference. Best read top to bottom once, then kept around as a cheat sheet.