Blog
navigate_next
Java
Record Patterns and enhanced SWITCH statement in Java 21
Pratik Dwivedi
December 8, 2023

Introduction

Data Science and Analytics are two of the most popular applications of computers and technology today. In this post, we discuss how to record patterns and an enhanced switch statement to facilitate data definition, sophisticated analytics, and semantic ease.

Java 21 introduces the Record data type to define complex data structures/data aggregate objects, and Record Patterns to deconstruct the Record values.

Record data type

The Record data type in Java21 allows for the definition of composite data types that can represent your aggregate data structures like primitive types/JSON/XML etc.

The java.lang.Record is the common base class of all Java language record classes. A record class is a shallowly immutable, transparent carrier for a fixed set of values, called the record components. All record components are immutable, in order not to modify the underlying source data in any way, and enabling the compiler to provide faster processing as it knows the data values will not be changed during the program. These record components are declared in the class header, which forms the record descriptor for the class. You just need to use the "record" keyword to create a record class in Java.Java 21 automatically provides the following when you construct a Record class:-

  • All record components are marked as Final
  • A default Canonical constructor, based on the record components is provided
  • For every data component, an accessor method is provided by default
  • A <span  class="teal" >toString()</span> method implementation is provided, that prints the record components along with the component names

This alleviates the programmer from providing a lot of boilerplate code and allows him to focus on the processing that these records must undergo to provide some meaningful results/inferences. If you provide your own constructor and accessor methods, you must implement all the validations and checks yourself, otherwise, the defaults provided by Java are good enough for almost all practical purposes. You can have static variables inside records that can be accessed the same way as classes by using the record class name. Also, you can apply annotations like <span class="pink">@Target</span> / <span class="pink">@NotNull</span> / <span class="pink">@Transient</span> / <span class="pink">@NotColumn</span> / <span class="pink">@Overrideetc</span>. to record components. In essence, the Record class object becomes a simple data carrier of immutable record components, each having its unique properties, and ready for safely applying analytical methods.

Record types can hold generic types, a behaviour similar to other Java classes.

 
record myList(int id, T value) { ... }

 // followed by 
 myList myDoubleList = new myList<>(int id, Double.valueOf(85634.9078));

Java 21 has extended the class "Class" to include two methods, <span class="teal">isRecord()</span> and <span class="teal">getRecordComponents()</span>, where <span class="teal">getRecordComponents()</span> method returns an array of record component objects within a record, ordered in their original order of declaration. Records can be serialized and de-serialized, also they can be declared to be runnable. So, records are very useful constructs for modelling domain model classes, data transfer objects (DTOs) and temporary data holders.

Next, to process records, bind values to record components, match values against a record type or apply transformations to records; Java 21 provides the abstraction of Record patterns.

Record patterns and their significance

A record pattern consists of a type and a (possibly empty) record component pattern list.Record patterns aim to extend pattern matching to express more sophisticated, templated data queries. You can use a record pattern inside a switch/for/instance construct to test whether a value is an instance of a record class type and, if it is, to recursively perform pattern matching on its component values.e.g. myList<Double> (var someInt, double someValue) is a record pattern used below

 switch(someList) 

case   myList (var someInt, double someValue) -> System.out.println("List of Double with id = " + someInt);

As demonstrated above, there is no need to declare instance variables, the record pattern does this itself, and if the value matches the pattern then these instance variables are initialized by invoking the accessor methods of the record type myList<>, thereby making the record components easily accessible.

Record and type patterns can be nested to enable a powerful, declarative, and composable form of data navigation and processing. By composable we mean data definitions can be easily combined, mixed, and matched to create new aggregate types or functionality.

Querying data at the language level, and not delegating it to some other API or database level, not only adds speed but also power in the hands of the developer. Instead of having to rely on the functions provided by the 3rd party querying engine, the developer can code custom queries that suit the applications' business logic. These data queries can be templatized, and parameterized and can have configurable runtime behavior.

Java 21 does not aim to change the syntax or semantics of type patterns, so existing old code can work fine and newer code can follow time-tested coding patterns.

Type Patterns and enhanced Pattern matching

