Meaning of "shallowly immutable" in the documentation of Record in Java 14

Shallowly immutable means, that if a class has fields, these fields are treated as being final. However, their fields (i.e. the fields of the fields) don't need to be final.

You don't need to implement a constructor, it's already implemented this way for you. But if you choose to implement it yourself, e.g. for argument validation, then this invariant should hold.


If you consider a class as a composite or hierarchy of other classes and primitives (ints, arrays, etc.), shallow immutability refers to the immutability (constant-ness) of just the first level.

It is in contrast to the term 'deep immutability', which refers to the immutability of the whole hierarchy. Most of the tangible benefits that you hear about immutability, such as implicit thread-safety, apply only to something which is deeply immutable.

Consider this class

class Foo {
    private final MutableBar bar;

    //ctor, getter
}

This class is shallowly immutable. It cannot be changed directly, but can be changed indirectly, for example

foo.getBar().setSomeProperty(5);

so it is not deeply immutable.

Another example of shallow immutability, using only primitives

class Foo {
    private final int[] ints;

    Foo(int[] ints) {
        this.ints = ints;
    }
}

This can be mutated like so

int[] ints = {1};
Foo foo = new Foo(ints);
ints[0] = 2;

For a small hierarchy, it is sometimes simple to make a shallowly immutable class deeply immutable. It usually involves defensive copies, or switching mutable classes to immutable variations.

class Foo {
    private final int[] ints; 

    Foo(int[] ints) {
        // copy to protect against the kind of mutation shown above
        this.ints = Arrays.copyOf(ints, ints.length);
    }

    // if you must have a getter for an array, make sure not to return the array itself, 
    // otherwise the caller can change it.
    // for performance reasons, consider an immutable List instead - no copy required
    int[] getInts() {
        return Arrays.copyOf(ints, ints.length);
    }
}