javarecord

Default values for record properties


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?


Solution

  • 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.