java

Modern Java: How Recent Features Transform Verbose Code Into Clean, Expressive Programming

Discover modern Java features that cut boilerplate code by 80%. Learn records, sealed classes, pattern matching & more to write cleaner, safer code. Transform your Java today.

Modern Java: How Recent Features Transform Verbose Code Into Clean, Expressive Programming

Java has been around for a long time, and I’ve written a lot of code in it. Over the years, the language has picked up a reputation for being verbose. You might have felt that too—writing pages of code just to hold a simple piece of data, or wrestling with null checks. But the language hasn’t been standing still. In its recent versions, Java has added tools that let you say more with less, write safer code, and express your intentions clearly. I want to share some of these tools with you. Think of them as ways to clean up your code, making it simpler to write and easier for the next person to understand.

Let’s start with a common task: creating a simple class to hold data. Imagine you need a class to represent a point in space with x and y coordinates. In the past, you’d write a class with private final fields, a constructor, getter methods, and equals, hashCode, and toString. It’s a lot of typing for a simple idea. Now, you can use a record.

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

That’s it. With that single line, the Java compiler gives you everything. It creates a constructor that takes an x and a y. It makes the fields final and private. It gives you accessor methods called x() and y(). It also writes the equals, hashCode, and toString methods for you. Using it is straightforward.

Point origin = new Point(0, 0);
Point myPoint = new Point(5, 10);
System.out.println(myPoint); // Prints: Point[x=5, y=10]
boolean same = origin.equals(new Point(0, 0)); // true

I use records for data transfer objects, return values from methods, or any place where I need a straightforward container for immutable data. It cuts out pages of repetitive code and makes my intent obvious: this is plain data.

Sometimes, you design a class or an interface with a specific purpose, and you don’t want just any other class to extend it. In the past, you could use final to stop extension entirely, or you could make the constructor package-private. But what if you want to allow only a specific set of classes to extend it? This is where sealed classes come in.

A sealed class or interface lets you explicitly list which classes are allowed to extend or implement it. This is powerful for modeling domains. Let’s say you are writing a graphics program and have a Shape interface.

public sealed interface Shape permits Circle, Rectangle, Triangle {
    double area();
}

This declaration says that only Circle, Rectangle, and Triangle can implement the Shape interface. You must then define those classes, marking them as final, sealed, or non-sealed.

public final class Circle implements Shape {
    private final double radius;
    public Circle(double r) { this.radius = r; }
    public double radius() { return radius; }
    @Override public double area() { return Math.PI * radius * radius; }
}

public final class Rectangle implements Shape { /* ... */ }
public final class Triangle implements Shape { /* ... */ }

Now, the compiler knows all the possible shapes. This becomes incredibly useful when combined with switch expressions, which we’ll get to. It turns a design decision into a compile-time rule, preventing your codebase from sprouting unknown Shape implementations later that could break your logic.

Checking an object’s type and casting it is a routine operation. The old way felt clunky. You’d write an instanceof check and then follow it with an explicit cast on a new line.

// The repetitive way
if (obj instanceof String) {
    String str = (String) obj;
    System.out.println(str.toLowerCase());
}

Pattern matching for instanceof cleans this up. It introduces a pattern variable right in the check.

// The new, cleaner way
if (obj instanceof String str) {
    // 'str' is already of type String and ready to use
    System.out.println(str.toLowerCase());
}

The variable str is only in scope where the instanceof check is true. This might seem like a small saving, but when you do it dozens of times a day, it removes a lot of visual noise and eliminates a potential error source—forgetting the cast or casting to the wrong type. You can even use it in complex conditions.

if (obj instanceof String str && !str.isEmpty()) {
    System.out.println("Non-empty string: " + str);
}

The switch statement has evolved from a simple jump table into a powerful expression. With pattern matching, you can now switch on the type of an object and safely extract its components. This works beautifully with sealed classes.

Let’s go back to our Shape example. Before, you might have used a long chain of if-else and instanceof checks. Now, you can use a switch.

public String describe(Shape s) {
    return switch (s) {
        case Circle c -> "A circle with radius " + c.radius();
        case Rectangle r -> "A rectangle " + r.width() + " by " + r.height();
        case Triangle t -> "A triangle with base " + t.base();
        // No default needed! The compiler knows this covers all Shapes.
    };
}

