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.