Records

Record Patterns — Building on JAVA Records

Manoj Nalledathu Palat

Records are becoming quite popular nowadays. Put it simply, they are just constant classes. So what’s the big deal about them. Well, the actual power of records comes with record patterns. Let’s delve into this interesting construct.

Let us say that we have a stack of books:

And we have been confronted with a task — Given a Book, select the book or do not select depending on the type and some conditions. In the old way, let us say the Book is described as a Class:

package oldshelf.book;

import oldshelf.author.Author;

public class Book {
 public Author author;
 public String title;
 
 Book(Author author, String title) {
  this.author = author;
  this.title = title;
 }
}

And let us say we have different kinds of Books – For simplicity’s sake, let us assume that we have only the following categories of books — Moving to the technical representation here is a snapshot of the Type Hierarchy:

For the completeness’ sake, let us look at the definitions of each:

package oldshelf.book;

import oldshelf.author.Author;

public class ChildrensBook extends Book {

 ChildrensBook(Author author, String title) {
  super(author, title);
  // TODO Auto-generated constructor stub
 }

 public int uptoAge;
}
package oldshelf.book;

import oldshelf.author.Author;

public class Comic extends Book {

 String nameOfMainCharacter;
 public int fromAge;

 Comic(Author author, String title) {
  super(author, title);
  // TODO Auto-generated constructor stub
 }
}
package oldshelf.book;

import oldshelf.author.Author;

public class GunBook extends Book {

 GunBook(Author author, String title) {
  super(author, title);
  // TODO Auto-generated constructor stub
 }
}
package oldshelf.book;

import oldshelf.author.Author;

public class Novel extends Book {
 
 Novel(Author author, String title) {
  super(author, title);
  // TODO Auto-generated constructor stub
 }

 String yearOfPublication;

}

Now, I must have lost you already. Anyway, for those who remain, let us see our selection criteria:

package oldshelf;

import oldshelf.book.*;

public class MySelection {

 public boolean isBookSuitableForAge(Object o, int age) {
  if (!(o instanceof Book))
    return false;
  
  if (o instanceof Comic) {
   Comic comic = (Comic) o;
   return age >= comic.fromAge;
  }
  
  if (o instanceof ChildrensBook) {
   ChildrensBook cb = (ChildrensBook) o;
   return age <= cb.uptoAge;
  }
  
  if (o instanceof GunBook) {
   return false;
  }
  return age > 18;
 }
}

No, I don’t want you to go through each and every line of code, but just want to highlight the following points which we do for a Book:

  • if its not a book, its of course false
  • if its a comic book, cast, then we look whether the age is greater than the field fromAge
  • if its a childrensBook, cast, then check for less than..
  • if its GunBook, no
  • default is to check for age>18.

Essentially, the point is that the condition is different for each type but the operations have similarities in different ways. And definitely there is a priority in terms of the control flow.

Disclaimer here: Let us see what we can do with Records. The design is suboptimal and just pedagogic (academic), a more acceptable design would be sealed and permits — but I don’t want to use those restricted identifiers and the concepts here.

So we create a Book in the new shelf as :

package newshelf.book;
import newshelf.author.*;

record BookInfo(Author author, String title) {}

record Novel(BookInfo info, String genre) {}
record GunBook(BookInfo info) {}
record ChildrensBook(BookInfo info, int uptoAge) {}
record Comic(BookInfo info, int fromAge) {}

class Book {
 
 @SuppressWarnings("preview")
 public boolean isBookSuitableForAge(Object o, int age) {
  
  return switch (o) {
    case Comic(var info, var fromAge) -> age >= fromAge;
    case ChildrensBook(BookInfo info, int uptoAge) -> age <= uptoAge;
    case GunBook(var inf) -> false;
    default -> age > 18;
  };
 }

}

There are just records here. We can see that across the different types of Books, the first component is a BookInfo which itself is another record. Interesting part is this switch:

return switch (o) {
    case Comic(var info, var fromAge) -> age >= fromAge;
    case ChildrensBook(BookInfo info, int uptoAge) -> age <= uptoAge;
    case GunBook(var inf) -> false;
    default -> age > 18;
  };

This is the Switch Pattern. Switch patterns are available rom Java 21 onwards) take a type as an expression to switch over and can have case statements that take a type in the Case arm or the case LHS. Let us look at the Case arm in detail, say the first case statement:

case Comic(var info, var fromAge) -> age >= fromAge;

Are you familiar with this syntax? If not its, ok. Here, if we think logically, it would mean if the Object o is a type of Comic, then, this arm should be taken.. but wait a second, why should we specify parameters in the Type?

Well, this is a record, and a record is identified by the Name and Type — its kind of a cross between a type and a method.. we can have a different record with the same name with different parameter types.

ok. so we understand that.. but the RHS doesn’t make much sense.. We’ve just given some variable name on the LHS and we are just using that on RHS. How’s that possible?

Under the hood, the compiler will extract these variables, or let’s say would deconstruct this variable fromAge and it will check whether age is less than fromAge of that record and will return accordingly. This is a deconstruction pattern.

Ok, but a question lingers? what happens if its not a Comic? Since its “->” this is essentially equal to a break after the Case? How does this go to the next line to check the next case?

Well, that’s done by the compiler for you. There is an implicit loop which checks one by one, whether which type you are switching on and whether the type matches? Why one by one? Because a subtype can come earlier than a supertype and the match should be as close as possible. Well, that stuff is for Switch Patterns, let us get back to record patterns.

One more quick question — I just define var fromAge, and then the type is found out by the compiler, Is that correct? yes, the compiler does that work for you.

So now, with the use of records in Case, we are able to extract the extract variable or component of the record. and off you go..

Great! so we are going to have support for Selection? What about if we have lot of Books? Is there any record support in looping? Yes and no — in 20, we had a preview where the foreach construct was supporting Records, but now its gone in 21 since a few issues need to be ironed out — It will come at a later stage.

Before we conclude, lets look at the usage of Record patterns in another construct, a simpler one this time — The “instanceof”.

public String getAuthor(Object o) {
  if (o instanceof BookInfo(
              Author(Name(var f, var lastName), var dob, String Nationality),
              String title)) {
     return lastName;
  }
  return null;
 }

We can see that, we can use the Record patterns to multiple nesting level and still get the lastname?

How does the compiler do it?

Internally, the compiler unwinds each of them.. ie generates code to unwind each of these and then creates the variables and populates the values using the record accessors. All behind the scenes!

Great! So now we can inspect one Book. Is there anything coming for multiple books? Loops?

Yes, this is also on the cards — We had a glimpse of Record Patterns for ForEach in Java 20 as preview but that got shelved and we can expect a more, technically sound version to show its head soon.

And before winding up, one last point — Here I was interested only in lastName, but I have to give all the components — That’s not minimalistic. Well, the underscore is available from Java 22 onwards — just use _ and you don’t have to bother about variable you don’t care..

Happy Pattern Creation with Record Patterns!

Total
0
Shares
Previous Post

Java Records — Etched in Finality

Next Post

Thinking differently about testing

Related Posts