Because Shape is sealed and we’ve handled all three permitted types, the compiler knows the switch is exhaustive. It won’t ask for a default clause. If you later add a new shape, like Hexagon, to the permits list, the compiler will immediately flag this switch as non-exhaustive, prompting you to handle the new case. This turns a common runtime bug—forgetting to handle a type—into a compile-time error.

You can also handle null directly within the switch, which is a nice touch.

String format(Object o) {
    return switch (o) {
        case null -> "It's null";
        case Integer i -> "Integer: " + i;
        case String s -> "String: " + s;
        default -> "Unknown: " + o.toString();
    };
}

Null pointer exceptions are famously common. While Java doesn’t have a built-in null-safety type system like some other languages, you can adopt practices that greatly reduce the risk. The key is being deliberate about where null can and cannot appear.

First, use Optional for return types when a value might be absent. This forces the caller to think about the empty case.

public Optional<Customer> findCustomer(String id) {
    // ... lookup logic
    if (customerExists) {
        return Optional.of(customer);
    } else {
        return Optional.empty(); // Never return null here.
    }
}
// The caller must handle the absence.
findCustomer("123").ifPresentOrElse(
    c -> System.out.println(c.name()),
    () -> System.out.println("Not found")
);

Second, validate parameters at the start of public methods. Use Objects.requireNonNull.

public void processOrder(Order order, String notes) {
    Objects.requireNonNull(order, "Order must not be null");
    Objects.requireNonNull(notes, "Notes must not be null");
    // Now you can safely use order and notes.
}

Third, consider using annotation-based tools. Libraries like JetBrains’ @NotNull and @Nullable or the Checker Framework give your IDE and build tools the power to warn you about potential null problems during development. It’s a layer of safety that catches mistakes before you run the code.

Building multi-line strings for JSON, SQL, or HTML used to be a painful mess of escape sequences and concatenation.

String oldJson = "{\n" +
                 "  \"name\": \"Jane\",\n" +
                 "  \"active\": true\n" +
                 "}";

Text blocks change everything. You use triple quotes to open and close the block.

String newJson = """
    {
      "name": "Jane",
      "active": true
    }
    """;

The compiler handles the formatting. The incidental whitespace—the spaces used to align the text within your Java source code—is automatically stripped away. What’s left is the string you intended. This is a massive win for readability and maintainability when you’re working with embedded languages. You can copy a snippet of JSON directly from a file, paste it between the triple quotes, and it just works.

The try-with-resources statement, introduced earlier, automatically closes resources like streams and connections. It used to require you to declare the resources inside the try parentheses. Now, you can use resources that are already declared, as long as they are effectively final.

// A resource you might get from somewhere else
Connection conn = dataSource.getConnection();
// Suppose some conditional logic might happen here...
try (conn; Statement stmt = conn.createStatement()) {
    // Use the connection and statement
    ResultSet rs = stmt.executeQuery("SELECT * FROM users");
}
// Both 'conn' and 'stmt' are closed automatically here.

This is especially helpful when the resource initialization is complex or conditional. It keeps the convenience of automatic resource management without forcing awkward code structure.

Creating a small, ad-hoc list used to involve new ArrayList<>(Arrays.asList(...)). For unmodifiable collections, it was even more verbose. Now, simple factory methods exist.

List<String> colors = List.of("Red", "Green", "Blue");
Set<Integer> primes = Set.of(2, 3, 5, 7, 11);
Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87);

The collections returned by List.of, Set.of, and Map.of are immutable. You cannot add, remove, or change their elements. Attempting to do so throws an UnsupportedOperationException. They are also null-hostile; passing null to these methods causes a NullPointerException. I use these constantly for constants, default values, or returning a fixed set of results. They are concise and communicate intent: “This collection is fixed.”

Java is statically typed, which means you have to declare the type of every variable. Sometimes, the type declarations can get very long, especially with generics.

Map<String, List<Map<String, Integer>>> veryLongName = new HashMap<String, List<Map<String, Integer>>>();

The var keyword lets the compiler infer the type from the initializer on the right-hand side.

var myMap = new HashMap<String, List<Map<String, Integer>>>();

The variable myMap still has the full, explicit type HashMap<String, List<Map<String, Integer>>>; you just didn’t have to type it twice. Use var when the type is obvious. It reduces clutter, especially with long class names or complex builder patterns.

var path = Paths.get("data.txt"); // Inferred as Path
var list = new ArrayList<String>(); // Inferred as ArrayList<String>
var message = "Hello"; // Inferred as String

