Immutable Classes mittels Annotations generieren

Funktionale Java-Entwicklung: Unveränderliche Klassen mit der Bibliothek Immutables

Manuel Mauky

© Shutterstock / Abdoabdalla

In der funktionalen Programmierung sind unveränderliche Daten ein unverzichtbares Werkzeug. Und auch Java hat in der Vergangenheit das eine oder andere funktionale Konzept adaptiert. Eine native Unterstützung für Unveränderlichkeit fehlt jedoch nach wie vor. Mit Hilfe geeigneter Drittbibliotheken lässt sich aber einiges erreichen. In diesem Artikel wollen wir daher die Generierung von unveränderlichen Klassen mit Hilfe der Bibliothek Immutables vorstellen.

Unter unveränderlichen Daten versteht man Datenstrukturen, die nach dem initialen Anlegen nicht mehr verändert werden können. Einmal erzeugt, können nur noch Kopien abgeleitet, aber keine Daten mehr „inplace“ verändert werden. Dies hat verschiedene Vor- und Nachteile. In funktionalen Sprachen wird diese Eigenschaft u. a. dazu genutzt, ungewollte Seiteneffekte zu verhindern, denn eine Veränderung eines Werts außerhalb der Funktion wäre ein solcher Seiteneffekt. In Kombination mit einem funktionalen Programmierstil können unveränderliche Datenstrukturen die Lesbarkeit, Verständlichkeit und Testbarkeit von Code erhöhen, indem die Menge der möglichen Einflussfaktoren begrenzt wird. Unveränderbare Objekte können beispielsweise ohne Bedenken an fremde Methoden als Parameter übergeben werden, ohne dass man Angst haben muss, dass der Fremdcode das eigene Objekt unvorhergesehen manipuliert. Auch beim Thema Multithreading können unveränderliche Daten eine Erleichterung sein und viele Fehlerquellen von vornherein ausschließen.

Nachteilig kann sich das in manchen Situationen auf die Performance auswirken, da eben häufiger Kopien der Daten erzeugt werden müssen. Auch hierfür gibt es geeignete Hilfsmittel und Techniken zur Vermeidung von Problemen, die aber sicherlich nicht in jedem Anwendungsgebiet ausreichend sind. Letztlich ist also wie bei fast allen technischen Themen eine Abwägung nötig.

Hat man sich dann einmal für die funktionale Programmierung und Immutability entschieden, stellt sich die Frage der konkreten Umsetzung. Java ist keine funktionale Sprache, bringt in der Standardbibliothek aber dennoch bereits ein paar unveränderliche Klassen mit, beispielsweise die Klassen aus dem neuen Date and Time API. Im Allgemeinen ist die Unterstützung für diesen Programmierstil in Java aber begrenzt.

Zwar können in Java Variablen als final markiert werden, das wirkt sich aber nur auf die Referenz selbst aus, sodass diese nicht mehr neu belegt werden kann. Über die Veränderbarkeit der Objekte selbst, auf die eine Referenz zeigt, sagt das final-Schlüsselwort nichts aus. Nur bei flachen Datentypen wie int, double oder boolean sorgt das Schlüsselwort effektiv für die Unveränderlichkeit des Werts.

Um unveränderliche Datenstrukturen nutzen zu können, müssen wir diese also entweder selbst implementieren oder aus entsprechenden Drittbibliotheken beziehen. Beispielsweise bringt die Bibliothek vavr neben zahlreichen nützlichen funktionalen Helferlein auch einen Satz von unveränderlichen Collections mit. Diese gibt es in verschiedenen Ausprägungen von Listen über Queues hin zu Sets und Maps mit unterschiedlichen Eigenschaften und Performancecharakteristiken.

Allerdings sind Collections allein noch nicht wirklich zufriedenstellend. Wir möchten auch in der Lage sein, beliebige Datenstrukturen zu definieren. Wir bräuchten also ein unveränderliches Pendant zu Plain Old Java Objects (POJOs), und prinzipiell ist es auch nicht schwer, solche Klassen selbst zu programmieren. Nimmt man ein klassisches POJO als Ausgangspunkt, also eine Java-Klasse mit Feldern, die per Getter und Setter verfügbar gemacht wurden, dann genügen einige wenige Schritte, um die Klasse unveränderlich zu machen.

