← 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. Let's break down step by step what it consists of and why each part is needed.

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. From the outside you see a small set of operations, and how everything is actually arranged is the object's own business.

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.

public class Account {
    private long id;          // field: object's data
    private long balance;     // balance in kopecks

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

    public long getBalance() {
        return balance;
    }
}

Account acc = new Account();  // created an object
acc.deposit(500);
System.out.println(acc.getBalance()); // 500

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.

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

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

Account acc = new Account(1, 1000); // id and balance set right away

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.

public class Account {
    private long balance;

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

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 with no body. A class promises to fulfill it via implements.

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

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

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.

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

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

public class Circle extends Shape {
    private final double radius;

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

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

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.

List<Animal> animals = List.of(new Dog(), new Animal());

for (Animal a : animals) {
    System.out.println(a.sound()); // "Woof", then "..."
}

This is the power of OOP: you add a new class Cat extends Animal — and the loop above 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.

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

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
Point p2 = new Point(3, 4);
System.out.println(p.equals(p2)); // true — comparison by values

Record fields are immutable (final): the values don't change after creation. This makes a record ideal 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.

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

Status s = Status.PAID;
if (s == Status.PAID) {
    System.out.println("Order paid");
}

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.
  • Collections — List, Set, Map and how to store groups of objects.
  • Generics — type-safe classes and methods that collections are built on.