Fundamentals 16 min read

Java Syntax Revolution (Part 2): No More POJOs—Records, Sealed Classes, Text Blocks

From Java 9 to 17 the language adds text blocks, switch expressions, helpful NullPointerExceptions, records and sealed classes, each illustrated with real‑world code, pitfalls and best‑practice tips that dramatically cut boilerplate, prevent bugs like missing breaks, and make data‑centric code far more concise.

Tinker Programmer
Tinker Programmer
Tinker Programmer
Java Syntax Revolution (Part 2): No More POJOs—Records, Sealed Classes, Text Blocks

Why I Started Looking for a POJO‑Free Solution

After upgrading a project from JDK 8 to 17 I spent a night fixing Lombok compatibility issues—old Lombok didn’t support JDK 17, newer Lombok conflicted with MapStruct, and the replacement MapStruct broke another code‑generation plugin. The pain made me wonder whether I could avoid Lombok entirely and let the language handle pure data classes.

Switch‑Case Break Bugs and the Switch Expression Fix

In a 2020 e‑commerce order‑status switch I missed the break statements for the first two cases. All paid orders fell through to the COMPLETED branch, each receiving a ¥50 coupon. Within half an hour more than a hundred coupons were issued and I paid ¥500 out of pocket.

int discount = 0;
switch (order.getStatus()) {
    case PAID:
        discount = 10;
    case SHIPPED:
        discount = 20;
    case COMPLETED:
        discount = 50;
        break;
    default:
        discount = 0;
}

The old switch’s default fall‑through makes a missing break a silent, hard‑to‑detect bug. Java 14’s switch expression eliminates fall‑through entirely:

int discount = switch (order.getStatus()) {
    case PAID -> 10;
    case SHIPPED -> 20;
    case COMPLETED -> 50;
    case CANCELLED -> 0;
};

Each branch is independent, the compiler flags any missing case, and the expression can return a value directly. For multi‑line logic you can use yield inside a block, but you must not mix the old : syntax with the new -> syntax.

Text Blocks End the Escape‑Hell of Multiline Strings

Before Java 15 a multiline JSON, SQL or HTML literal required concatenation and escaped newlines, making the source unreadable:

String json = "{
" +
    "  \"name\": \"张三\",
" +
    "  \"age\": 25,
" +
    "  \"address\": \"" + address + "\"
" +
    "}";

Java 15’s text blocks let you write the same content with three double quotes, preserving line breaks and requiring no escaping:

String json = """
    {
        "name": "张三",
        "age": 25,
        "address": "%s"
    }
    """.formatted(address);

Gotchas: the opening triple quotes must be followed by a line‑break, common leading indentation is stripped, trailing spaces are removed unless you add \s, and you can continue a line with a backslash ( \) to avoid a line break.

Helpful NullPointerException (JDK 14+)

Traditional NPE stacks only show the line that threw, leaving you guessing which variable was null. With the helpful NPE feature the stack trace includes the exact expression that evaluated to null:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Address.getCity()" because the return value of "User.getAddress()" is null
    at Test.main(Test.java:10)

This works out‑of‑the‑box on JDK 14 and later, dramatically speeding up debugging. Only bytecode‑enhancement tools that rewrite stack frames may interfere.

Records – The End of POJO Boilerplate

Java 16 makes records a permanent feature. A single declaration creates a final, immutable data carrier and the compiler automatically generates private final fields, a canonical constructor, accessor methods (named after the fields), equals, hashCode and toString:

public record User(Long id, String name, Integer age, String email) {}

Pitfalls:

Records are final classes; they cannot extend other classes and cannot be subclassed (they implicitly extend java.lang.Record).

All fields are final ; there are no setters, so records are unsuitable for mutable JPA entities.

You cannot add instance fields beyond those declared in the header.

Jackson 2.12+ supports record (de)serialization; older versions require an upgrade.

You can add a compact constructor for validation and custom instance methods:

public record User(Long id, String name, Integer age) {
    public User {
        if (id == null || id < 1) throw new IllegalArgumentException("id illegal");
        if (name == null || name.isBlank()) throw new IllegalArgumentException("name required");
        if (age == null || age < 0 || age > 150) throw new IllegalArgumentException("age illegal");
    }
    public boolean isAdult() { return age >= 18; }
}

When comparing with Lombok, the rule of thumb is:

Use records for pure data carriers, DTOs, VO, method return values, or event objects.

Stick with Lombok when you need builders, mutable fields, inheritance, or JPA entities.

Both can coexist; choose per use‑case.

Sealed Classes (JDK 17) – Controlling Inheritance

Before Java 17 inheritance was either final (no subclass) or unrestricted. Sealed classes introduce a middle ground:

public sealed interface Shape permits Circle, Rectangle, Triangle { double area(); }
public final class Circle implements Shape { double radius; public double area() { return Math.PI * radius * radius; } }
public final class Rectangle implements Shape { double width, height; public double area() { return width * height; } }
public final class Triangle implements Shape { double base, height; public double area() { return 0.5 * base * height; } }

Subclasses must be declared final, sealed, or non‑sealed. The permits clause must list subclasses that reside in the same package or module.

Combined with switch expressions, the compiler can perform exhaustive checks: adding a new subclass without updating the switch triggers a compile‑time error, preventing runtime bugs like the missing‑case scenario described earlier.

Summary of the Five Syntax Revolutions (Java 9‑17)

Text blocks eliminate multiline‑string escaping.

Switch expressions remove accidental fall‑through and allow direct return values.

Helpful NPE pinpoints the null source, cutting debugging time.

Records replace verbose POJOs and remove Lombok‑related compatibility concerns.

Sealed classes give fine‑grained inheritance control and enable compile‑time exhaustiveness checks.

In practice I see at least a 30 % reduction in boilerplate and a noticeable drop in low‑level bugs after adopting these features.

Three Immediate Actions You Can Take

Identify a pure‑data DTO in your codebase, convert it to a record, and remove Lombok annotations.

Search globally for switch statements, replace simple cases with the -> expression form, and watch out for mixed syntax.

Find any place where you concatenate multiline SQL/JSON strings, upgrade the project to JDK 17+, and rewrite the literals as text blocks, respecting the indentation and continuation rules.

What’s Next?

The next article will cover the full “pattern‑matching” family: type‑pattern instanceof without casts, switch‑pattern matching, record deconstruction, and how sealed classes let you drop the visitor pattern entirely.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

javaSealed ClassesRecordsText BlocksSwitch ExpressionsHelpful NPE
Tinker Programmer
Written by

Tinker Programmer

Solving problems with code, sharing practical tech insights, and leveling up together!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.