Language selector

Records in Java - reflection

Things get interesting

When Java 14 was (about to be) released I saw a few discussions following more or less this schema:

  • Oh, records in Java, cool, finally we have automatically generated setters and getters!
  • No, records in Java are POJOs without any setters…
  • Oh, I see… Cool, finally in Java we have generated Beans, without any setters though!

Now it’s time for me to write two most important things about records in Java. The first:

Records are not JavaBeans

and the second:

Records are not JavaBeans

People shaken and stirred will find the explanation this below.

New kids in the class

In the previous entry on records it was shown, that the records look a bit like enumeration types introduced in Java 5. Just like an enum inherits directly from java.lang.Enum and nothing can inherit from it, a record inherits directly from java.lang.Record and nothing can inherit from it as well.

One of the differences is that enum is a keyword, whereas record (just like var) is not. (Don’t focus on the syntax colouring in the browser, please. The JS plugin doesn’t support records now ;-)) .

//this won't work
var enum = ...
//this works
var record = ...

Introduction of records brought us two new methods in the Class class.

Just as we can check if an object is an enum by using Class.isEnum(), we can now check if an object is a record by calling Class.isRecord().

And when the result of the isRecord() tells us ‘it’s a record’, then we may wish to check what are the components of the record. We can get them in the form of an array calling public RecordComponent[] Class.getRecordComponents(). This works just as getting the list of fields, methods, etc. Of course, having a RecordComponent, we can try to get deeper to evaluate the possibilities. Probably the most intriguing one is getAccessor(), which allows us to get the value of the component.

Please pay some attention to the name of this method. It’s not a getter, it’s an accessor. So called gettery by convention have their names starting with get (or is).

Description of the bean

Let me follow this path. In this journey our companions will be two classes. A “typical” bean BeanWithSetters and a record ReflectionCheck. They look the following way (the entire code available on GitHub):

record ReflectionCheck(int a, String b) {}

class BeanWithSetters {

	private final UUID id;
	private String stringField;

	public BeanWithSetters(UUID id, String stringField) {
		this.id = id;
		this.stringField = stringField;
	}

	public UUID getId() {
		return id;
	}

	public String getStringField() {
		return stringField;
	}

	public void setStringField(String stringField) {
		this.stringField = stringField;
	}

}

What results can we expect when we make them go through the following machine?

BeanInfo beanInfo = Introspector.getBeanInfo(object.getClass());
Stream.of(beanInfo.getPropertyDescriptors()).forEach(System.out::println);

For JavaBean we get what we expect. The bean has two properties (except the class), one of which has only a getter (read method), the other one has a setter (write method) and a getter:

java.beans.PropertyDescriptor[name=class; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@5ce65a89; required=false}; propertyType=class java.lang.Class; readMethod=public final native java.lang.Class java.lang.Object.getClass()]
java.beans.PropertyDescriptor[name=id; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@51521cc1; required=false}; propertyType=class java.util.UUID; readMethod=public java.util.UUID BeanWithSetters.getId()]
java.beans.PropertyDescriptor[name=stringField; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@1b4fb997; required=false}; propertyType=class java.lang.String; readMethod=public java.lang.String BeanWithSetters.getStringField(); writeMethod=public void BeanWithSetters.setStringField(java.lang.String)]

What happens when we perform a similar analysis of a record?

java.beans.PropertyDescriptor[name=class; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@5ce65a89; required=false}; propertyType=class java.lang.Class; readMethod=public final native java.lang.Class java.lang.Object.getClass()]

The only thing working like in a JavaBean is getting the class of the record. The components from the record’s declaration don’t generate any getters. A record is not a JavaBean, Q.E.D.

Implications

The fact that the records are not JavaBeans has some implications. Java 14 didn’t emerge in the void. There are more than 25 years of development, creating the ecosystem, tons libraries and frameworks, many developers use it every day. Summarising: it’s a mass with some momentum, which inherited an assumption that (almost) everything is a JavaBean if there’s some data inside.

