- Encapsulation — bundle data with the methods that operate on it and hide internal state behind a public API (private fields + getters/setters).
- Abstraction — expose what an object does, hide how, via interfaces and abstract classes.
- Inheritance — a subclass reuses and extends a superclass's members
(
extends). - Polymorphism — one reference type, many runtime forms; the actual object's overridden method is called.
class Animal { String sound() { return "..."; } }
class Dog extends Animal { String sound() { return "Woof"; } } // inheritance + override
Animal a = new Dog(); // polymorphism
a.sound(); // "Woof"
A good one-liner: encapsulation hides data, abstraction hides complexity, inheritance reuses behavior, polymorphism varies it.
Inheritance lets a subclass acquire the fields and methods of a superclass with
extends, modeling an is-a relationship and enabling reuse + polymorphism.
class Vehicle { void start() { } }
class Car extends Vehicle { void honk() { } } // Car is-a Vehicle
Java allows single inheritance of classes only — a class can extend exactly
one superclass (to avoid the "diamond problem"). It can implement multiple
interfaces, which is how Java gets multiple-type flexibility without
multiple class inheritance. final classes can't be extended at all.
java.lang.Object is the root of every class hierarchy — if you don't
extends anything, you implicitly extend Object. So every object has its
methods, the most important being:
equals(Object)/hashCode()— value equality and hashing.toString()— string representation.getClass()— the runtime class.clone(),finalize()(deprecated), and thewait/notifyfamily for threading.
class Foo { } // implicitly: class Foo extends Object
Object o = new Foo();
o.toString(); // "Foo@1b6d3586" by default
Rule of thumb: because everything is-an Object, an Object reference (or
Object[]) can hold anything — the basis of pre-generics collections.
- Single — one subclass, one superclass. ✅
- Multilevel — a chain (
C extends B extends A). ✅ - Hierarchical — many subclasses of one parent. ✅
- Multiple inheritance of classes — extending two classes. ❌ (not allowed)
- Multiple inheritance of types — implementing many interfaces. ✅
class A { }
class B extends A { } // single
class C extends B { } // multilevel
class D extends A { } // hierarchical (with B)
class E implements I1, I2 { } // multiple interfaces — OK
Rule of thumb: Java forbids multiple class inheritance but allows multiple interface inheritance — you get many capabilities, but a single implementation lineage.
To avoid the diamond problem: if B and C both extended A and overrode
a method, and D extended both B and C, the compiler couldn't tell which
version D inherits. C++ allows it and pays for it with complexity (virtual
inheritance).
// Hypothetical — illegal in Java:
// class D extends B, C { } // which greet() does D get?
interface B { default String greet() { return "B"; } }
interface C { default String greet() { return "C"; } }
class D implements B, C {
public String greet() { return B.super.greet(); } // YOU resolve it
}
Java permits multiple interface inheritance because conflicting default
methods force the class to resolve the ambiguity explicitly. Rule of
thumb: Java trades raw power for predictability.
super refers to the immediate superclass. It does two jobs: call the
parent constructor (super(args), must be the first statement) and call an
overridden parent method or access a parent field (super.method()).
class Animal {
Animal(String name) { }
void describe() { System.out.println("an animal"); }
}
class Dog extends Animal {
Dog(String name) {
super(name); // chain to parent constructor
}
@Override void describe() {
super.describe(); // reuse parent behavior
System.out.println("...specifically a dog");
}
}
If you don't call super(...) explicitly, the compiler inserts a no-arg
super() — which fails to compile if the parent has no no-arg constructor.
No — constructors are not inherited. But every subclass constructor must
invoke a superclass constructor first, via an explicit super(...) or an
implicit no-arg super() the compiler inserts.
class Base {
Base(int id) { } // no no-arg constructor
}
class Sub extends Base {
Sub() { super(1); } // MUST call super(int) explicitly
// Sub() { } // would NOT compile — implicit super() missing
}
So if a parent defines only parameterized constructors, the child is forced to call one. Rule of thumb: inheritance reuses methods and fields, not constructors — the child builds the parent's slice of itself by chaining up.
- Upcasting — treating a subclass object as its superclass type. Always safe, implicit. Enables polymorphism.
- Downcasting — casting a superclass reference back to a subclass. Needs an
explicit cast and can throw
ClassCastExceptionif the object isn't actually that subtype — guard it withinstanceof.
Animal a = new Dog(); // upcast — implicit, safe
Dog d = (Dog) a; // downcast — explicit
if (a instanceof Cat c) { // pattern: test before casting
c.meow();
}
Rule of thumb: upcast freely to program to the supertype; downcast rarely,
and only after an instanceof check.
instanceof tests whether an object is an instance of a given type (or subtype),
returning a boolean. Since Java 16, pattern matching lets you bind a
variable in the same expression, avoiding a separate cast.
Object o = "hello";
if (o instanceof String) { } // classic
if (o instanceof String s) { // pattern matching
System.out.println(s.length()); // s is already a String
}
null instanceof X is always false. Rule of thumb: heavy instanceof
chains are often a smell — prefer polymorphism (let each type implement the
method) unless you're at a true type boundary (deserialization, equals).
Inheritance (is-a) tightly couples a subclass to its parent's implementation; changes to the base can silently break subclasses (the "fragile base class" problem). Composition (has-a) builds behavior by holding other objects and delegating — looser coupling, more flexible.
// inheritance misused: a Stack is-a List? leaks all List methods
class Stack<T> extends ArrayList<T> { }
// composition: Stack HAS a list, exposes only stack operations
class Stack<T> {
private final List<T> items = new ArrayList<>();
void push(T t) { items.add(t); }
T pop() { return items.remove(items.size() - 1); }
}
Guideline ("favor composition over inheritance"): use inheritance only for a true is-a with a stable base designed for extension; otherwise compose. It also sidesteps single-inheritance limits.
- is-a -> inheritance. A
Caris aVehicle->class Car extends Vehicle. - has-a -> composition/aggregation. A
Carhas anEngine-> theCarholds anEnginefield.
class Engine { }
class Car extends Vehicle { // is-a Vehicle
private final Engine engine = new Engine(); // has-a Engine
}
Modeling tip: if you can't truthfully say "X is a Y," don't use inheritance —
reach for has-a (composition) instead. Misusing is-a (e.g. Stack extends Vector) is a classic design smell.
Both are has-a, differing in lifecycle ownership:
- Composition — the part can't exist without the whole; the whole owns and
creates it (a
Houseand itsRooms). Strong ownership. - Aggregation — the part can exist independently and may be shared (a
Teamand itsPlayers; players outlive the team).
class House {
private final Room room = new Room(); // composition: created/owned here
}
class Team {
private final List<Player> players; // aggregation: passed in, shared
Team(List<Player> players) { this.players = players; }
}
Rule of thumb: "owns and destroys together" = composition; "references but doesn't own" = aggregation.
Three ways, by strength:
finalclass — outright bansextends(e.g.String).private/package-private constructors — no outside subclass can callsuper(...), so it can't be extended elsewhere.sealedclass (Java 17+) — allows only a namedpermitslist of subclasses.
final class Money { } // no subclasses at all
sealed class Shape permits Circle, Square { } // only these two
Reasons: protect invariants (an immutable class shouldn't be subclassed into
mutability), security, and clearer design. Rule of thumb: design for
inheritance and document it, or prohibit it with final/sealed.
- Cohesion — how focused a class is on a single responsibility. High
cohesion is good: a
UserValidatorthat only validates users. - Coupling — how dependent classes are on each other's internals. Low coupling is good: classes interact through small, stable interfaces.
// low coupling: depends on an interface, not a concrete logger
class OrderService {
private final Logger log; // injected abstraction
OrderService(Logger log) { this.log = log; }
}
The goal of good OO design is high cohesion, low coupling — classes that do one thing well and lean on each other as little as possible, which makes systems easier to change and test.
The L in SOLID: a subtype must be usable anywhere its supertype is expected, without breaking the program's correctness. A subclass that strengthens preconditions, weakens postconditions, or throws on inherited operations violates it.
// Classic violation: Square is-a Rectangle?
class Rectangle { void setWidth(int w){} void setHeight(int h){} }
class Square extends Rectangle {
void setWidth(int w){ /* also sets height — breaks setWidth's contract */ }
}
// code that sets width then expects height unchanged now fails
The fix is usually composition or a shared abstraction, not inheritance. Rule of thumb: if a subclass can't honor everything the parent promises, it shouldn't extend it.
Five OO design principles for maintainable code:
- S — Single Responsibility: a class has one reason to change.
- O — Open/Closed: open for extension, closed for modification.
- L — Liskov Substitution: subtypes must be usable wherever their base is.
- I — Interface Segregation: prefer small, specific interfaces over fat ones.
- D — Dependency Inversion: depend on abstractions, not concretions.
// Dependency Inversion: high-level code depends on an interface
interface Repository { void save(Order o); }
class OrderService {
private final Repository repo; // not a concrete DB class
OrderService(Repository repo) { this.repo = repo; }
}
Liskov is the one interviewers probe most: a subclass that throws on, or
weakens, an inherited method (the Square extends Rectangle problem) violates
it.
More Object-Oriented Programming interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.