← Back to the section

Java is a language in which almost all code lives inside classes. That is why object-oriented programming (OOP) here is not "one of the styles" but the foundation you work with every day.

for (Animal a : animals) System.out.println(a.sound()); three objects one variable what is printed Dogsound() → "Woof" Catsound() → "Meow" Animalsound() → "..." Animal a Dogsound() → "Woof"= new Dog()Woof Catsound() → "Meow"= new Cat()Meow Animalsound() → "..."= new Animal()... the call never changes — the object behind the variable does

The variable is declared as Animal, but the method comes from the object you put into it: Dog prints "Woof", Cat prints "Meow", a plain Animal prints an ellipsis. That is polymorphism, and everything else — classes, inheritance, interfaces — is built for it.

Why OOP at all

When a program grows, the main problem is not "how to write the logic" but "how not to get lost." OOP offers a simple idea: group the data and the actions on it into a single object and hide the details inside.

A short formula: a class is a blueprint, an object is a concrete thing built from that blueprint.

Classes and objects

A class describes what data a thing has (fields) and what it can do (methods). An object is a concrete instance created from a class via new.

live example

public class AccountDemo {
    static class Account {
        private long balance;                // field: object's data

        void deposit(long amount) {          // method: an action
            balance += amount;
        }

        long getBalance() {
            return balance;
        }
    }

