Encapsulation, inheritance and polymorphism
Encapsulation: keep fields private and expose behaviour through methods, so each object protects its own rules.
Access levels, from widest to narrowest: public, protected (same package plus subclasses), package-private (no keyword), private.
Inheritance: class Car extends Vehicle reuses and extends behaviour. A class has one superclass but can implement many interfaces.
Polymorphism: a Vehicle variable can hold a Car or a Bike, and the overridden method that runs is chosen at runtime. Mark overrides with @Override so the compiler catches typos.
Inheritance with extends
A subclass extends a superclass, inheriting its fields and methods (an "is-a" relationship). A class can extend only one class. Call the parent's constructor with super(...); it must be the first statement.
class Employee {
protected final String name;
Employee(String name) { this.name = name; }
double pay() { return 50_000; }
}
class Manager extends Employee {
Manager(String name) { super(name); }
@Override double pay() { return super.pay() * 1.5; }
}Method overriding rules
An override has the same name and parameters. It may return a subtype (covariant return), can't reduce visibility, and can't throw broader checked exceptions. static, private and final methods can't be overridden. Always add @Override so the compiler catches mistakes.
Polymorphism
Runtime polymorphism: a parent-type variable can hold any subtype, and the JVM calls the object's own override (dynamic dispatch). Compile-time polymorphism is overloading. This is what lets you write code against an abstraction and plug in new types later.
List<Employee> staff = List.of(new Employee("Ravi"), new Manager("Asha"));
for (Employee e : staff) {
System.out.println(e.name + ": " + e.pay()); // each object's own pay()
}Abstraction and abstract classes
An abstract class can't be instantiated and may declare abstract methods that subclasses must implement, alongside shared fields and code. Use it when related classes share state and behaviour; use an interface when you only need a contract.
abstract class Shape {
abstract double area();
String describe() { return getClass().getSimpleName() + " of area " + area(); }
}
class Circle extends Shape {
private final double r;
Circle(double r) { this.r = r; }
double area() { return Math.PI * r * r; }
}Upcasting, downcasting and instanceof
Upcasting (child to parent) is automatic. Downcasting needs a check, or it throws ClassCastException. Since Java 16, pattern matching for instanceof checks and casts in one step.
Employee e = new Manager("Asha"); // upcast
if (e instanceof Manager m) { // check + cast (Java 16)
System.out.println(m.name + " is a manager");
}Composition over inheritance
Inheritance ties classes tightly: a change in the parent can break every child. Composition (a class holding other objects, "has-a") is usually more flexible: you can swap parts and combine behaviour freely. Inherit only when the child truly is a special kind of the parent.
class OrderService {
private final PaymentGateway gateway; // has-a: swap Razorpay for another gateway easily
private final Notifier notifier;
OrderService(PaymentGateway gateway, Notifier notifier) {
this.gateway = gateway;
this.notifier = notifier;
}
}Example
abstract class Payment {
abstract double fee(double amount);
}
class UpiPayment extends Payment {
@Override double fee(double amount) { return 0; }
}
class CardPayment extends Payment {
@Override double fee(double amount) { return amount * 0.02; }
}
List<Payment> payments = List.of(new UpiPayment(), new CardPayment());
for (Payment p : payments) {
System.out.println(p.fee(1000)); // 0.0, then 20.0: runtime dispatch
}Common mistake
Trying to override a static method. Static methods are hidden, not overridden, so the variable's declared type (not the object) decides which one runs.
Under the hood
Runtime polymorphism works through a per-class virtual method table. The JIT usually removes the cost: if only one implementation is loaded it inlines the call directly, and deoptimizes later if another subclass appears. Prefer composition over inheritance; deep hierarchies are brittle because subclasses depend on their parents' internals.
Check yourself
Which modifier allows access from subclasses in other packages?
How this connects
Know these first
Part of Java from zero.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.