Language selector

Records in Java - why and what for

Java has the record

Java 14 introduced records as a preview feature. There was (is?) a decent amount of confusion, declarations and even heavy insults against ‘enemy’ libraries and IDE plugins. Let me take part in this, please.

First of all one could say: finally. After case classes in Scala and data classes in Kotlin, ‘they finally do that in Java’. That’s true, but we need to add a few pieces to get the full picture.

  • Java tries to be backward compatible as much as possible (if that’s good or if that’s bad, that’s another thing).
  • In May 2020 Java celebrated 25 anniversary and was in the full bloom, it’s not a new language without any ecosystem, so the changes need to fit in and make sense, linke in any other mature system or technology.
  • The hordes of developers need to get prepared for the changes, so drop-by-drop causing to be a better approach than an ice-bucket-challenge.

If you’d like to start with something formal, I recommend reading and understanding JEP 359.

My perception of the records

I think I’d start explaining ‘what records are’ with ‘forget everything you know about Java and let’s start from scratch’. Then I would focus on one sentence from the JEP, which I really like: Records can be considered a nominal form of tuples. Or, as I think Mr. Brian Goetz stated somewhere, named tuples. All right, but what are these tuples? Well, let’s take a look in the friendly yard of SQL. When you type:

SELECT (gross_weight, gross_volume) FROM packages;

as the result you’ll get a set of tuples, each of them having two elements. (Yes, your DB access programme most probably shows them in a table, that’s why they seem to look like rows.) In Java the method returning mass and volume of a single package can be written like this:

GrossWeightAndVolume getGrossWeightAndVolume(PackageID id) {...}

However, in order to make this work, we need to create another class GrossWeightAndVolume, most probably as a JavaBean, together with constructor, getters, and adding equals(), hashCode(), maybe even toString() might be needed too, to have nice output in logs. Instead, we could employ Project Lombok and use @Value annotation. (Using Lombok has its prons and cons but that’s a story for another post.)

In this very moment the ‘performance maniacs’ come in and because of ‘increasing the performance’ instead of another type (the size of the JARs, classloader, … they say) they insist on using primitive types and in this madness they write the signature of the method as de facto dictated by the SQL query, using an array to represent the tuples:

double[] getGrossWeightAndVolume(long id) {...}

Trouble is, having such a SQL query, we can have another one (let’s not focus on the ‘business value’ for the sake of example):

SELECT (total_mileage, trunk_volume) FROM cars;

You can again create a JavaBean (and maintain it!) or happily use double[].

Things get ‘fun’ when we realise that the tuples coming both in the SQL, both in double[] need to be taken care of really well, because they can never get mixed. If they get mixed, we can suddenly encounter absurds like this:

SELECT (gross_weight, gross_volume) FROM packages
UNION
SELECT (total_mileage, trunk_volume) FROM cars;

Two double[] can be mixed exactly the same way. Can it be even ‘funnier’? Of course! We’re not skipping types and compiler to be bored… What happens when we need to return an integer and a floating number? Then we have to change the array to Number[], the autoboxing kicks in… Maybe we need to add a String too… so what do we then? Serializable[]? ‘But you don’t have to, you can use the Pair or the Triple class with generics!’. Right, but how can we tell then that one Pair<Double, Double> can’t be mixed with another Pair<Double, Double> which logically and semantically keeps something different? When you see Set<Pair<Double, Double>>, what do you expect to find inside? Package sizes? Complex numbers?

Every time when you get rid of type control in Java checked by the compiler, and you start treating the same types differently depending on the context, then to the name Java you actually add Script.

IMHO for these reasons, in order not to mix plums with Camaros, Java has named tuples, which are called records.

How to create a record?

Very easy. JEP says the records are a ‘restricted form of a class, just like enum’. If you’d like to define a record e.g. for complex numbers, it can be written like this:

record Complex(double real, double imaginary) {}

If you need to keep the mass, and the size of packages, another record can be defined:

record PackageSize(double grossWeight, double grossVolume) {}

What’s the outcome? The compiler will instantly create for us:

  • canonical constructor, so we can create new instances of records: Complex aComplex = new Complex(3.0, 1.7);
  • accessors for each component, so we can e.g. get the real part: double x = aComplex.real();
  • equals() and hashCode() methods using all record’s components,
  • toString() method which will return the name of the record’s class plus the names and values of all the components, e.g. Complex[real=3.0, imaginary=1.7]

And that’s actually it when it comes to generated items. However, it hurts me a bit that we, the developers, forget about even more important aspect of the records: a record gives us the name and the type! So having:

Set<Complex> aSet = new Set<>();

we cannot add to this set a different kind of record (even if it has exactly the same components):

// no way, na da
aSet.add(new PackageSize(15.0, 10.0));

It helps to remember that the compiles is not an old grumpy architect, but a friendly pal, who is always pair programming with you.

Read only

If you have missed that by any chance it’s worth highlighting, that the records don’t have anything like ‘setters’. For each component only the access/read method is generated, there is no method to change them. Of course, under the hood the value of each component is stored as a ‘private final’ field, so it’s obvious the references can’t be changed or overwritten. What can be done, is to have as a component something that’s mutable, like ‘ArrayList’ or ‘AtomicInteger’. However, it doesn’t seem to be always reasonable.

In the next post I describe what can be and what cannot be done with the records.

Language selector