Java has been making improvements to pattern matching since Java 16.Java 16  extended the <span class="pink">instanceof</span> operator to take a type pattern and perform pattern matching.

 
// Old code
if (obj instanceof String) {
String localString = (String)obj;
... use localString ...
}

// New code
if (obj instanceof String localString) {
... use localString ...
}


So in the new code, there is no need to explicitly cast obj to a String, instead you can directly use the string used for comparison, <span class="pink">localString</span> here in your code. Internally, if the pattern matches and the <span class="pink">instanceof</span> expression is true, the pattern variable <span class="pink">localString</span> is initialized to the value of obj cast to String, which can then be used directly in the contained block.

There is more functionality added to the <span class="pink">instanceof</span> operator, consider the code below that uses a Triangle record pattern

 
public record Triangle(int base, int sideA, int sideB, int height) {}

void calclulateArea(Object o) {
  if (o instanceof Triangle(int base, int sideA, int sideB, int height)) {
    System.out.println("Area is = " + 1 / 2 * (base * height));
  }
}

If you closely check what’s happening here, you will see that now, as soon as a value is matched against the pattern, the accessor methods of the pattern(Triangle in this case) are invoked, its data components initialized into local variables and thereby disaggregating the record into its constituent components. Here, the <span class="pink">instanceof</span> operator detects the Triangle record pattern, calls its accessor methods and initializes the variables named base/height/sideA etc. So no need to do the following:

 
Triangle triangle = (Triangle)o ;
int base = triangle.getBase();
int height = triangle.getHeight();  ....
... System.out.println("Area is = "+ 1/2*(base*height) );

i.e. No need to instantiate a variable of Triangle type just to invoke its accessor methods, it's done for you implicitly.

This not only makes your code simpler, it demonstrates the semantics of the underlying data in the record pattern(Triangle) and facilitates the usage of its data components.The rule of thumb is "An expression is compatible with a record pattern if it could be cast to the record type in the pattern without requiring an unchecked conversion."

Patterns of generic/raw classes

Another cool feature of record class is that you can define patterns of generic/raw classes. Then you can match other record patterns that use generic types.e.g. Let's define a record pattern consisting of a Stack of objects and the length of the Stack.

 
record FixedLengthStack(Stack stk, Integer len){};

