Key Enhancements from Java 14 to 21

Text Blocks - Java 15

Text blocks were introduced in Java 15, simplifying the formatting of multi-line strings like JSON, SQL, and HTML.


String jsonMessage = """
                    {
                       "firstName": "Mitra",
                       "lastName": "Manjush",
                       "email": "test@gmail.com"
                    }
                    """;
System.out.println(jsonMessage);
                

Output:

{
   "firstName": "Mitra",
   "lastName": "Manjush",
   "email": "test@gmail.com"
}

Switch Updates - Java 14

The new Arrow syntax was introduced in Java 14, making switch statements look cleaner.


int d = 2;
String day = switch(d) {
   case 1 -> "SUNDAY";
   case 2 -> "MONDAY";
   case 3 -> "TUESDAY";
   case 4 -> "WEDNESDAY";
   case 5 -> "THURSDAY";
   case 6 -> "FRIDAY";
   case 7 -> "SATURDAY";
   default -> throw new IllegalArgumentException();
};
System.out.println(day);
                

Output: MONDAY

Sealed classes - Java 17

Sealed classes and interfaces restrict which other classes or interfaces can extend or implement them. This feature helps model various possibilities in a domain by defining its entities and their relationships.

Declaring Sealed Classes: To declare a sealed class, use the sealed modifier in its declaration. You must also specify which classes are permitted to extend the sealed class using the permits clause.

Example:


public sealed class Animal permits Lion, Elephant, Penguin {
    // common animal properties and methods
}

final class Lion extends Animal {
    // specific implementation for Lion
}

non-sealed class Elephant extends Animal {
    // specific implementation for Elephant
}

sealed class Penguin extends Animal permits EmperorPenguin {
    // specific implementation for Penguin
}

final class EmperorPenguin extends Penguin {
    // specific implementation for EmperorPenguin
}
                

Constraints on Permitted Subclasses: Permitted subclasses must adhere to specific constraints. They must be accessible at compile time, directly extend the sealed class, and use one of the following modifiers: final, sealed, or non-sealed.

Sealed Interfaces: Like sealed classes, you can also seal interfaces in Java.


sealed interface Transaction permits Deposit, Withdrawal, Transfer {
    void process();
}

final class Deposit implements Transaction {
    // implementation for Deposit
}

non-sealed class Withdrawal implements Transaction {
    // implementation for Withdrawal
}

sealed class Transfer implements Transaction permits InternationalTransfer {
    // implementation for Transfer
}

final class InternationalTransfer extends Transfer {
    // implementation for InternationalTransfer
}
                

Here, Transaction is a sealed interface that permits specific classes (Deposit, Withdrawal, Transfer) to implement it, allowing for a controlled and well-defined set of transaction types

Sealed classes and interfaces offer a structured way to define relationships in a domain-specific manner. They are particularly useful in scenarios where you need to tightly control the extension of a class or interface.

Records - Java 16

Records aim to reduce the amount of boilerplate code you have to write when creating classes to represent data. Traditionally, you'd need to define constructors, getters, setters, equals, hashCode, and toString methods manually. Records automate much of this process, allowing you to focus on the data itself.

Records are inherently immutable. Once you create an instance of a record, you can't modify its fields. Moreover, records are implicitly marked as 'final,' ensuring that they cannot be extended. This immutability and finality contribute to the predictability and safety of your code.

Unlike traditional Java classes, records do not allow you to declare instance fields. Instead, they rely on automatically generated fields based on the components you define. This restriction ensures that records maintain their simplicity and immutability.

Records automatically generate essential methods like equals(), hashCode(), and toString(). This reduces the likelihood of bugs and saves you from writing boilerplate code.


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

public class RecordsExample {
    public static void main(String[] args) {
        Point p1 = new Point(2, 3);
        Point p2 = new Point(2, 3);

        System.out.println("p1 equals p2: " + p1.equals(p2)); // true
        System.out.println("p1 hashCode: " + p1.hashCode());   // Same as p2's hashCode
        System.out.println("Point p1: " + p1);               // Point p1: Point[x=2, y=3]
    }
}
    

