Immutables

“If you tell the truth you don’t have to remember anything.”

Mark Twain

In der objektorientierten Softwareentwicklung können Objekte in zwei unterschiedliche Kategorien eingruppiert werden. Entweder sie sind nicht veränderbar (immutable) oder veränderbar (mutable).

Diese Unterscheidung hat nicht nur eine philosophische Größe, sie zeigt auch Konsequenzen in der tagtäglichen Programmierung. Der Großteil der Klassen, mit dem der Entwickler in Berührung kommt sind Mutables. Dies entspricht auch der grundlegenden Idee der Objekt-Orientierten-Programmierung. Dort zeichnen sich Objekte durch einen internen Zustand und ein Verhalten aus. Bei der Interaktion zwischen zwei Objekten verändert sich der Zustand der Objekte und führt zu einem veränderten zukünftigen Verhalten. Bekannte Vertreter dieser Grupe sind die Java Beans.

Grundlagen

Häufig ist es aber doch angenehmer, wenn die eigenen Objekte sich nicht verändern können. Wenn fremde Bibliotheken ins Spiel kommen oder der eigene Quellcode zum undurchschaubaren Dickicht gewuchert ist, kann es passieren, dass naive Annahmen zu unverhofften Seiteneffekten führen. Der prominenteste Vertreter der Immutables in Java ist die Klasse String.

Neben der grundsätzlichen Sicherheit, dass niemand das eigene Objekt verändern kann, haben Immutables weitere hieraus resultierende Vorteile.

Immutables sind Thread-safe. Da niemand eine Instanz dieser Klasse verändern kann, sind alle Zugriffe auf diese Klasse sicher.

Keine Kopiermechanismen nötig. Wenn sich der Inhalt einer Instanz nicht ändern kann, muss sie niemals kopiert werden. Es kann immer mit der Referenz auf sie gearbeitet werden. Damit entfallen Kopiermethoden, ein Copy-Constructor und die clone Methode.

Interning möglich bei gleichen Instanzen. Ohne die Möglichkeit, dass sich der interne Zustand einer Instanz ändert, sind verschiedene Instanzen mit dem gleichen Zustand austauschbar. Aus dieser Äquivalenz folgt, dass alle Instanzen durch eine einzige ersetzt werden können. Dies nutzen u.a. die Number-Klassen in Java. Alle Werte zwischen -127 und 127 werden bei der Nutzung der valueOf Methode aus einem internen Pool bezogen.

Lazy Hashcode Berechnung. Der Hashcode einer Instanz sollte sich auch bei Mutables nie ändern, weil man solche Instanzen in einer HashMap nicht so einfach widerfindet. Bei Immutables kann man den einmal berechneten Wert aber unbesorgt zwischenspeichern, weil er sie niemals ändern wird.

Keine Setter notwendig. Wenn kein Wert verändert werden kann, werden auch keine entsprechenden Setter Methoden benötigt. Alle Werte werden initial im Konstruktor gesetzt.

Implementierung

Wenn eigene Immutables erstellt werden sollen, ist Vorsicht geboten. Denn alles, was den Zustand der Instanz beschreibt, muss immutable sein. Das ist bei den primitiven Typen, den Basis Typen wie Number, String und Boolean zwar gegeben, aber schon die Collection Implementierungen sind ein Einfallsstor für Veränderungen.

class PseudoImmutable {
  private List<String> list;
  PseudoImmutable(List<String> list) {
    this.list = list;
  }
  List<String> getList() {
    return list;
  }
}

Die oben dargestellte Klasse ist in keiner Weise unveränderlich. Die übergebene Liste kann jederzeit manipuliert und damit der Zustand des umgebenen Objektes verändern.

List<String> list = new ArrayList<>("Jens");
PseudoImmutable pseudoImmutable = new PseudoImmutable(list);
pseudoImmutable.getList(); // Jens 
list.add("Alicia");
pseudoImmutable.getList();// Jens, Alicia 

