Enforce immutable collections in a Java record?
Asked Answered
I

1

21

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?

Illnatured answered 19/5, 2021 at 13:18 Comment(6)
If you're interested though, kotlin which is built on top of and compiled against the JVM, has in-built immutable collections and other fancy stuff, you can get started hereFashion
@Fashion - Guessed that was the case. Yes would be nice to have something along the lines of the Kotlin collections.Illnatured
FYI, both Google Guava and Eclipse Collections offer explicitly immutable collections.Antrorse
Your claim of "old school POJO constructor" is simply incorrect. Records support a compact constructor that let you transform the arguments, which are sugar for the old-school version that you're "glass 1% empty" about.Ostensive
@BrianGoetz The answer below explains how to use the compact constructor to transform the arguments - I'd overlooked or forgotten that could be done. The question used the term "canonical constructor" when it probably ought to have written "compact".Illnatured
FYI - I write an annotation processor that adds a companion builder for records. This builder can optionally wrap collections in List.of, etc. github.com/Randgalt/record-builderPalladium
A
37

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.

Abreact answered 19/5, 2021 at 13:55 Comment(5)
That's neat, I hadn't known that that's possible in Records. It's also interesting to point out that Set.copyOf() doesn't actually do any copying if the input is already an immutable set created via Set.of() (or related methods). Just like the Guava immutable collections did.Ottavia
Interesting, it's a bit nasty essentially re-writing the arguments, but it does work! Have to switch off parameter modification warnings.Illnatured
@Illnatured I agree it felt a bit weird when I found about it :)Abreact
You can think of the compact constructor as an N-to-N transform on the parameters followed up by a call to the canonical constructor with the transformed values. This permits validation (fail on bad inputs), normalization (e.g. reduce fractions to lowest terms), and defensive copies. While it feels a little weird at first, this is exactly why it is there.Ostensive
@Illnatured If your IDE is warning you when modifying a parameter of a compact record constructor, that's an IDE bug. While parameter modification is usually discouraged, compact constructors are an exception to this. You should file an RFE with your IDE.Ostensive

© 2022 - 2024 — McMap. All rights reserved.