Classes, Objects & Inheritance
Classes, constructors, encapsulation, inheritance, interfaces and polymorphism in Java.
Classes and objects
A class is a blueprint; an object is an instance of that blueprint created with new.
public class Car {
// fields (state)
private String model;
private int speed;
// constructor
public Car(String model) {
this.model = model;
this.speed = 0;
}
// methods (behaviour)
public void accelerate(int amount) {
this.speed += amount;
}
public String describe() {
return model + " is going " + speed + " km/h";
}
}
Car car = new Car("Civic");
car.accelerate(40);
System.out.println(car.describe()); // Civic is going 40 km/h
Encapsulation
Fields are usually private, exposed only through public methods (getters/setters). This protects invariants — the class controls exactly how its state can change.
public class BankAccount {
private double balance;
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
balance += amount;
}
public double getBalance() {
return balance;
}
}
Inheritance
A subclass reuses and extends a parent class with extends:
public class Vehicle {
protected int speed = 0;
public void accelerate() {
speed += 10;
}
}
public class SportsCar extends Vehicle {
@Override
public void accelerate() {
speed += 30; // sports cars accelerate faster
}
}
Vehicle v = new SportsCar();
v.accelerate();
System.out.println(v.speed); // 30 — the overridden method ran
Every class in Java implicitly extends Object, which is why every object has .toString(), .equals() and .hashCode() by default.
Interfaces
An interface defines a contract — what a class must do, not how. A class can implement multiple interfaces (Java only allows single class inheritance, but unlimited interface implementation):
public interface Payable {
double calculatePay();
}
public class Employee implements Payable {
private double hoursWorked;
private double hourlyRate;
public Employee(double hoursWorked, double hourlyRate) {
this.hoursWorked = hoursWorked;
this.hourlyRate = hourlyRate;
}
@Override
public double calculatePay() {
return hoursWorked * hourlyRate;
}
}
Polymorphism
Different classes implementing the same interface (or extending the same parent) can be treated uniformly:
List<Payable> payables = List.of(
new Employee(40, 25.0),
new Employee(20, 30.0)
);
double total = 0;
for (Payable p : payables) {
total += p.calculatePay(); // each object runs its own implementation
}
Abstract classes
Use an abstract class when subclasses should share some real implementation and be forced to implement other parts themselves:
public abstract class Shape {
public abstract double area(); // must be implemented by subclasses
public void printArea() { // shared, concrete implementation
System.out.println("Area: " + area());
}
}
public class Circle extends Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public double area() {
return Math.PI * radius * radius;
}
}
Best practices
- Favor composition over inheritance where possible — deep inheritance hierarchies become fragile to change.
- Keep fields
privateand expose behaviour through methods, not raw setters for everything. - Program to an interface, not a concrete class (
Payable p = new Employee(...), notEmployee e = ...), so implementations can be swapped freely.
Common mistakes
- Forgetting
@Override— it's optional but catches typos in method signatures at compile time. - Confusing an abstract class (can have state + concrete methods) with an interface (traditionally contract-only, though modern Java interfaces can have
defaultmethods too). - Overusing inheritance for code reuse when composition would be simpler and more flexible.
Interview questions
Q: When would you use an abstract class instead of an interface? When subclasses need to share actual field state and some concrete method implementations, not just a method contract. A class can only extend one abstract class but can implement many interfaces.
Q: What is polymorphism, concretely? The ability to call the same method name on different object types and have each execute its own behaviour — enabled by inheritance/interfaces plus method overriding, resolved at runtime ("dynamic dispatch").