Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java records with nullable components

I really like the addition of records in Java 14, at least as a preview feature, as it helps to reduce my need to use lombok for simple, immutable "data holders". But I'm having an issue with the implementation of nullable components. I'm trying to avoid returning null in my codebase to indicate that a value might not be present. Therefore I currently often use something like the following pattern with lombok.

@Value
public class MyClass {
 String id;
 @Nullable String value;

 Optional<String> getValue() { // overwrite the generated getter
  return Optional.ofNullable(this.value);
 }
}

When I try the same pattern now with records, this is not allowed stating incorrect component accessor return type.

record MyRecord (String id, @Nullable String value){
 Optional<String> value(){
  return Optional.ofNullable(this.value); 
 }
}

Since I thought the usage of Optionals as return types is now preferred, I'm really wondering why this restriction is in place. Is my understanding of the usage wrong? How can I achieve the same, without adding another accessor with another signature which does not hide the default one? Should Optional not be used in this case at all?

like image 616
Leikingo Avatar asked Jul 16 '20 23:07

Leikingo


People also ask

Can records implement interfaces Java?

Records can be extended by additional constructors, static fields, and static as well as non-static methods. The canonical constructor can be overridden. Records can implement interfaces (including sealed ones) but cannot extend classes, nor can they be inherited from.

Can records have constructors?

If you want your record's constructor to do more than initialize its private fields, you can define a custom constructor for the record. However, unlike a class constructor, a record constructor doesn't have a formal parameter list; this is called a compact constructor.

Does Java 8 have records?

Takeaway. The takeaway of this article is that you can use Java records with Java 8, 9, ... even before it becomes available.

What is canonical constructor in Java?

The compiler also creates a constructor for you, called the canonical constructor. This constructor takes the components of your record as arguments and copies their values to the fields of the record class. There are situations where you need to override this default behavior.


Video Answer


2 Answers

A record comprises attributes that primarily define its state. The derivation of the accessors, constructors, etc is completely based on this state of the records.

Now in your example, the state of the attribute value is null, hence the access using the default implementation ends up providing the true state. To provide customized access to this attribute you are instead looking for an overridden API that wraps the actual state and further provides an Optional return type.

Ofcourse as you mentioned one of the ways to deal with it would be to have a custom implementation included in the record definition itself

record MyClass(String id, String value) {
    
    Optional<String> getValue() {
        return Optional.ofNullable(value());
    }
}

Alternatively, you could decouple the read and write APIs from the data carrier in a separate class and pass on the record instance to them for custom accesses.

The most relevant quote from JEP 384: Records that I found would be(formatting mine):

A record declares its state -- the group of variables -- and commits to an API that matches that state. This means that records give up a freedom that classes usually enjoy -- the ability to decouple a class's API from its internal representation -- but in return, records become significantly more concise.

like image 174
Naman Avatar answered Sep 25 '22 22:09

Naman


Due to restrictions placed on records, namely that canonical constructor type needs to match accessor type, a pragmatic way to use Optional with records would be to define it as a property type:

record MyRecord (String id, Optional<String> value){
}

A point has been made that this is problematic due to the fact that null might be passed as a value to the constructor. This can be solved by forbidding such MyRecord invariants through canonical constructor:

record MyRecord(String id, Optional<String> value) {

    MyRecord(String id, Optional<String> value) {
        this.id = id;
        this.value = Objects.requireNonNull(value);
    }
}

In practice most common libraries or frameworks (e.g. Jackson, Spring) have support for recognizing Optional type and translating null into Optional.empty() automatically so whether this is an issue that needs to be tackled in your particular instance depends on context. I recommend researching support for Optional in your codebase before cluttering your code possibly unnecessary.

like image 28
Lovro Pandžić Avatar answered Sep 22 '22 22:09

Lovro Pandžić