Zunächst entfernt man die Setter und legt stattdessen einen Konstruktor für sämtliche Felder an. Die Felder können anschließend als final markiert werden. Um veränderbare Instanzen ausschließen zu können, muss auch die Klasse selbst als final markiert werden, wodurch abgeleitete Klassen verhindert werden. Das ist notwendig, da andernfalls in der abgeleiteten Klasse wiederum veränderlicher Zustand eingeführt werden könnte. Somit könnte man für eine Instanz der Elternklasse nicht mehr zweifelsfrei sicherstellen, dass sie unveränderlich ist. Außerdem muss man sich bei den Typen aller Felder auf flache Datentypen oder ebenfalls unveränderliche Typen beschränken. Zum Schluss implementiert man noch die equals– und hashCode-Methoden, sodass zwei Instanzen mit gleichen Inhalten auch als identisch erkannt werden.

Prinzipiell genügt eine solche Implementierung bereits, jedoch ist die Arbeit damit nicht gerade angenehm. Der Grund dafür ist, dass wir es für die Umsetzung unserer eigentlichen Anforderungen in der Regel eben doch mit veränderbaren Dingen zu tun haben. Aus funktionaler Perspektive handelt es sich bei geänderten Daten lediglich um eine neue Version der entsprechenden Entität. Die alte Version bleibt unverändert, jedoch wird eine Kopie erzeugt, die den neuen Zustand repräsentiert. Diese Denkweise mag zunächst ungewohnt erscheinen, ist jedoch auch ungemein praktisch. Funktionale Sprachen bieten geeignete Sprachmittel zum einfachen Kopieren der Objekte. In Java müssen wir das jedoch wiederum selbst implementieren.

Ein übliches Pattern dazu sind sogenannte Wither-Methoden als Pendant zu Settern. Beispielsweise könnte eine unveränderliche Person-Klasse eine Methode withLastname enthalten. Genau wie ein Setter nimmt die Methode den zukünftigen Wert als Parameter entgegen. Anstatt aber das existierende Objekt zu verändern, wird stattdessen eine Kopie erzeugt, die sämtliche Werte des alten Objekts übernimmt und nur für das eine Feld (in unserem Beispiel der Nachname) einen neuen Wert vergibt. Listing 1 zeigt eine beispielhafte Implementierung.

public final class Person {
  private final String firstname;
  private final String lastname;

  public Person(String firstname, String lastname) {
    this.firstname = firstname;
    this.lastname = lastname;
  }

  public Person withLastname(String newLastname) {
    return new Person(this.firstname, newLastname);
  }

  // ...
}

Diese Wither-Methoden können noch optimiert werden: Wie oben bereits beschrieben, sollten die Klassen mit vernünftigen Equals-Methoden ausgestattet werden, die auch alle enthaltenen Felder einschließen. Da unveränderliche Klassen keinen Lebenszyklus besitzen, spielt der Zeitpunkt für die Prüfung auf Gleichheit keine Rolle. Zwei Instanzen, die einmal als gleich erkannt wurden, werden auch zu allen späteren Zeitpunkten gleich sein. Das ist bei normalen Objekten nicht der Fall, da sich die Felder der Objekte dort in der Zwischenzeit hätten ändern können. Da wir das für unsere Klasse aber ausschließen können, müssen wir in der Wither-Methode nur dann eine Kopie erzeugen, wenn sich der neue Wert vom bisherigen unterscheidet. Wir können also eine entsprechende Prüfung einbauen und in dem Fall direkt this zurückgeben. Für den konkreten Anwendungsfall der Person-Klasse sei noch angemerkt, dass in der Realität zwei Personen mit gleichem Vor- und Nachnamen natürlich nicht ein und dieselbe Person sind. Das spielt für die Betrachtung der Prüfung auf Gleichheit aber keine Rolle, sondern zeigt nur, dass für die korrekte Modellierung der Realität noch weitere Attribute notwendig wären, beispielsweise ein tatsächlich eindeutiger (eventuell rein technischer) Identifier.

Auf den ersten Blick sind Wither-Methoden nicht besonders kompliziert. Allerdings steigt der Aufwand schnell an, wenn neue Felder hinzugefügt werden sollen. Nicht nur müssen für jedes neue Feld ein Getter und Wither ergänzt, auch alle bisherigen Wither müssen erneut angefasst werden, da ja der neue Konstruktor dann nicht mehr zum bisherigen Aufruf passt. Wirklich praxistauglich ist dieses Vorgehen daher nicht.

Ein Ansatz zur Lösung des Problems sind Codegeneratoren, wie sie beispielsweise die Bibliothek Immutables liefert.

