- Encapsulation: bundling state and behavior together and restricting direct access to internals via access modifiers (
privatefields,publicgetters/setters or methods that validate input).
class Account {
private double balance;
public void deposit(double amt) {
if (amt <= 0) throw new IllegalArgumentException();
balance += amt;
}
}
- Abstraction: exposing what an object does while hiding how, via
abstractclasses and interfaces that define a contract without full implementation. - Inheritance: a subclass (
extends) reuses and specializes a superclass's fields/methods, modeling an "is-a" relationship. Java supports single inheritance of classes but multiple inheritance of interfaces. - Polymorphism: the same method call behaves differently depending on the actual runtime type (overriding, resolved dynamically via virtual dispatch) or on argument types at compile time (overloading).
Shape s = new Circle(5);
s.area(); // calls Circle.area() at runtime, even though s is typed as Shape
Java's design (single class inheritance + interfaces, mandatory access modifiers, and virtual-by-default instance methods) makes all four pillars first-class, idiomatic parts of the language rather than conventions layered on top.
| Abstract class | Interface | |
|---|---|---|
| Fields | any (instance state) | only public static final constants |
| Constructors | yes | no |
| Method bodies | abstract + concrete methods | abstract, default, static, private methods |
| Inheritance | single (extends) | multiple (implements several) |
| Access modifiers on methods | any | implicitly public (unless private) |
| When to use | shared state + partial implementation, "is-a" hierarchy | a capability/contract multiple unrelated classes can fulfill |
abstract class Vehicle {
protected int speed;
abstract void accelerate();
void brake() { speed = 0; } // shared implementation
}
interface Drivable {
void steer(double angle);
default void honk() { System.out.println("Beep!"); } // Java 8+
}
class Car extends Vehicle implements Drivable {
void accelerate() { speed += 10; }
public void steer(double angle) { /* ... */ }
}
Rule of thumb: use an abstract class when subclasses share meaningful state/implementation; use an interface to describe a capability that unrelated classes can opt into (and to get multiple "inheritance" of behavior via default methods).
Overloading — same method name, different parameter list, within the same class (or class hierarchy) — chosen by the compiler based on the static types of the arguments:
void print(int x) {}
void print(String s) {}
void print(int x, int y) {}
Overriding — a subclass redefines a method it inherited, with the same signature — chosen at runtime based on the object's actual (dynamic) type, via virtual dispatch:
class Animal { void sound() { System.out.println("..."); } }
class Dog extends Animal { @Override void sound() { System.out.println("Woof"); } }
Animal a = new Dog();
a.sound(); // "Woof" — decided at runtime by a's actual type
Key rules for a valid override: same name and parameter types, covariant (same or narrower) return type, no narrower access modifier, and it can't throw new/broader checked exceptions than the overridden method. The @Override annotation isn't required but catches signature mistakes at compile time.
Overloading is static polymorphism (resolved at compile time); overriding is dynamic polymorphism (resolved at runtime) — this distinction is a very common interview follow-up.
Overriding requires dynamic dispatch: the JVM looks up which implementation to run based on the object's actual runtime type. That mechanism only applies to instance methods.
Static methods belong to the class itself, not to any instance, and are bound at compile time based on the declared (static) type of the reference — this is called method hiding, not overriding:
class A { static void greet() { System.out.println("A"); } }
class B extends A { static void greet() { System.out.println("B"); } }
A ref = new B();
ref.greet(); // prints "A" — resolved by ref's declared type (A), not the object's actual type
Private methods aren't inherited in any visible sense — a subclass can't even see, let alone override, a superclass's private method. If a subclass declares a method with the same name/signature, it's simply an unrelated new method local to the subclass, not an override; calling it from within the subclass just calls that new method, and any call from within the superclass still calls the superclass's own private method.
Both cases boil down to the same idea: overriding needs a virtual method table entry resolved per-object at runtime, and neither static (class-bound) nor private (not inherited/visible) methods participate in that mechanism.
Constructor chaining lets one constructor delegate to another instead of duplicating setup logic:
class Employee {
String name;
double salary;
Employee(String name) {
this(name, 0.0); // chains to the two-arg constructor
}
Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
}
class Manager extends Employee {
int teamSize;
Manager(String name, double salary, int teamSize) {
super(name, salary); // calls Employee's constructor first
this.teamSize = teamSize;
}
}
Rules:
this(...)calls another constructor in the same class;super(...)calls a constructor in the immediate superclass.- Either call, if present, must be the first statement in the constructor body — so you can never call both
this(...)andsuper(...)in the same constructor. - If a constructor has neither, the compiler implicitly inserts
super()(a no-arg call to the superclass constructor) as the first line — which fails to compile if the superclass has no accessible no-arg constructor.
This guarantees superclass state is always fully initialized before subclass initialization runs, which is why field initializers and instance blocks in the superclass execute before the subclass constructor body.