Java 19: Record Patterns

Let’s have a look at record patterns, another quite interesting Java 19 preview feature delivered by Project Amber. Project Ambers aims at improving the Java language by simple and powerful new features that enhance the Java syntax.

Java had always the reputation of being very verbose and looking at other popular programming languages like Python, this is definitely the case. However Project Amber works an removing boilerplate code from the language step by step and Java programs written with the latest language features can be as short as in other languages.

On big step forward was the introduction of records also by Project Amber, which remove the need to write getters and setters and override methods inherited by the Object class (equals, hashCode and toString), which are often missed to implement properly. If you only want an immutable class that holds your data, records are the one line solution to that. All the boilerplate code we used to write will be added by the compiler automatically.

With the new record patterns syntax, extracting data from your records becomes as easy as defining them.

Let’s assume we have three simple records that we want to use in our code:

record Monkey(String name, int age) {};
record Donkey(String name, int age) {};
record Zoo(Monkey monkey, Donkey donkey){};

Monkey and Donkey are simple data classes that hold the name and age of the animal. Then we have a record Zoo that consists of a single Monkey and Donkey as properties. It is a very small zoo.

As a simple example for pattern matching, we want to print the name and age of the animals after we checked that we are actually dealing with a Zoo object by an instanceof check. In older Java versions, this would look like the following code:

public static void printAnimalNameAndAge(Object o) {
  if (o instanceof Zoo z ) {
    String mName = z.monkey().name();
    String dName = z.donkey().name();
    int mAge = z.monkey().age();
    int dAge = z.donkey().age();
			
    System.out.println(mName + " the monkey is "+ mAge + " years old." );
    System.out.println(dName + " the donkey is "+ dAge + " years old." );
  }
}

In line (2) we check if the parameter o is a Zoo object and define the variable z for it using the regular pattern matching syntax . If so, we extract the animal name and age in line (3) to (7). Finally, we print name and age for both animals to the console.

With the new record patterns, we can write the code even more compact and skip line (3) to (7) completely and extract the data together with the instanceof check:

public static void animalNameAndAge(Object o) {
  if (o instanceof Zoo (Monkey(String mName, int mAge) m, Donkey(String dName, int dAge) d )) {
    System.out.println(mName + " the monkey is "+ mAge + " years old." );
    System.out.println(dName + " the donkey is "+ dAge + " years old." );
  }
}

In line (2), we check for the object type Zoo and create variables for the Monkey, Donkey and both ages and names at the same time if the Zoo check passes. This makes the code a lot shorter and easier to read. The syntax looks like we would invoke the constructor after the type declaration without the new keyword. The variables declared can then be used in the if block.

The same syntax can also be used in switch statements, which opens up some nice option for switching over corresponding types. This is actually a long wanted language feature also know as a type switch.

Let’s look at a simple example. We have the following class hierarchy with a sealed interface Animal and a couple of implementations:

sealed interface Animal permits Donkey, Monkey, Mouse {};
record Donkey(String name, int age) implements Animal {};
record Monkey(String name, String sex) implements Animal {};
record Mouse(String name, int noOfKids) implements Animal {};

You can now switch over this hierarchy and extract the member variables at the same time with a record pattern:

Animal[] animals =  {new Donkey("Bob", 5), new Monkey("Jeff", "male"), new Mouse("Mick", 42)};

for (Animal animal : animals ) {
  switch (animal) {
  case Donkey(String name,int age) -> 
    System.out.println("Donkey " + name + " is " + age + " years old.");
  case Monkey(String name,String sex) -> 
    System.out.println("Donkey " + name + " is " + sex + ".");
  case Mouse(String name,int noOfKids) -> 
    System.out.println("Mouse " + name + " has " + noOfKids + " little mice.");
  }	
}

In line (5), (7) and (8) you can see the new record pattern in action. The best thing about this switch statement is, because Animal is a sealed interface, the compiler will make sure that all subtypes of Animal will be included in the case statements. If one is missing the compiler will complain. You won’t even need a default case, since the compiler makes sure that all cases are covered. If some wise developer decides to introduce a new animal subclass, but misses to implement the case statement, the compiler will throw an error too.

Another useful use case would be to use record patterns in the enhanced for loop, but this has not been released in Java 19. However, the second preview of record patterns (JEP 423) will include changes for the enhanced for loop and will be released with Java 20.

If you want to check out the code, you can find it in this Github repo. Since this is a preview feature in Java 19, make sure you start and compile your program in preview mode as described here.


Beitrag veröffentlicht

in

von

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert