Make record truly immutable even a field is a collection

272 views Asked by At

The naive approach to have a record field that is a collection leads to mutable records, and I think that's something we don't want.

Do people generally ignore this aspect, or does one try hard to make the record truly immutable?

If the latter, what would one do?

The naive approach would be record Foo(List<String> bar) { }, and I guess people can do foo.bar().add(...) and similar things to mutate this “immutable” record.

The record constructor could clone the list, then make it unmodifiable, would that be sufficient?

record Foo(List<String> bar) {
    Foo {
        bar = new ArrayList<>(bar);
        bar = Collections.unmodifiableList(bar);
    }
}

I'm not sure if this leads to some other way where foo.bar() ends up being mutable after all.

All of the above can only work because String is immutable, I understand that we need to make sure that a record is only constructed of immutable things.

2

There are 2 answers

0
Chaosfire On

As far as the state of the list is concerned, this is enough for the record to be immutable. A test that proves it:

public class ImmutabilityTests {

  private List<String> initialList;
  private Foo foo;
  private List<String> expectedList;

  @BeforeEach
  void setUp() {
    this.initialList = new ArrayList<>(Arrays.asList("a", "b", "c"));
    this.foo = new Foo(this.initialList);
    this.expectedList = new ArrayList<>(this.initialList);
  }

  @Test
  public void testRecordListEqualsInitialList() {
    assertEquals(this.expectedList, this.foo.bar(), "Record list does not equal initial list");
  }

  @Test
  public void testChangingInitialListDoesNotAffectRecordList() {
    this.initialList.clear();
    assertEquals(this.expectedList, this.foo.bar(), "Changing initial list should not affect record list");
  }

  @Test
  public void testChangingRecordListThrowsException() {
    assertThrows(UnsupportedOperationException.class, () -> this.foo.bar().clear(), "Record list was not immutable");
  }
}

The list in Foo is equal to the initial list, changing initial list does not change list in record, and attempting to change list in Foo throws exception.

Additionally you can simplify the record constructor, using List.copyOf():

Returns an unmodifiable List containing the elements of the given Collection, in its iteration order. The given Collection must not be null, and it must not contain any null elements. If the given Collection is subsequently modified, the returned List will not reflect such modifications.

public record Foo(List<String> bar) {
  public Foo {
    bar = List.copyOf(bar);
  }
}
2
Michael Gantman On

Method Collections.unmodifiableList() is your answer. it creates an immutable list regardless of what are the contents. Even if the instances that you store in the list are of a mutable class. One more trick before you pass your list as a parameter do this:

public final ArrayList<MyClass> constantList = Collections.unmodifiableList(origList);

This will protect you from someone assigning new list instance to your constantList