Java

Local Records or Classes to Improve the Readability of Stream Operations

You can use local classes or local records to improve the readability of stream operations.

Java 14 came with the preview language feature of Records — a special lightweight class, comparable to similar constructs in other languages, such as record classes in C#, data classes in Kotlin and case classes in Scala.

There’s A) already numerous blog posts explaining Java 14 records and B) numerous articles comparing usage of records with Project Lombok’s @Value (for making immutable classes), so I won’t do that again here. 😉

Brian Goetz explains in JEP 384: Records (Second Preview the motivation behind them and the rules, such as restrictions on the declaration of a record, and the similarities with a “normal” class.

My eye caught the section of local records:

A program that produces and consumes records is likely to deal with many intermediate values that are themselves simple groups of variables. It will often be convenient to declare records to model those intermediate values. One option is to declare “helper” records that are static and nested, much as many programs declare helper classes today. A more convenient option would be to declare a record inside a method, close to the code which manipulates the variables. Accordingly, this JEP proposes local records, akin to the traditional construct of local classes.

In the following example, the aggregation of a merchant and a monthly sales figure is modeled with a local record, MerchantSales. Using this record improves the readability of the stream operations which follow:

photography of vinyl records on wooden surace
Photo by Dominika Roseclay.

The MerchantSales below is a mutable tuple, holding both a single Merchant and the sales which is computed as the stream is processed. We need to capture both, to be able to sort on the computed sales but ultimately return the (original) merchant for that sales.

List<Merchant> findTopMerchants(List<Merchant> merchants, int month) {

   // Local record
   record MerchantSales(Merchant merchant, double sales) {}

   return merchants.stream()
       .map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
       .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
       .map(MerchantSales::merchant)
       .collect(toList());
}

The fact that this is a record defined in a method makes this a local record, and I could immediately recognise the advantages in many Stream API situations where the stream needed to accumulate many values grouped together: like the example shows, map X, calculate or generate Y and keep both around for the next steps in the stream.

Of course, in all these situations I worked around it by obviously also introducing a helper POJO, or re-designing the entire stream logic, but reading the JEP made me remember that Java supports local classes (not records; I mean actually plain classes) pretty much since the beginning.

Local classes are non-static because they have access to instance members of the enclosing block.

Local records and local (inner) classes increase the use of encapsulation. You don’t need to make the type more widely available outside the block where it’s created.

Here’s how the example looks like with a local class. I’m using Lombok’s @Data which generates the required argument constructor and getters/setters to stay in the spirit of less-verbosity-is-more, but you can always use plain vanilla Java too.

List<Merchant> findTopMerchants(List<Merchant> merchants, int month) {
   // Local class
   @Data class MerchantSales {
      final Merchant merchant;
      final double sales;
   }

   return merchants.stream()
       .map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
       .sorted((m1, m2) -> Double.compare(m2.getSales(), m1.getSales()))
       .map(MerchantSales::getMerchant)
       .collect(toList());
}

So, when not on Java 14 yet, or can’t enable the records preview feature, one can always use a local class instead to improve the readability of stream operations.