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(), notp.getX(). equals/hashCodecompare by the value of all fields. This makes a record an ideal key for aMapor an element of aSet.
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. UseorElse,orElseThrow,ifPresent,map. - Don't make class fields
Optional. Optional was designed for method return values, not for storage. Let a field be plain, possiblynullinside. - Don't pass
Optionalas a method parameter. It muddles the call. Better to add a method overload or accept a plain value. - Don't wrap collections. An empty
Listalready 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
recordgenerates 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 viaorElse/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 anenum. 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.
What to read next
- Lambdas and the Stream API — where
Optional.mapandswitchshow up most often. - OOP in Java — how
recordandsealeddiffer from regular classes and inheritance. - Tooling and build — how to build and run a project on the current version of the language.