Die Idee ist dabei folgende: Als Entwickler/in legt man lediglich ein Interface an, das die gewünschten Getter enthält. Dieses Interface stattet man mit einigen Annotationen aus und den Rest erledigt die Bibliothek. Diese erzeugt daraus eine Klasse, die das Interface implementiert und zusätzlich für alle Felder die entsprechenden Wither-Methoden, eine sinnvolle toString-Implementierung sowie equals und hashCode generiert. In Listing 2 ist beispielhaft unser Person-Interface mit der @Value.Immutable-Annotation zu sehen.

import org.immutables.value.Value;

@Value.Immutable
public interface Person {
  String getFirstname();
  String getLastname();
}

Die Codegenerierung selbst basiert auf dem Java Annotation Processing API. Durch die Nutzung dieses Standards ist die Integration in die üblichen Build-Werkzeuge vergleichsweise einfach möglich. In einem Maven- oder Gradle-Projekt reicht es beispielsweise, die Bibliothek in die Liste der Abhängigkeiten aufzunehmen. Der Scope kann dabei auf provided (bei Maven) bzw. compileOnly (bei Gradle) gestellt werden, da die Bibliothek nur zum Compile-Zeitpunkt benötigt wird und nicht zur Laufzeit.

Bei der Nutzung von IntelliJ als Entwicklungsumgebung muss der Annotation Processor in den Einstellungen aktiviert werden. Zum Ausführen der Codegenerierung genügt dann ein Klick auf Build Project und einen Moment später sind die generierten Dateien für die Verwendung bereit. Ähnliche Einstellungen existieren auch in Eclipse und anderen IDEs.

Im folgenden Beispiel ist zu sehen, wie die generierte Klasse ImmutablePerson benutzt werden kann. Dabei fällt auf, dass auch ein Builder generiert wurde, der für die Erzeugung von Objekten genutzt wird. Damit wird zunächst eine Instanz angelegt und später mit der withLastname-Methode eine Kopie erzeugt. Das ursprüngliche Objekt wird dabei nicht verändert, auch wenn wir hier im Beispiel die Referenz des ursprünglichen Objekts überschreiben:

ImmutablePerson luise = ImmutablePerson.builder()
                                       .firstname("Luise")
                                       .lastname("Müller")
                                       .build();

luise = luise.withLastname("Meier");

Codestyle

Die Bibliothek benutzt einen Standardstil bei der Generierung der Klassen. Beispielsweise wird standardmäßig die Bezeichnung des Interface genommen und das Präfix Immutable davor gesetzt, um den Klassennamen der zu generierenden Klasse zu erhalten.

Dieser Stil kann mittels der Annotation @Value.Style auch den eigenen Wünschen angepasst werden. Diese erlaubt eine recht umfangreiche Anpassung der Namensgebung – sowohl für die Klasse selbst als auch für die generierten Methoden. Beispielsweise können wir die Namensgebung für den Klassennamen umkehren, sodass das Interface ImmutablePerson lauten muss und die daraus generierte Klasse nur noch Person heißt. Dazu wird in der Style-Annotation das Attribut typeAbstract auf Immutable* gesetzt und das Attribut typeImmutable auf *. In Listing 3 sind diese und einige weitere Style-Änderungen zu sehen. Die Angabe init = set* führt dazu, dass die Methoden des Builders wie normale Setter aussehen. Das Attribut depluralization kommt bei Feldern zum Tragen, die einen Collection Type haben.

@Value.Style(
  typeAbstract = "Immutable*",
  typeImmutable = "*",
  init = "set*",
  depluralization = true
)
public interface ImmutablePerson {
...
}

Für die Person-Klasse wäre es beispielsweise vorstellbar, dass diese eine Liste von Spitznamen enthält. Immutables generiert für Collection-Felder automatisch Methoden zum direkten Hinzufügen und Entfernen von Elementen. Bei einer Methode List<String> getNicknames() im Interface würde also zusätzlich auch die Methode addNicknames zum Builder hinzugefügt werden. Mit dem depluralization-Attribut kann dies so konfiguriert werden, dass stattdessen die Einzahl im Namen benutzt wird. Die Methode würde dann also addNickname lauten. Ob einem dieses Detail wichtig ist oder nicht, muss aber jeder selbst entscheiden. Es soll an dieser Stelle nur zeigen, dass die Codegenerierung bei Bedarf stark angepasst werden kann.