Here, we have an object that could be any kind of number. The new feature checks what type it is (like Integer, Long, or Double) and lets you use it right away as that type. This makes your code easier to read and less likely to have errors.

Pattern Matching For INSTANCEOF - Java 16

Java 16 introduced a handy feature called Pattern Matching for instanceof. It makes it easier to work with different types of objects. Before, you had to do two steps to check an object's type and then use it. Now, you can do it all at once. For example, take a look at this code:


Object someNumberType = 12345;

if (someNumberType instanceof Integer i) {
    // Now, i is an Integer
} else if (someNumberType instanceof Long l) {
    // Now, l is a Long
} else if (someNumberType instanceof Double d) {
    // Now, d is a Double
}
    

Here, we have an object that could be any kind of number. The new feature checks what type it is (like Integer, Long, or Double) and lets you use it right away as that type. This makes your code easier to read and less likely to have errors.

Pattern Matching for Switch - Java 21

Java 21 introduces an exciting feature called Pattern Matching for switch, making coding much more straightforward and cleaner. This new feature allows you to directly handle different types of objects (like shapes in our example) in a switch statement without extra code. For instance, let's consider shapes like triangles, circles, and squares. With Pattern Matching, you can easily specify actions for each shape directly in the switch cases. For example:


switch (shape) {
    case Triangle t when t.area() > 100 -> {
        // Code for large triangles
    }
    case Circle c -> {
        // Code for circles
    }
    case Square s -> {
        // Code for squares
    }
    default -> throw new IllegalStateException("Unexpected Shape: " + shape);
}
    

In this setup, each case in the switch statement can handle a specific type of shape and execute different code based on the shape’s properties, like its area. This not only makes your code more readable but also helps in avoiding common errors, like forgetting to update the switch statement when a new shape is added. Java's new Pattern Matching for switch is a big step towards writing safer and more maintainable code.

Record Patterns - Java 21

Java 21 introduced 'record patterns', greatly simplifying how developers handle data objects. For instance, consider a record representing a name, with first, middle, and last names:


record Name(String firstname, String middleName, String lastName) {}
    

With the introduction of record patterns, extracting data from such records becomes straightforward and efficient. In a typical usage scenario:


if (o instanceof Name(var fname, var mname, var lname)) {
    System.out.println(lname + "," + fname + " " + mname);
}
    

This code elegantly deconstructs a Name object into its components (firstname, middleName, lastName) using the instanceof pattern, assigning them to variables (fname, mname, lname). It simplifies data extraction, making your code cleaner and more readable.

Enhancing this further, Java now allows these record patterns to be used in switch statements as well. For example:


switch (name) {
    case Name(var fname, var mname, var lname)
when !mname.isEmpty()  ->
    System.out.println(lname + "," + fname + " " + mname);
    case Name(var fname, var mname, var lname)  ->
    System.out.println(lname + "," + fname);
}
    

In this switch statement, we handle different scenarios based on the presence of a middle name. If the middle name isn't empty, the full name is printed. Otherwise, only the first and last names are displayed.

Sequenced Collections - Java 21

Java 21 brings a significant enhancement with the introduction of Sequence Collections, a feature aimed at simplifying operations on collections like ArrayLists. Traditionally, performing tasks like accessing the first or last element of a list, or iterating through a list in reverse order, required a bit of extra code and knowledge about collections. With Sequence Collections, Java introduces a new interface that makes these tasks much more straightforward.

For example, you can now reverse a collection easily with a new reversed method, which provides a view of the collection in reverse order. This addition also includes convenient methods for adding, removing, and accessing elements at both the start and end of a collection.

Explained more detail here: Sequenced Collections in Java 21.

This page is still a work in progress