19

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?

M A
  • 71,713
  • 13
  • 134
  • 174
stridecolossus
  • 1,449
  • 10
  • 24
  • 1
    If you're interested though, kotlin which is built on top of and compiled against the JVM, has in-built [immutable collections](https://kotlinlang.org/docs/collections-overview.html#collection-types) and other fancy stuff, you can get started [here](https://kotlinlang.org/docs/getting-started.html) – Lino May 19 '21 at 13:36
  • @Lino - Guessed that was the case. Yes would be nice to have something along the lines of the Kotlin collections. – stridecolossus May 19 '21 at 13:55
  • FYI, both [*Google Guava*](https://github.com/google/guava/wiki/ImmutableCollectionsExplained) and [*Eclipse Collections*](https://www.eclipse.org/collections/#immutable) offer explicitly immutable collections. – Basil Bourque May 19 '21 at 16:20
  • 5
    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. – Brian Goetz May 21 '21 at 14:07
  • @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". – stridecolossus May 26 '21 at 08:54
  • FYI - I write an annotation processor that adds a companion builder for records. This builder can optionally wrap collections in List.of, etc. https://github.com/Randgalt/record-builder – Randgalt Aug 18 '21 at 13:28

1 Answers1

34

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.

M A
  • 71,713
  • 13
  • 134
  • 174
  • 8
    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. – Joachim Sauer May 19 '21 at 13:59
  • 2
    Interesting, it's a bit nasty essentially re-writing the arguments, but it does work! Have to switch off parameter modification warnings. – stridecolossus May 19 '21 at 14:03
  • 2
    @stridecolossus I agree it felt a bit weird when I found about it :) – M A May 19 '21 at 14:08
  • 8
    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. – Brian Goetz May 21 '21 at 14:09
  • 4
    @stridecolossus 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. – Brian Goetz May 26 '21 at 13:10