Default values for record properties
Asked Answered
S

1

29

If I have a Java record with 2 properties and I want to define default values for the properties which should be used instead of null. I can either override the getters:

public record MyRecord(Set<String> strings, Boolean required) {

    @Override
    public Boolean required() {
        return Objects.requireNonNullElse(this.required, Boolean.TRUE);
    }

    @Override
    public Set<String> strings() {
        return Objects.requireNonNullElse(this.strings, Set.of());
    }
}

…or I can achieve much the same thing by overriding the default constructor:

public record MyRecord(Set<String> strings, Boolean required) {

    public MyRecord(Set<String> strings, Boolean required) {
        this.strings = Objects.requireNonNullElse(strings, Set.of());
        this.required = Objects.requireNonNullElse(required, Boolean.TRUE);
    }
}

Both of these seem a bit verbose, is there a more concise way to assign default values to record properties?

Scent answered 8/9, 2022 at 9:22 Comment(4)
The second option seems to be the most correctSofko
A Lombok builder would do this well too... depends if you value having a record specifically. Java should add optional parameters with default value as well as call by name (or whatever is the name for the syntax allowing to do method(param1 = "bla"), this would eliminate even more use cases for Lombok, like in Kotlin or ScalaCephalo
You can also add constructors, as long as they eventually (possibly through multiple calls to this(...) calls the generated constructor. For instance; public MyRecord() { this(Set.of(), true); }Kisangani
if you are using Spring and these are managed beans, @DefaultValue is another option.Territorial
H
24

Overriding the accessor methods like in your first variant violates the expectation that you can create an equal object using the accessor methods and the canonical constructor. From the documentation

For all record classes, the following invariant must hold: if a record R's components are c1, c2, ... cn, then if a record instance is copied as follows:

    R copy = new R(r.c1(), r.c2(), ..., r.cn());

then it must be the case that r.equals(copy).

But with your overridden accessor method, the following assertion fails:

MyRecord r1 = new MyRecord(null, null), r2 = new MyRecord(r1.strings(), r1.required());
assert r1.equals(r2);

because the internal fields contain different data.

So the only correct way to fix input data is during the construction, e.g.

public record MyRecord(Set<String> strings, Boolean required) {
    public MyRecord {
        if(strings == null) strings = Set.of();
        if(required == null) required = true;
    }
}

However, you shouldn’t do this null handling at all. Collections should never be null and using Boolean for the constructor implies having a Boolean record component type in general, i.e. also returned by the accessor method. And writing new MyRecord(null, null) instead of new MyRecord(Set.of(), true) doesn’t even save much typing.

If you want to support default values, you should overload the constructor, e.g.

public record MyRecord(Set<String> strings, boolean required) {
    public MyRecord {
        strings = Set.copyOf(strings); // enforce non-null immutable set
    }
    public MyRecord() {
        this(Set.of(), true);
    }
}

So you can use new MyRecord() for the defaults. Or you consider that records are immutable, so constructing multiple instances of defaults isn’t necessary

public record MyRecord(Set<String> strings, boolean required) {
    public static final MyRecord DEFAULT = new MyRecord(Set.of(), true);
    public MyRecord {
        strings = Set.copyOf(strings); // enforce non-null immutable set
    }
}

and use MyRecord.DEFAULT whenever you need the default values. Of course, you still can provide overloaded constructors for the cases that only one parameter should have default values, if that is needed.

Habsburg answered 14/10, 2022 at 12:31 Comment(1)
this is exactly what Spring does internally. Since they control the creation of the instances (beans), they also allow @DefaultValues annotations on record attributes.Territorial

© 2022 - 2024 — McMap. All rights reserved.