← Back to the section

Java was long criticized for its verbosity: to describe a simple data "box" with three fields, you had to write a constructor, getters, equals, hashCode, and toString by hand. Over the recent versions (17–21) the language has caught up considerably. Let's go through the features that make modern code shorter and safer, starting with the most useful one — record.

Why any of this: the verbosity problem

Imagine a class that just holds data — a point on a map, an order line, coordinates. It used to look like this:

public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    @Override
    public boolean equals(Object o) { /* ... 10 lines ... */ }
    @Override
    public int hashCode() { /* ... */ }
    @Override
    public String toString() { /* ... */ }
}

Thirty lines for the sake of two numbers. There's zero logic here — pure boilerplate that's easy to get wrong (forget a field in equals). This is exactly the pain that record removes.

record in more detail

record is a special kind of class for holding immutable data. You declare only the fields, and the compiler generates the constructor, accessor methods, equals, hashCode, and toString for you.

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

One line does everything the 30 lines above did. You use it like this:

Point p = new Point(3, 4);
System.out.println(p.x());        // 3 — the accessor is named after the field, no get
System.out.println(p);            // Point[x=3, y=4] — ready-made toString
Point q = new Point(3, 4);
System.out.println(p.equals(q));  // true — comparison by field values

Key properties of a record:

  • Immutability. Fields are final; you can't change them after creation. To "modify" a point, you create a new one.
  • Accessors are named after the fields: p.x(), not p.getX().
  • equals/hashCode compare by the value of all fields. This makes a record an ideal key for a Map or an element of a Set.

You can add your own methods and checks in the constructor. A compact constructor lets you validate arguments without listing the assignments:

public record Point(int x, int y) {
    public Point {                       // compact constructor — no parameter parentheses
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("Coordinates cannot be negative");
        }
        // the assignment this.x = x; is added by the compiler
    }

    public double distanceToOrigin() {   // your own method — go ahead
        return Math.sqrt(x * x + y * y);
    }
}

Short formula: a record is a "data class" without the boilerplate. Use it for DTOs, keys, coordinates, any immutable "boxes of values." When you need mutable state or inheritance — reach for a regular class.

Optional: how to stop fearing NPE

NullPointerException (NPE) is the most common runtime error in Java. It happens when you call a method on a variable that is actually null. A method that returns "maybe a value, maybe nothing" used to return null — and it was easy to forget the check.

Optional<T> is a "box" that either contains a value or is empty. It tells the caller explicitly: "there may be no result here, handle both cases."

Optional<User> found = repository.findById(42);   // explicit: may not be found

// safely retrieve the value or substitute a fallback
User user = found.orElse(User.guest());

// or react only if the value is present
found.ifPresent(u -> System.out.println("Hello, " + u.name()));

The power of Optional is in chains of transformations without a scatter of if (x != null):

String city = repository.findById(42)        // Optional<User>
        .map(User::address)                  // Optional<Address>
        .map(Address::city)                  // Optional<String>
        .orElse("unknown");                  // String, without a single NPE

If a value is missing at any step, the chain quietly "falls through" into an empty Optional and returns "unknown".

Optional antipatterns

Optional is easy to misuse. What not to do:

  • Don't call .get() without a check. opt.get() on an empty Optional throws an exception — that's just an NPE in disguise. Use orElse, orElseThrow, ifPresent, map.
  • Don't make class fields Optional. Optional was designed for method return values, not for storage. Let a field be plain, possibly null inside.
  • Don't pass Optional as a method parameter. It muddles the call. Better to add a method overload or accept a plain value.
  • Don't wrap collections. An empty List already means "there's nothing" — Optional<List<...>> is redundant. Return an empty list.

Short formula: Optional is a return type meaning "there may be no result," not a replacement for null everywhere.

switch expressions

The old switch was a statement: it performed actions, required break (forget it and you fall through to the next branch), and didn't return a value. The modern switch can be an expression — that is, return a result that you assign to a variable right away.

// arrow syntax: no break, no fall-through
String day = switch (dayOfWeek) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "weekday";
    case SATURDAY, SUNDAY -> "weekend";
};

Advantages: no break, several labels separated by commas, the whole expression returns a value. For an enum, the compiler also checks that all cases are handled. If a branch needs several lines, use a block with yield:

int code = switch (status) {
    case ACTIVE -> 1;
    case BLOCKED -> {
        log.warn("Blocked status");
        yield 0;                 // yield returns a value from the block
    }
};

sealed classes and pattern matching in brief

A sealed class (or interface) restricts the list of subtypes: only the listed types may implement it. This tells both the compiler and the reader: "there are exactly this many variants, no others."

public sealed interface Shape permits Circle, Square {}

public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}

Combined with pattern matching in switch, this gives a compact and safe dispatch by type. Previously you wrote if (s instanceof Circle) { Circle c = (Circle) s; ... } — with an explicit cast. Now the variable is declared right in the check:

double area = switch (shape) {
    case Circle c -> Math.PI * c.radius() * c.radius();   // c is already a Circle, no cast
    case Square s -> s.side() * s.side();
};

Because Shape is sealed, the compiler knows all the variants and won't require a default branch. Add a third shape and it will point out where the switch became incomplete. Pattern matching also works in a plain instanceof:

if (obj instanceof String str && !str.isBlank()) {
    System.out.println(str.length());   // str is available and already a String
}

text blocks

Multiline strings (JSON, SQL, HTML) used to be glued together from pieces with \n and escaped quotes — impossible to read. A text block is a string in triple quotes that keeps line breaks and quotes as they are:

String json = """
        {
            "name": "Ivan",
            "city": "Moscow"
        }
        """;

Inside, you don't need to escape quotes, and the compiler trims the left indentation to the leftmost non-empty line. Handy for embedded SQL queries and test data.

What else arrived in Java 17–21

A few small things you'll run into right away:

  • var — local variable type inference: var users = new ArrayList<User>();. The type is still static, you just don't write it twice. Only for local variables, where the type is obvious from the right-hand side.
  • List.of, Map.of — quick immutable collections: List.of("a", "b").
  • Helpful NullPointerExceptions — the NPE message now names exactly which variable turned out to be null.
  • Virtual threads (Java 21) — lightweight threads for scalable I/O; the details are a topic for the concurrency section, here just know they exist.

In short

  • record generates the constructor, accessors, equals/hashCode/toString — use it for immutable data classes (DTOs, keys, value objects).
  • Optional<T> is an honest return type "the value may be absent"; retrieve it via orElse/map/ifPresent, not via .get().
  • Optional antipatterns: .get() without a check, Optional in fields and parameters, wrapping collections.
  • switch expression returns a value, needs no break, checks exhaustiveness for an enum.
  • sealed + pattern matching give safe dispatch over a fixed set of types without manual casting.
  • Text block (""") — readable multiline strings without escaping.
  • var, List.of, clear NPEs, and virtual threads — the pleasant small stuff of modern Java.
  • Lambdas and the Stream API — where Optional.map and switch show up most often.
  • OOP in Java — how record and sealed differ from regular classes and inheritance.
  • Tooling and build — how to build and run a project on the current version of the language.