Object-Oriented Programming
Build robust Java models using classes, constructors, encapsulation, inheritance, polymorphism, interfaces, composition, records, and sealed types.
Classes, Objects, Fields, and Constructors
A class defines state and behavior; an object is a runtime instance. Constructors establish valid initial state. Fields should normally be private, with behavior exposed through methods rather than unrestricted mutation.
public final class User {
private final String id;
private String displayName;
public User(String id, String displayName) {
this.id = id;
this.displayName = displayName;
}
public String id() { return id; }
public String displayName() { return displayName; }
}
Favor constructors that prevent invalid states. If a field must never change after construction, make it final.
- Constructors should establish invariants.
- Encapsulation keeps object state under controlled mutation.
- Prefer immutable fields when the domain allows it.
Encapsulation, Inheritance, and Polymorphism
Encapsulation hides implementation details behind a stable API. Inheritance should model a genuine "is-a" relationship. Polymorphism allows code to depend on an abstraction while the runtime object determines the implementation.
interface PaymentProcessor {
void pay(long cents);
}
final class CardPayment implements PaymentProcessor {
public void pay(long cents) {
System.out.println("Card: " + cents);
}
}
final class Checkout {
private final PaymentProcessor processor;
Checkout(PaymentProcessor processor) {
this.processor = processor;
}
void complete(long cents) {
processor.pay(cents);
}
}
Checkout does not care whether payment uses a card, wallet, or test double. This is dependency inversion through an interface.
Inheritance creates tight coupling. Use composition when the relationship is really 'has-a' rather than 'is-a'.
- Polymorphism reduces coupling to concrete implementations.
- Interfaces are excellent boundaries for dependency injection and testing.
- Composition is often more flexible than deep inheritance hierarchies.
Abstract Classes and Interfaces
An abstract class can hold shared state and implementation while preventing direct instantiation. An interface primarily defines a capability or contract and can contain default, static, and private methods.
A class can implement multiple interfaces but extend only one class.
interface Auditable {
String actor();
}
abstract class BaseCommand {
private final long createdAt = System.currentTimeMillis();
long createdAt() { return createdAt; }
}
final class CreateUserCommand extends BaseCommand implements Auditable {
public String actor() { return "system"; }
}
- A class has single inheritance but can implement multiple interfaces.
- Abstract classes are useful when shared state or implementation is intrinsic.
- Interfaces define capabilities and reduce dependency on concrete classes.
Records, Sealed Classes, and Enums
Modern Java provides concise tools for domain modeling. A record is ideal for transparent, mostly immutable data carriers. A sealed type restricts which types may extend or implement an abstraction. An enum models a fixed set of named constants.
public record Point(int x, int y) {}
public sealed interface Result
permits Success, Failure {}
public record Success(String value) implements Result {}
public record Failure(String reason) implements Result {}
enum Status {
NEW, PROCESSING, COMPLETED, FAILED
}
Records generate accessors, a canonical constructor, equals, hashCode, and toString. They do not make referenced objects deeply immutable.
- Records reduce boilerplate for data-centric types.
- Sealed types make an inheritance boundary explicit.
- Records are shallowly immutable; referenced mutable objects can still change.