Im Beispiel wurde die Annotation @Value.Style direkt auf das Interface gesetzt. In der Regel ist es aber praktischer, projektweit einen einheitlichen Stil festzulegen und dann wiederzuverwenden. Dazu können eigene Annotationen definiert und mit der gewünschten Style-Annotation versehen werden. Anschließend muss nur noch die eigene Style-Annotation ohne weitere Konfiguration verwendet werden. Sowohl selbstdefinierte Style-Annotationen als auch die vorgegebene Annotation können nicht nur an Klassen, sondern auch an Packages gesetzt werden. Dazu legt man eine package-info.java-Datei im jeweiligen Package an und setzt die Annotation an das darin beschriebene Package. Der Style bezieht sich dann auf alle in diesem Package enthaltenen Klassen sowie alle Unter-Packages.

Modifiable-Klassen

Neben Immutable-Klassen lassen sich auch Modifiable-Klassen generieren. Dies mag zunächst eigenartig klingen, da doch der eigentliche Zweck der Bibliothek die Vermeidung von Mutationen ist. Tatsächlich kann dieses Feature aber in manchen Fällen ungemein praktisch sein. Zunächst setzt man neben der @Value.Immutable-Annotation auch noch @Value.Modifiable. Als Ergebnis erhält man eine ModifiablePerson-Klasse, die einem normalen POJO recht ähnlich ist, also neben den Getter-Methoden auch Setter enthält, mit denen sich die Instanz selbst auch verändern lässt.

Interessant wird es im Zusammenspiel mit der unveränderlichen Version, denn eine Transformation ist in beide Richtungen möglich. Ein Anwendungsfall dafür wäre beispielsweise eine Funktion, die als Argument ein unveränderliches Objekt bekommt und als Ergebnis eine neue veränderte Version des Objekts liefern soll. Dafür können – wie oben beschrieben – die generierten Wither-Methoden genutzt werden. Mit den Modifiable-Klassen wäre es aber auch möglich, aus dem Parameter zunächst eine modifizierbare Version zu erzeugen, die Änderungen direkt im klassischen Setter-Stil durchzuführen und anschließend für das Ergebnis wieder in eine unveränderliche Kopie umzuwandeln. Damit kann lokal bei Bedarf ein imperativer Stil gewählt werden, während von außen betrachtet trotzdem die Korrektheit der funktionalen Programmierung gewahrt bleibt.

Ein anderer Anwendungsfall für dieses Feature ist die Interaktion mit anderen Bibliotheken und Frameworks, die die übliche Struktur aus Gettern und Settern erwarten. Listing 4 zeigt die beispielhafte Benutzung der modifizierbaren Klassen.

ImmutablePerson person = ...

ModifiablePerson modifiablePerson = ModifiablePerson.create().from(person);

modifiablePerson.setFirstname("Luise");
modifiablePerson.setLastname("Müller");

ImmutablePerson newPerson = modifiablePerson.toImmutable();

Konsistenz, optionale Felder und Defaultwerte

Ein wichtiges Ziel bei der Benutzung der Immutables-Bibliothek ist die Sicherstellung von konsistenten und validen Objekten. Bei veränderbaren Objekten kann es sinnvoll sein, zu verschiedenen Zeitpunkten die Gültigkeit der Objekte zu prüfen, da diese ja durch Modifikationen in einen gültigen oder ungültigen Zustand gebracht worden sein könnten. Bei unveränderlichen Objekten ist dies nicht der Fall. Ein einmal ungültig erzeugtes Objekt wird nie wieder valide. Daher führt der generierte Code verschiedene Prüfungen durch, die verhindern sollen, dass überhaupt ungültige Objekte erzeugt werden können. Beispielsweise prüft der generierte Builder beim Aufruf der abschließenden build-Methode, ob alle Felder einen Wert erhalten haben. Nicht belegte Felder oder null als Wert führen zu einer Exception. Dies gilt übrigens auch bei der Umwandlung eines Modifiable-Objekts in die entsprechende unveränderliche Variante.

Eine einfache Prüfung auf Vorhandensein von Werten reicht aber nicht immer aus, um wirklich konsistente Objekte zu erhalten. Die Immutables-Bibliothek erlaubt es daher, zusätzliche Prüfungen zu implementieren, die bei der Erzeugung des Objekts ausgeführt werden. Dazu wird einfach eine Methode zum Interface hinzugefügt, die das Objekt auf Gültigkeit prüft und im Fehlerfall eine Exception wirft. Die Methode muss anschließend mit der Annotation @Check versehen werden, wie in Listing 5 zu sehen ist. Hier wird geprüft, dass der Vorname nicht leer ist.

@Value.Immutable
public interface Person {
  String getFirstname();
  String getLastname();