Now you can match a Stack of Strings/Floats/<Objects> with this record pattern.

 
static void recordInference(FixedLengthStack < Stack < String > , Integer > stringStack) {
    switch (stringStack) {
    case FixedLengthStack(var someStack,
      var itsLength) -> System.out.println("Stack of Strings of fixed length " + len);... // Inferred record pattern FixedLengthStack,Integer>(var someStack, var itsLength) ... } }

We are demonstrating two concepts via the above examples.

  1. You can use generic as well as user-defined classes in your record patterns
  2. You can use the VAR keyword to match against a record component without pre-stating the type of the component.The compiler will now try to infer the type of the pattern variable introduced by the VAR pattern and match it with the defined record pattern.So, you can have FixedLengthStack of different types like String/Double/user-defined classes.

Nestable Record Patterns

ADD - example of  OR extend the above example itself to show nesting. The real utility of pattern matching lies in its ability to match complex object graphs. An object graph is a collection of objects, the relationship between them and the operations/methods they can call on each other. A nested pattern consists of record patterns that are recorded in themselves and can be further decomposed into their constituent data components. Consider the example below

 
enum Acc_Type {
  SAVING,
  CURRENT,
  FIXED_DEPOSIT
}
enum Holding_Type {
  SINGLE,
  JOINT
}
record Person {
  String name, int age, int phone_number, String address
}
record Account {
  ind acc_id, Holding_Type htype, Person acc_holder
}
record Bank_Account {
  ind acc_id, Acc_Type atype, Holding_Type htype, Person acc_holder
}
record Wallet_Account {
  ind acc_id, Acc_Type htype, Person acc_holder
}

static void printAddress(Account some_Acc) {
  if (some_Acc instanceof Bank_Account(int id, Acc_Type atype, Holding_Type htype, Person holder)) {
    System.out.println(holder.address);
  }
  if (some_Acc instanceof Wallet_Account(int id, Holding_Type htype, Person holder)) {
    System.out.println(holder.phone_number);
  }
  .......
} 

Here, the Person itself is an aggregate record object in itself, which can be further decomposed or its data components accessed. So, if it's a wallet account we’re interested in knowing the phone number associated with the mobile wallet, else the address of the bank account holder. These constructs help in keeping the code concise and clean.  Also, we can nest a pattern inside another pattern, and decompose both the inner and outer patterns at once. In the example above, instead of passing the Person record in the pattern, we could have decomposed it and passed the constituents or record components of the Person, and then accessed the address directly.

 
if (some_Acc instanceof Bank_Account(int id, Acc_Type atype, Holding_Type htype, Person(String name, int age, int phone_number, String address))) {
  System.out.println(address); // no need to use holder.address here
}

Another very useful feature of record patterns is that they can now be used on for clauses.

 
static void print_Address(Account[] accountArray) {
  for (Bank_Account(var acc_id,
      var atype,
        var htype,
          var holder): accountArray) { // Record Pattern in he	ader!
    System.out.println("Account holder's adress is: " + holder.address);
  }
}

Next, we discuss the enhanced switch statement which allows for identifying and acting upon complex record patterns.

The Extended SWITCH statement

Starting from JDK 17, the SWITCH statement was extended to use type patterns, so now you can test patterns inside switch statements.

Java 21 further expanded the expressiveness and applicability of the Switch construct by allowing nested record patterns in case labels and relaxing the null-hostility.

Advance-Pattern matching in CASE labels

Consider the following example

 
class Shape {}
class Rectangle extends Shape {}
class Triangle extends Shape {
  boolean testIsoceles() {
    ...
  }, boolean testEquilateral()...
}

static void testShape(Shape s) {
  switch (s) {
  case null ->
  break;

  case Triangle t when t.testEquilateral() -> System.out.println("Its an Equilateral triangle");
  case Triangle t $$ t.testIsoceles() -> System.out.println("Its an isoceles triangle");

  case Rectangle t -> System.out.println("Its a rectangle");
  ...
  default -> System.out.println("Its just a shape");
  };
}

See how we can now give types/classes to be matched in CASE labels, this allows case labels with patterns where selection is determined by pattern matching, rather than just constants. You can also test multiple conditions in a case label, much like an IF statement adding fine-grained functionality to switch constructs. The above code also demonstrates how Switch blocks now allow when clauses to specify guards to pattern case labels. Also, you can have null as a case label, so no worrying about <span class="pink">NullPointerException</span> in Switch statements.

Some readers will suspect that in case many pattern labels could match a selector expression, there could be a situation where there is a label at the top which always matches and will never let the control flow to the labels below it. This is certainly a possibility, and the Java 21 compiler raises an error if a pattern label can never match because a preceding one will always match first.

If the very first case label above was

 
case Shape sp    -> System.out.println("Its a shape"); 

The compiler will give this error:
error: this case label is dominated by a preceding case label

Allowing enumerations in case labels

Another useful extension of the switch statement is that it now allows enumeration constants in case labels.

 
static void testforAccountType(Account acc) {
  switch (acc.Acc_Type) {
  case SAVINGS -> System.out.println("It's a Savings account !");
  case CURRENT -> System.out.println("It's a Current account !");
  .....
  default-> System.out.println("Some other account");
  }
}

Extending pattern matching to switch allows an expression to be tested against several patterns, each with a specific action so that complex data-oriented queries can be expressed concisely and safely. Of course, older switch statements continue to function as before.

Conclusion

Java 21 supports new and expressive ways of modelling data, and sophisticated pattern matching enables developers to express the semantic intent of their models clearly.  This further facilitates data usage and introduces flexibility and extensibility to the data processing routines.

The above are initial steps towards a more declarative, data-focused style of programming in Java.Java 21 enhancements help Java remain contemporary, competitive and efficient.Thanks for your time.

Pratik Dwivedi
December 8, 2023
Use Unlogged to
mock instantly
record and replay methods
mock instantly
Install Plugin