Object Oriented Java

Classes

public class Account {

    private final String owner;      // state
    private long cents;

    public Account(String owner, long opening) {   // constructor
        this.owner = owner;
        this.cents = opening;
    }

    public void deposit(long amount) {             // behaviour
        if (amount <= 0) throw new IllegalArgumentException("must be positive");
        cents += amount;
    }

    public long balance() { return cents; }
}

var account = new Account("Ann", 10_000);
account.deposit(5_000);
Modifier Visible in
private the same class
(none) the same package
protected package + subclasses
public everywhere

Fields private, methods as narrow as possible.

public Account(String owner) {
    this(owner, 0);        // delegate to the other constructor
}

Flexible constructor bodies (Java 25)

Statements before this(...) / super(...) are now allowed, so you can validate before an object half-exists:

public Positive(int value) {
    if (value <= 0) throw new IllegalArgumentException("must be positive");
    this.value = value;
}

Records

public record Address(String street, String zip, String city) {}

You get the constructor, accessors, equals, hashCode and toString; all fields are final.

public record Address(String street, String zip, String city) {

    public Address {                                 // compact constructor
        Objects.requireNonNull(street);
        if (!zip.matches("\\d{5}")) throw new IllegalArgumentException("bad zip: " + zip);
        city = city.strip();                         // parameters may be adjusted
    }

    public String oneLine() { return "%s, %s %s".formatted(street, zip, city); }

    public static Address fromCsv(String line) {     // static factory
        var p = line.split(";");
        return new Address(p[0], p[1], p[2]);
    }
}

Records are immutable — “changing” means creating a new one:

public record Person(String name, int age) {
    public Person withAge(int newAge) { return new Person(name, newAge); }
}
Record or class?

Record when the object is its data (DTOs, value objects). Class when it has mutable state or identity — two accounts with the same balance are not the same account.

Interfaces

public interface PaymentMethod {
    boolean pay(long cents);

    default String describe() { return getClass().getSimpleName(); }   // default impl

    static PaymentMethod standard() { return new Invoice(); }          // static method
}

public class CreditCard implements PaymentMethod {
    @Override
    public boolean pay(long cents) {  }
}

A class can implement any number of interfaces. Declare variables by the interface:

List<String> names = new ArrayList<>();
void process(List<String> input) {  }

Inheritance

public abstract class Vehicle {
    protected final String plate;

    protected Vehicle(String plate) { this.plate = plate; }

    public abstract int topSpeed();               // subclasses must implement

    public String describe() { return "vehicle " + plate; }
}

public class Car extends Vehicle {
    private final int hp;

    public Car(String plate, int hp) {
        super(plate);
        this.hp = hp;
    }

    @Override public int topSpeed() { return 50 + hp; }
    @Override public String describe() { return super.describe() + " with " + hp + " hp"; }
}
  • abstract — cannot be instantiated
  • final — class cannot be extended, method cannot be overridden
  • @Override — optional, but lets the compiler check you
Composition over inheritance

“A car is a vehicle” → inheritance. “A car has an engine” → a field. When unsure, prefer interfaces plus composition.

Sealed types

sealed fixes the set of subtypes, so the compiler knows every case:

public sealed interface Event permits SignUp, Order, SignOut {}

public record SignUp(String user) implements Event {}
public record Order(String user, long cents) implements Event {}
public record SignOut(String user) implements Event {}

String log(Event e) {
    return switch (e) {
        case SignUp(String u)          -> u + " signed up";
        case Order(String u, long c)   -> "%s ordered %d cents".formatted(u, c);
        case SignOut(String u)         -> u + " left";
    };   // no default needed
}

sealed interface + record + pattern matching is the modern way to say “a value is one of these things”.

equals and hashCode

Records do this for you. Plain classes do not:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Account a)) return false;
    return owner.equals(a.owner);
}

@Override
public int hashCode() { return Objects.hash(owner); }
Warning

Override equals → override hashCode, or HashMap and HashSet will misbehave.

★ Exercises

  1. Write ShoppingCart with add, remove and total. The internal list must not be mutable from outside.
  2. record Temperature(double celsius) with fahrenheit(), kelvin() and validation against absolute zero.
  3. sealed interface PaymentMethod with Cash, Card, Voucher, plus a fee(long) method using pattern matching.
  4. An interface with a default method and two implementations (email, SMS).
  5. Why is new Person("Ann", 30).equals(new Person("Ann", 30)) true for a record but false for a plain class?
  6. Model a small library: Book, Loan, Member. Decide deliberately what is a record.