  @Value.Check
  default void check() {
    if(getFirstname().trim().isEmpty()) {
      throw new IllegalStateException("Firstname may not be empty");
    }
  }
}

In manchen Fällen kann es aber durchaus gültig sein, wenn bestimmte Werte nicht vorhanden sind. Seit Java 8 steht hierfür die Optional-Klasse zur Verfügung, und auch die Immutables-Bibliothek bringt Unterstützung dafür mit. Liefert eine Getter-Methode als Rückgabewert ein Optional zurück, wird dieses durch den erzeugten Builder standardmäßig mit Optional.empty() initialisiert. Somit kann der Wert beim Erzeugen von Objekten weggelassen werden.

Des Weiteren ist es auch möglich, für bestimmte Felder Defaultwerte anzugeben. Hierzu wird eine Defaultimplementierung des Getters im Interface angelegt, die den Standartwert liefert. Außerdem muss auch hier eine spezielle Annotation vergeben werden. Damit verlangt der Builder nun auch hier nicht mehr zwingend einen Wert für dieses Feld. Listing 6 zeigt die Implementierung eines optionalen Feldes und eines Feldes mit Standardwert.

@Value.Immutable
public interface Person {
	//…

	@Value.Default
	default String getSpecies() {
		return "human";
	}

	Optional getNickname();
}

Fazit

Die Arbeit mit unveränderlichen Werten mag für viele Entwickler zunächst ungewohnt erscheinen. Wie so oft geht die Arbeit aber leichter von der Hand, wenn man ein wenig Übung hat. Besonders die andere Denkweise der funktionalen Programmierung, die Seiteneffekte und Mutationen zu vermeiden versucht, dürfte vor allem am Anfang schwierig sein. Leider macht es einem Java dabei nicht so angenehm wie echte funktionale Programmiersprachen wie zum Beispiel Haskell. Mit den richtigen Hilfsmitteln lassen sich aber auch in Java ganz brauchbare Ergebnisse erzielen. Die Bibliothek Immutables stellt dazu einen Codegenerator zur Verfügung, der auf der einen Seite relativ schnell einsatzbereit ist und ohne viel Konfiguration bereits gute Ergebnisse liefert. Auf der anderen Seite steckt die Bibliothek aber auch voller zusätzlicher Features, die für viele Problemstellungen interessante Lösungen bieten und eine tiefgreifende Anpassung an die eigenen Wünsche erlauben. Die hier gezeigten Funktionen stellen dabei nur einen kleinen Teil der Möglichkeiten dar. Beispielsweise unterstützt Immutables auch von Haus aus die Serialisierung und Konvertierung zu JSON mittels Jackson oder GSON. Damit eignet sich der Codegenerator auch für die Erzeugung von Message-Objekten, zum Beispiel in JAX-RS Services. Die ausführliche Dokumentation der Bibliothek ist ein weiterer Pluspunkt.

Ob die Verwendung von unveränderlichen Daten für das eigene Projekt in Frage kommt, hängt natürlich weiterhin von den jeweiligen Gegebenheiten und auch den Vorlieben der Projektbeteiligten ab. Aber wenn die Entscheidung auf einen funktionalen Stil fällt, sind Bibliotheken wie Immutables sicherlich eine Bereicherung.

Geschrieben von
Manuel Mauky
Manuel Mauky
Manuel Mauky arbeitet seit 2010 als Softwareentwickler bei der Saxonia Systems AG in Görlitz. Er ist vor allem im Frontend-Bereich aktiv, seit einiger Zeit vor allem mit JavaFX. Daneben interessieren ihn Themen wie Softwarearchitektur, funktionale Programmierung und Reactive Programming. Manuel ist Gründungsmitglied und Leiter der Görlitzer Java User Group. Twitter: @manuel_mauky
Kommentare

Hinterlasse einen Kommentar

3 Kommentare auf "Funktionale Java-Entwicklung: Unveränderliche Klassen mit der Bibliothek Immutables"

avatar
4000
  Subscribe  
Benachrichtige mich zu:
benneq
Gast

Ein Vergleich zu Lombok wäre schon angebracht. Es gibt sicherlich hier und da ein paar Vor- und Nachteile.

Fe Liks
Gast

Ein Vergleich zu Lombok würde mich auch interessieren. Abgesehen davon, dass Lombok auch nicht-immutable Klassen (POJOs) generieren kann, gibt es sicher noch mehr Unterschiede…

dennis
Gast

In Listing 6 ist vermutlich die Katze des Autors auf die Tastatur gesprungen: Optional getNickname();