Damit die Instanz unveränderlich wird, muss eine Kopie der Liste erstellt werden. Bei Listen die veränderliche Instanzen enthalten, müssen diese auch kopiert werden.

class PseudoImmutable {
  private List<String> list;
  PseudoImmutable(List<String> list) {
    this.list = List.copyOf(list);
  }
  List<String> getList() {
    return list;
  }
}

Im Konstruktor wird die Liste mit List.copyOf kopiert und eine neue, nicht veränderbare Liste erzeugt. Wird die ursprüngliche Liste verändert, dann ändert sich diese Kopie nicht. Da die Liste nicht veränderbare String Instanzen enthält, reicht eine einfache Kopie. Enthält die Liste veränderbare Objekte, dann müssen auch diese kopiert oder vor Veränderung geschützt werden.

Die Klasse ist noch immer nicht ganz unveränderbar, denn sie ist nicht vor Vererbung geschützt. Die Methode getList kann überschrieben werden und damit das Ergebnis wieder in eine veränderbare Liste umgewandelt werden. Die Methode oder die gesamte Klasse muss noch als final deklariert werden.

Wer sich die ganze Arbeit nicht machen will, kann auch ein wenig faulen Zauber benutzen und sich die immutable Klasse generieren lassen. Das Projekt Immutables stellt einen prakischen Framework dafür bereit.

@Value.Immutable
interface AncestorTree {
  Person getRoot();
}

Aus einer Typ Definition und einigen Annotation werden passende unveränderbare Implementierungen erzeugt.

@Generated({"Immutables.generator", "AncestorTree"})
public final class ImmutableAncestorTree extends AncestorTree{
  private final Person root;

    private ImmutableAncestorTree(Person root) {
        this.root = root;
    }
 
    @Override
    public Person getRoot() {
        return root;
    }
 ...
 }

Es gibt aber auch hier traditonelle Mittel um unveränderliche Implementierungen zu verwenden. So stellt die Java Standard Bibliothek Wrapper Klassen für die Collection Klassen bereit.

List list = new ArrayList<>("Jens", "Hans", "Hermann");
List unmodifiableList = Collections.unmodifiableList(list);
list = null;

Im Prinzip sind es Guard Decorator, die jeden schreibenden Zugriff untersagen. Das im obigen Beispiel die ursprungliche list Variable auf null gesetzt wird, hat einen gewichtigen Grund. Solange eine Referenz auf die Ursprungsliste existiert, kann diese weiter manipuliert werden. Nur wenn alle Zugriffe über unmodifiableList stattfinden, ist die Liste vor Änderungen sicher.

Für eigene Klassen kann man den Guard Decorator Ansatz natürlich auch verwenden.

public interface Tree {
    Node getRoot();
    void add(Node node);
    
    static Tree unmodifiable(Tree tree) {
        return new UnmodifiableTree(tree);
    }

    final class UnmodifiableTree implements Tree {
      private final Tree wrapped;
    
      public UnmodifiableTree(Tree tree) {
        this.wrapped = tree;
      }
    
      @Override
      public Node getRoot() {
        return wrapped.getRoot();
      }
    
      @Override
      public void add(Node node) {
        throw new UnsupportedOperationException();
      }
    }
}

Das bestehende Interface Tree wurde in diesem Fall um eine UnmodifiableTree Klasse ergänzt und eine Default-Methoden zu Erzeugung der unveränderbaren Instanzen ergänzt.

Tree immutable = Tree.unmodifiable(createTreeById(42));

Die Tree Instanz aus der Methode createTreeById wird direkt an die Methode unmodifiable übergegeben und in einen nicht veränderbaren Wrapper eingefügt.

Fazit

Nicht veränderbare Klassen sind eine wichtiger Faktor um eigene Software sauberer und sicherer zu implementieren. Ihre Erstellung ist einfach und kann durch Tools wie Immutables unterstützt werden.