    public static void main(String[] args) {
        Account first = new Account();       // created an object
        Account second = new Account();
        first.deposit(500);
        System.out.println(first.getBalance());   // 500
        System.out.println(second.getBalance());  // 0 — its own balance
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →

Each object stores its own field values: two Account instances are two independent balances.

Constructors

A constructor is a special method that is called when an object is created. It exists so that the object appears in a correct state right away, rather than empty.

live example

public class ConstructorDemo {
    static class Account {
        private final long id;
        private long balance;

        Account(long id, long balance) {     // constructor
            this.id = id;
            this.balance = balance;
        }

        String info() {
            return "Account#" + id + ", balance " + balance;
        }
    }

    public static void main(String[] args) {
        Account acc = new Account(1, 1000);  // id and balance set right away
        System.out.println(acc.info());      // Account#1, balance 1000
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →

If you don't write a constructor, Java adds an empty no-argument one. As soon as you declare your own, the empty one is no longer created automatically. The keyword this here means "the current object" and helps distinguish a field from a parameter with the same name.

Encapsulation and access modifiers

Encapsulation is hiding the internals of an object. Fields are made private, and access is given through methods. This way the object controls its own data and prevents itself from being put into an invalid state.

live example

public class EncapsulationDemo {
    static class Account {
        private long balance = 1000;

        void withdraw(long amount) {
            if (amount > balance) {
                throw new IllegalArgumentException("Insufficient funds");
            }
            balance -= amount;               // withdrawing more than you have is not allowed
        }

        long getBalance() {
            return balance;
        }
    }

    public static void main(String[] args) {
        Account acc = new Account();
        acc.withdraw(300);
        System.out.println(acc.getBalance());     // 700
        try {
            acc.withdraw(5000);
        } catch (IllegalArgumentException e) {
            System.out.println("Refused: " + e.getMessage());
        }
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →

Access modifiers control visibility:

  • private — only inside its own class.
  • (no modifier) — within the same package (package-private).
  • protected — the package plus subclasses.
  • public — from anywhere.

A practical rule: make everything as closed as possible and expose exactly what is truly needed from the outside.

Inheritance

Inheritance (extends) lets one class take the fields and methods of another and add its own. The base class is called the parent (superclass), the derived one the child (subclass).

public class Animal {
    public String sound() {
        return "...";
    }
}

public class Dog extends Animal {
    @Override
    public String sound() {  // override the parent's behavior
        return "Woof";
    }
}

The @Override annotation is not required but is desirable: it checks that you really are overriding a parent method rather than accidentally creating a new one (for example, because of a typo in the name).

Inheritance shouldn't be overused: it tightly couples classes. Often it is more flexible not to inherit but to hold another object inside as a field (this is called composition).

Interfaces

An interface describes what an object can do without saying how. It is a contract: a list of methods that by default have no body. A class promises to fulfill it via implements.

live example

public class NotifierDemo {
    interface Notifier {
        void send(String message);           // signature only, no implementation
    }

    static class EmailNotifier implements Notifier {
        @Override
        public void send(String message) {
            System.out.println("Email: " + message);
        }
    }

    public static void main(String[] args) {
        Notifier notifier = new EmailNotifier();
        notifier.send("order paid");         // Email: order paid
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →

A single class can implement several interfaces — unlike inheritance, where there is only one parent. This is the main way to give classes shared "capabilities" without building a rigid hierarchy.

default methods

An interface can give a method a ready-made implementation via default. Then everyone who implements the interface gets it for free and can override it if they wish.

public interface Notifier {
    void send(String message);

    default void sendAll(List<String> messages) { // ready-made implementation
        messages.forEach(this::send);
    }
}

default was invented so that methods could be added to existing interfaces without breaking the code of everyone who already implements them.

Abstract classes

An abstract class (abstract) is something between an ordinary class and an interface. It cannot be created via new; it serves as a common template: some methods are implemented, some are left to subclasses.

live example

public class ShapeDemo {
    abstract static class Shape {
        abstract double area();              // no body — a subclass implements it

        String describe() {                  // shared code for all shapes
            return "Area: " + area();
        }
    }

    static class Circle extends Shape {
        private final double radius;

        Circle(double radius) {
            this.radius = radius;
        }

        @Override
        double area() {
            return Math.PI * radius * radius;
        }
    }

    public static void main(String[] args) {
        System.out.println(new Circle(2).describe());   // Area: 12.56...
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →

When to pick which: an interface — when you only need a contract and shared capabilities (you can implement many interfaces); an abstract class — when subclasses share common state (fields) and common ready-made code.

Polymorphism

Polymorphism means "many forms": a variable of a parent or interface type can refer to an object of any subclass, and its own version of the method will be called. The calling code does not know and should not know which object is in front of it.

live example

import java.util.List;

public class PolymorphismDemo {
    static class Animal {
        String sound() {
            return "...";
        }
    }

    static class Dog extends Animal {
        @Override
        String sound() {
            return "Woof";
        }
    }

    static class Cat extends Animal {
        @Override
        String sound() {
            return "Meow";
        }
    }

    public static void main(String[] args) {
        List<Animal> animals = List.of(new Dog(), new Cat(), new Animal());
        for (Animal a : animals) {
            System.out.println(a.sound());   // Woof, Meow, ...
        }
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →

This is the power of OOP: add Fox extends Animal and put it into the list — the loop works with it without a single edit. Which method to call, Java decides at runtime based on the actual type of the object.

record — a concise immutable data class

Often a class is needed just to hold a set of values. Previously you had to write the fields, constructor, getters, equals, hashCode, and toString by hand. A record does all of this for you in a single line.

live example

public class RecordDemo {
    record Point(int x, int y) {}

    public static void main(String[] args) {
        Point p = new Point(3, 4);
        System.out.println(p.x());                      // 3 — the getter is named after the field
        System.out.println(p);                          // Point[x=3, y=4] — ready-made toString
        System.out.println(p.equals(new Point(3, 4)));  // true — comparison by values
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →

Record fields are immutable (final): the values don't change after creation. This makes a record handy for passing data around — DTOs, keys in a Map, method results. You can also add checks in a record's constructor:

public record Point(int x, int y) {
    public Point {                       // compact constructor
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("Coordinates must be non-negative");
        }
    }
}

enum — an enumeration

An enum defines a fixed set of named values. This is more reliable than storing strings or numbers: the compiler won't let you use a value that isn't in the list.

live example

public class EnumDemo {
    enum Status { NEW, PAID, SHIPPED, CANCELLED }

    public static void main(String[] args) {
        Status s = Status.PAID;
        if (s == Status.PAID) {
            System.out.println("Order paid");   // Order paid
        }
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →

An enum is a full-fledged class: you can add fields, a constructor, and methods to it to attach extra data to each value.

public enum Planet {
    EARTH(9.8), MARS(3.7);    // each value has its own gravity

    private final double gravity;

    Planet(double gravity) {  // enum constructor
        this.gravity = gravity;
    }

    public double gravity() {
        return gravity;
    }
}

In short

  • A class is a blueprint (fields + methods), an object is an instance built from the blueprint, created via new; a constructor sets the starting state.
  • Encapsulation: fields are private, access is through methods; keep everything as closed as possible.
  • Inheritance (extends) — take and extend another class; there is only one parent. Don't overuse it, composition is often better.
  • An interface (implements) — a "what it can do" contract; you can implement many, and there are default methods with a ready-made body. An abstract class — a template with shared code and state, cannot be created directly.
  • Polymorphism — one call works with any subclass; new types are added without touching the old code.
  • A record — a concise immutable data class (ready-made getters, equals, hashCode, toString); an enum — a fixed set of values, with fields and methods if needed.
  • Syntax and data types — variables, primitives, var and the basic language constructs.
  • CollectionsList, Set, Map and how to store groups of objects.
  • Generics — type-safe classes and methods that collections are built on.
  • Records and Optional — record in more detail, plus sealed classes and pattern matching.