Search code examples
javaimmutabilityjava-recordjava-16

Enforce immutable collections in a Java record?


Java records are used to implement shallowly immutable data carrier types. If the constructor accepts mutable types then we should implement explicit defensive copying to enforce immutability. e.g.

record Data(Set<String> set) {
    public Data(Set<Thing> set) {
        this.set = Set.copyOf(set);
    }
}

This is mildly annoying - we have to

  1. implement an old-school POJO constructor (replicating the fields) rather than using the canonical constructor and
  2. explicitly initialise every field just to handle the defensive copy of the mutable field(s).

Ideally what we want to express is the following:

record SomeRecord(ImmutableSet<Thing> set) {
}

or

record SomeRecord(Set<Thing> set) {
    public SomeRecord {
        if(set.isMutable()) throw new IllegalArgumentException(...);
    }
}

Here we use a fictitious ImmutableSet type and Set::isMutable method, in either case the record is created using the canonical constructor - nice. Unfortunately it doesn't exist!

As far as I can tell the built-in collection types (introduced in Java 10) are hidden, i.e. there is no way to determine if a collection is immutable or not (short of trying to modify it).

We could use Guava but that seems overkill when 99% of the functionality is already in the core libraries. Alternatively there are Maven plug-ins that can test classes annotated as immutable, but again that's really a band-aid than a solution.

Is there any pure-Java mechanism to enforce a immutable collection?


Solution

  • You can do it already, the arguments of the constructor are mutable:

    record SomeRecord(Set<Thing> set) {
        public SomeRecord {
            set = Set.copyOf(set);
        }
    }
    

    A related discussion mentions the argument are not final in order to allow such defensive copying. It is still the responsibility of the developer to ensure the rule on equals() is held when doing such copying.