Suddenly the records show up, being perfect for keeping data, and they don’t have any _getter_s. You know, a VO, a DTO, but no _getter_s. Cool, isn’t it?

That’s why some libraries may for time being not support them in the ideal way. (That’s why I think the concept of preview features is great for everyone to recover from the shock and make needed changes.)

Let’s examine Jackson in version 2.10.3 (whole code on GitHub). Having a record (for example this one):

record SerializationRecordCheck(String justOneField) {}

if we wish to turn this record into JSON using Jackson:

new ObjectMapper().writeValueAsString(new SerializationBeanCheck("Bean"))

we have to face such a beautiful exception:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 
No serializer found for class SerializationRecordCheck and 
no properties discovered to create BeanSerializer 
(to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

These four lines can explain that ‘records are not Java Beans’ perhaps even better than BeanInfo. Jackson expects that it’ll be given for serialization a JavaBean, that is: an object having some methods starting with get/is (except getClass()). However, there are no such methods in the record!

I guess the most probable scenario in the future is that Jackson (and other libraries as well) will check the result of the isRecord() and will (de)serialize records differently than JavaBeans. Just as they handle enums already.

Okay, but what shall be done if we badly want to use records now and they appear at the edges of our hexagon?

Most probably we don’t want to set the SerializationFeature.FAIL_ON_EMPTY_BEANS flag, because we’re trying to serialize the record, so we do care about the data.

  • We can add the getters by hand, so they are calling accessors inside. I don’t recommend that.
  • We can create our own serializer. It makes sense if we don’t want to keep our domain code pure, free from any annotations.
  • We can also annotate all record’s components with@JsonProperty.

This is how the last solution looks in the code:

record SerializationRecordCheck(@JsonProperty String justOneField) {}

Annotations we use for the components “are copied” for the accessors and fields in the records. So we can image the record looking the following way after being compiled:

@JsonProperty 
private final String justOneField;

@JsonProperty 
public String justOneField() {
    return justOneField;
}

“Oh, you saw it, you saw it! They didn’t implement thre records as they should, because this librarys doesn’t work!!1one”. Two questions:

  • Were enums or the generic types working in all the libraries right away after being introduced?
  • Is the tail supposed to wag the dog?

A similar issue was faced in Scala when some case classes were used by some Java libraries expecting getter. In Scala, to make the getter appear in the class as well, the components have to be annotated with @BeanProperty. For me, it’s hard to say if that was better or worse approach. It was better, because it makes things work “right away” with the ecosystem. It was worse, because the rotting leg wasn’t chopped off. Let’s be honest: Scala doesn’t have such coverage and such reach as Java does, so Scala had to fit into Java. Does Java itself need to fit into Java?

I think the subject of ‘lack of getters’ was summarised in the best way by the records JEPs' author, Mr. B. Goetz himself: link to his e-mail.

What do you think?

I think I kind of like this “no getters approach”, but I develop not only in Java, so I got used to this concept long ago. Actually I could only sigh ‘well, finally…’ Besides very accurate judgement of the Java architects I see additional benefit: the accessors simplify our lives even more. Naming is one of the two most difficults contepts in IT. Theoretically the _getter_s, as the name implies, were supposed to start with get prefix. However, for a boolean there could also be is. That made the less thought-out solution crash already (because of the “regex diarrhea”). What if that was a Boolean? Then shall we stick to get or was it more like is. What about field Boolean wasDelivered: getWas..., isWas..., just was... alone? With the accessors the rule is trivial.

More on serialization

Some say we could have some new features in Java sooner if not ‘oh crap, this *** serialization’. Records can be serialized just like other objects, all we need is to implement the Serializable interface. The binary format is a bit different, but the JVM handles that, we don’t have to. For the intrigued souls some code to run.

However, there is a huge change when it comes to DEserialization. Records are deserialized using the canonical constructor, we one could risk stating that Java is returning to the OOP track.

Language selector