However, don’t use var when it hides important information. var data = parseInput(); is bad if the reader doesn’t know what parseInput() returns. Good naming becomes even more important when using var. I use it to remove repetitive noise, not to obscure meaning.

Finally, a small but delightful feature. You can now run a single .java file directly, without manually compiling it first. This is fantastic for writing small scripts, trying out an idea, or teaching.

Create a file called HelloScript.java.

#!/usr/bin/env java --source 21
public class HelloScript {
    public static void main(String[] args) {
        System.out.println("Running directly!");
        for (String arg : args) {
            System.out.println("Arg: " + arg);
        }
    }
}

Make it executable (chmod +x HelloScript.java on Linux/Mac) and run it: ./HelloScript.java arg1 arg2. The JVM handles the compilation in memory. The first line is a “shebang,” telling your shell how to run the file. This isn’t for building large applications, but it makes Java feel more approachable for small, one-off tasks. I use it for quick data-processing scripts where I want access to Java’s robust libraries without the ceremony of a full project setup.

These features, used together, change the feel of Java programming. Code becomes more about stating your business logic and less about the ceremonial syntax that surrounds it. You spend less time writing boilerplate getters, tedious casts, and fragile null checks. You spend more time solving the actual problem. The language is helping you write cleaner, safer, and more expressive code. I encourage you to try them in your next project, one feature at a time, and see how they can simplify your work.

Keywords: Java programming language, modern Java features, Java records, Java sealed classes, pattern matching instanceof, switch expressions Java, Java null safety, text blocks Java, try with resources, Java collections factory methods, var keyword Java, Java single file execution, Java code optimization, clean Java code, Java boilerplate reduction, Java 17 features, Java 21 features, immutable data classes Java, type safety Java, Java best practices, functional programming Java, Java syntax improvements, object oriented programming Java, Java development tools, enterprise Java development, Java performance optimization, Java code readability, defensive programming Java, Java memory management, concurrent programming Java, Java design patterns, microservices Java, Spring Boot Java, Maven Java project, Gradle Java build, IntelliJ IDEA Java, Eclipse Java IDE, Java unit testing, JUnit testing framework, Java debugging techniques, Java profiling tools, Java application deployment, Docker Java containers, Kubernetes Java apps, cloud native Java, reactive programming Java, Java stream API, lambda expressions Java, method references Java, Optional class Java, Java generics, annotation processing Java, reflection API Java, Java serialization, JSON processing Java, REST API Java, database connectivity Java, JDBC programming, JPA Hibernate, SQL queries Java, NoSQL databases Java, Redis caching Java, message queues Java, Apache Kafka Java, web development Java, servlet programming, JSP development, Thymeleaf templates, security programming Java, authentication authorization Java, JWT tokens Java, encryption decryption Java, logging frameworks Java, SLF4J Logback, exception handling Java, error management strategies, code review practices Java, refactoring techniques Java, legacy code modernization, Java migration strategies, performance tuning Java, garbage collection optimization, JVM tuning parameters, monitoring Java applications



Similar Posts
Blog Image
Micronaut Unleashed: The High-Octane Solution for Scalable APIs

Mastering Scalable API Development with Micronaut: A Journey into the Future of High-Performance Software

Blog Image
Unlock the Magic of Microservices with Spring Boot

Harnessing the Elusive Magic of Spring Boot for Effortless Microservices Creation

Blog Image
Unlock the Secrets to Bulletproof Microservices

Guardians of Stability in a Fragile Microservices World

Blog Image
**Proven Java I/O Optimization Techniques That Cut Processing Time by 70%**

Optimize Java I/O performance with buffering, memory-mapping, zero-copy transfers, and async operations. Expert techniques to reduce bottlenecks and boost throughput in high-performance applications.

Blog Image
Java's AOT Compilation: Boosting Performance and Startup Times for Lightning-Fast Apps

Java's Ahead-of-Time (AOT) compilation boosts performance by compiling bytecode to native machine code before runtime. It offers faster startup times and immediate peak performance, making Java viable for microservices and serverless environments. While challenges like handling reflection exist, AOT compilation opens new possibilities for Java in resource-constrained settings and command-line tools.

Blog Image
**10 Essential Java Module System Techniques for Scalable Enterprise Applications**

Discover 10 practical Java module system techniques to transform tangled dependencies into clean, maintainable applications. Master module declarations, service decoupling, and runtime optimization for modern Java development.