Why this topic matters

Java is, by design, an object-oriented language. A Java program is, fundamentally, a set of classes that send messages to each other. Even the simplest “Hello World” program lives inside a class. The language designers made this choice in 1995 because OOP, done well, scales: a codebase of a million lines organised into small classes with clear contracts is maintainable; the same lines crammed into a single procedural file are not.

Mastering OOP is the difference between writing code that grows safely and writing code that fights you at every change. A well-designed class hierarchy lets you add a new feature by adding a new class, with no edits to existing classes. A poorly designed hierarchy forces every new feature to touch a dozen files, each change rippling unpredictably through the system. The skills in this topic are the skills that separate a junior developer who can write a function from a senior developer who can design a system.

OOP is also the language feature most heavily tested in Java interviews. Questions about equals and hashCode, the difference between abstract classes and interfaces, the contract of Comparable, the strategy and template-method patterns — these are the bread and butter of technical screens. The OOP topic is the one to revisit before any job interview.

What you will learn

You will learn how to design your own types using class, how to instantiate them with new, how to hide implementation details behind private fields and public methods, how to factor common code into a parent class with extends, how to override methods for runtime polymorphism, and how to define capabilities with interface that you can implement across unrelated classes. You will also meet the modern additions to Java's OOP toolkit: record for immutable data carriers (Java 16), sealed classes and interfaces (Java 17), and pattern matching for switch (Java 21).

By the end of this topic you should be able to design a small class hierarchy from scratch, justify your choice of interface versus abstract class, override equals, hashCode, and toString correctly, and read production Java code that uses inheritance and interfaces heavily. You will also have the vocabulary to discuss OOP design trade-offs — the kind of discussion that drives architecture reviews at most companies.

Why OOP matters

Most production Java code is OOP. Mastering these concepts is the difference between code that grows safely and code that fights you at every change.

Core concepts

The vocabulary you will pick up in this topic. Each term is covered in detail in its own tutorial.

  • Class — a blueprint defining fields, methods, and constructors for objects.
  • Object — an instance of a class, created with new.
  • Field — a variable that holds state, declared inside a class.
  • Method — a named block of code on a class that operates on its fields.
  • Constructor — a special method that initialises a new object's fields.
  • Encapsulation — hiding state behind private fields and public methods.
  • Inheritance — one class extending another, acquiring its fields and methods.
  • Polymorphism — a parent-typed reference calling the actual subclass's overridden method.
  • Abstraction — modelling concepts with abstract classes or interfaces.
  • Interface — a contract that a class can implement; supports multiple inheritance of type.
  • Record — a concise immutable data carrier introduced in Java 16.
  • Sealed — a class or interface that explicitly lists its permitted subtypes (Java 17).

Common pitfalls

The five bugs and design smells we see most often in OOP code.

  • Breaking the equals/hashCode contract. If you override one without the other, your class will misbehave in HashMap and HashSet. Use IDE generators or switch to a record.
  • Calling overridable methods from a constructor. If a parent constructor calls a method the subclass overrides, the subclass version runs before the subclass constructor body. Fields are not yet initialised.
  • Inheritance for code reuse rather than “is-a”. Stack extends Vector is the classic mistake — a Stack is not a Vector, it has one. Prefer composition.
  • Mutating objects returned from getters. Return defensive copies of mutable fields, or make the field type immutable.
  • Deep inheritance hierarchies. Three or more levels of inheritance are usually a smell. Composition plus interfaces usually expresses the same design more flexibly.

Best practices

  • Prefer immutability. Mark fields final; provide no setters; return new objects from operations.
  • Start every field private. Widen visibility only when there is a real reason.
  • Use a record instead of writing a boilerplate data class.
  • Program to an interface, not an implementation. List<String> list = new ArrayList<>();
  • Favor composition over inheritance. Inject collaborators via constructor parameters.
  • Annotate overridden methods with @Override — the compiler will catch typos and signature mismatches.
  • Limit a class to one responsibility. If you cannot describe the class's job in one sentence, split it.

Frequently asked questions

Should I use an abstract class or an interface?

Use an abstract class when subclasses share state (fields) or a common skeleton (template method pattern). Use an interface to define a capability that any class can provide, regardless of where it sits in the class hierarchy. Since Java 8 added default methods and Java 9 added private methods to interfaces, the line has blurred, but the rule of thumb stands: state → abstract class; capability → interface.

Why should I prefer composition over inheritance?

Inheritance is rigid: a subclass is locked into its parent's implementation, and changes to the parent can break subclasses in subtle ways. Composition is flexible: a class can hold references to several collaborators and swap them at runtime. The classic example is java.util.Stack, which extends Vector — a design mistake from 1996 that cannot be undone because so much code relies on it. Modern Java style favours composition; the Effective Java book devotes a whole chapter to the principle.

When should I use a record instead of a class?

Use a record when your class is a transparent carrier of immutable data — a point, a money amount, a configuration entry, a DTO. The compiler generates the constructor, accessors, equals, hashCode, and toString for you, eliminating boilerplate. Use a regular class when you need mutability, multiple constructors with different semantics, or complex validation that does not fit a compact constructor.

What is the Liskov Substitution Principle?

The LSP says that anywhere a parent type is used, an instance of any subclass should work without surprising behaviour. If Square extends Rectangle, then a Square must behave correctly wherever a Rectangle is expected — but setting the width of a Square also changes its height, which violates the Rectangle contract. The LSP is the L in SOLID; respecting it is what makes inheritance safe.

Tutorials in this topic

The five tutorials below are listed in the recommended reading order. Each tutorial links to the next at the bottom of the page.

  • Classes & Objects — fields, constructors, methods, the this keyword.
  • Inheritance — extends, super, @Override, sealed classes.
  • Polymorphism — virtual method dispatch, strategy and template method patterns.
  • Encapsulation — private fields, validation, immutability, the builder pattern.
  • Interfaces — default methods, static methods, functional interfaces, sealed interfaces.