Immutable Objects

tiny-tool.de

tiny-tool.de

Im modernen Software-Engineering nimmt die Unveränderlichkeit von Objekten, bekannt als Immutables, eine zentrale Rolle ein. Diese Praxis bietet diverse Vorteile, die insbesondere die Sicherheit und Wartbarkeit von Software-Anwendungen positiv beeinflussen können.

  • Zunächst gewährleisten Immutable Objects eine erhöhte Sicherheit und Einfachheit, da sie nach ihrer Initialisierung nicht mehr modifiziert werden können. Diese Eigenschaft schützt sie vor unerwünschten Veränderungen, die durch Nebeneffekte in Methoden oder simultanen Zugriffen in mehrthreadigen Umgebungen entstehen könnten. Ein konstanter Zustand der Objekte vereinfacht somit die Fehleranalyse und die Code-Pflege erheblich.
  • Des Weiteren sind Immutables von Natur aus thread-save, da sie nach ihrer Erstellung keinen Zustandsänderungen mehr unterliegen. Dies erleichtert die Datenhandhabung in mehrthreadigen Anwendungen, indem Synchronisationsprobleme vermieden werden.
  • Obwohl das Erstellen neuer Immutable Instanzen in manchen Kontexten aufwendiger sein kann als die Modifikation bestehender Objekte, führt deren Unveränderlichkeit in parallelen Verarbeitungsszenarien zu einer verbesserten Stabilität und Performance.
  • Die Förderung funktionaler Programmierparadigmen stellt einen weiteren Vorteil dar. Immutables unterstützen eine Entwicklungsmethodik, die auf Unveränderlichkeit und Nebeneffektfreiheit abzielt, was die Code-Qualität hinsichtlich Lesbarkeit und Wartbarkeit steigert.
  • Zudem sind Immutable Objekte besonders cache-freundlich, da ihr unveränderlicher Zustand eine zuverlässige und effiziente Zwischenspeicherung ermöglicht. Veränderungen durch andere Anwendungsteile, die potenziell den Cache invalidieren könnten, treten nicht auf.
  • Die Implementierung von verlässlichen equals()- und hashCode()-Methoden ist bereits vorhanden.

Trotz der aufgeführten Vorteile gibt es Situationen, in denen der Einsatz von Immutables nicht optimal ist, etwa bei der Verarbeitung sehr großer Datenmengen, die häufige Änderungen erfordern. In solchen Fällen ist eine sorgfältige Abwägung ratsam.

org.immutables

org.immutables bietet ein Annotation-Processing-Toolkit, das es Entwicklern ermöglicht, saubere und thread-sichere Immutable Objekte durch einfache Deklarationen zu generieren. Mit Annotationen wie @Value.Immutable können Entwickler Klassen definieren, deren Instanzen unveränderlich sind, ohne die Boilerplate-Code-Menge manuell schreiben zu müssen.

Warum org.immutables wählen?

  1. Weniger Boilerplate: Generiert automatisch alle nötigen Methoden wie equals(), hashCode() und toString() sowie Builder-Methoden für deine Immutable-Objekte.
  2. Flexibilität: Ermöglicht die Definition von abstrakten Methoden, die in der generierten Klasse implementiert werden, was eine klare Trennung von Definition und Implementierung fördert.
  3. Anpassbarkeit: Bietet umfangreiche Anpassungsmöglichkeiten, einschließlich der Nutzung von eigenen Methoden und Feldern, die in die generierte Klasse integriert werden können.
  4. Integration: Arbeitet nahtlos mit anderen Java-Tools und -Frameworks zusammen, unterstützt JSON-Serialisierung/Dekodierung und bietet Erweiterungen für die Zusammenarbeit mit gängigen Datenbanken.

Durch den Einsatz von org.immutables in Java-Projekten können Entwickler die Vorteile von Unveränderlichkeit nutzen, ohne den damit verbundenen manuellen Aufwand. Dies führt zu sichererem, wartungsfreundlicherem und klarerem Code.

siehe auch Immutables.org

Beispiel für ein Immutable mit org.immutables

package de.object.tests.immuables;

import org.immutables.value.Value;

import java.util.List;
import java.util.UUID;

@Value.Immutable
@ValueObject
public interface User {

    UUID getId();
    String getName();
    List getAliases();

}

Erzeugen eines Immutable-Objects

User user = ImmutableUser.builder()
        .id(UUID.randomUUID())
        .name("Max Mustermann")
        .aliases(List.of("Max", "Musti"))
        .build();

Das gezeigte Code-Snippet verwendet eine benutzerdefinierte Annotation namens @ValueObject, die speziell für die Steuerung der Erzeugung von unveränderlichen Objekten (Immutables) mittels der Bibliothek org.immutables verwendet wird. Diese Annotation ist darauf ausgelegt, auf Paket- oder Typ-Ebene (z.B. Klassen oder Interfaces) angewendet zu werden, um die Erzeugung von Immutable-Objekten durch org.immutables zu steuern und zu konfigurieren. Hier sind die Schlüsselaspekte dieser Konfiguration:

  • @Target({ElementType.PACKAGE, ElementType.TYPE}): Gibt an, dass diese Annotation auf ganze Pakete oder auf Typen (wie Klassen oder Interfaces) angewendet werden kann.
  • @Retention(RetentionPolicy.CLASS): Die Annotation wird bis zur Compile-Zeit im Bytecode erhalten bleiben, ist aber zur Laufzeit nicht mehr durch Reflection zugänglich.
  • @Value.Style: Definiert einen Satz von Stilregeln und Konventionen, die von org.immutables für die Generierung der Immutable-Objekte verwendet werden.
    • jdkOnly = true: Beschränkt die Verwendung auf JDK-eigene Klassen, was die Kompatibilität sicherstellt.
    • defaultAsDefault: Methoden, die als Standard markiert sind, definieren Standardwerte für Attribute in Immutable-Objekten.
    • allParameters = true: Konstruktoren von Immutable-Objekten werden erwartet, alle Attribute als Parameter zu haben.
    • depluralize = true: Versucht, Namen von Sammlungsmethoden zu depluralisieren, um benutzerfreundlichere Namen für Zugriffsmethoden zu generieren.
    • visibility = Value.Style.ImplementationVisibility.PUBLIC: Stellt die Sichtbarkeit der generierten Immutable-Implementierungen auf öffentlich ein.
    • defaults = @Value.Immutable(): Ermöglicht es, dass Klassen, die mit dieser Annotation markiert sind, standardmäßig als Immutable behandelt werden.
    • get = {"is*", "get*"}: Definiert die Namenskonventionen für Zugriffsmethoden, um entweder mit get oder is zu beginnen, was die Lesbarkeit und Konventionen im Java-Bean-Stil unterstützt.

Die Definition von @ValueObject dient also als eine mächtige Vorlage oder Konfiguration, die die Erstellung von Immutable-Objekten in einem Projekt vereinheitlicht und vereinfacht, indem sie festgelegte Konventionen und Einstellungen vorschreibt. Entwickler können diese Annotation verwenden, um die Erstellung und das Verhalten ihrer Immutable-Objekte auf eine konsistente Weise zu steuern, was zur Klarheit, Wartbarkeit und zur Einhaltung von Best Practices beiträgt.

Beispiel für eine benutzerdefinierte Annotation @ValueObject

package de.object.tests.immuables;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.immutables.value.Value;

@Target({ElementType.PACKAGE, ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
@Value.Style(
    jdkOnly = true,
    defaultAsDefault = true,
    allParameters = true,
    depluralize = true,
    visibility = Value.Style.ImplementationVisibility.PUBLIC,
    defaults = @Value.Immutable(),
    get = {"is*", "get*"})
public @interface ValueObject {

}

org.projectlombok

Die Project Lombok Bibliothek verfolgt das Ziel, überflüssigen Code zu eliminieren und die Produktivität von Entwicklern signifikant zu steigern.

Project Lombok erreicht dies durch eine Reihe von Annotationen, die in den Quellcode eingefügt werden können und bei der Kompilierung automatisch den sonst manuell zu schreibenden Code generieren. Dadurch werden gängige Muster wie Getter und Setter, Konstruktoren, Builder, toString(), equals() und hashCode() Methoden mit minimalen Code-Zeilen implementiert.

Highlights von Project Lombok

  1. @Data: Eine einzige Annotation, die eine Klasse mit Gettern, Settern, toString(), equals(), und hashCode() Methoden sowie einem All-Argumente-Konstruktor ausstattet.
  2. @Builder: Ermöglicht ein fließendes API-Design für das Erstellen von Objektinstanzen, was besonders nützlich ist, wenn Objekte viele Felder haben.
  3. @Slf4j: Fügt der Klasse ein SLF4J-Logger-Feld hinzu, wodurch das manuelle Erstellen von Logger-Instanzen überflüssig wird.
  4. @NonNull: Automatisiert Null-Checks für Methodenparameter, was hilft, die NullPointerExeption zu vermeiden.
  5. @Cleanup: Automatisiert das sichere Schließen von Ressourcen, um Speicherlecks zu vermeiden.

Warum Project Lombok?

Durch den Einsatz von Project Lombok können Entwickler den Fokus von sich wiederholendem Code weg und hin zur tatsächlichen Geschäftslogik verschieben. Dies führt nicht nur zu saubererem und lesbarem Code, sondern auch zu einer erheblichen Reduzierung von Entwicklungsaufwand und Fehleranfälligkeit.

Insgesamt bietet Project Lombok umfangreiche Möglichkeiten, die Java-Entwicklung zu vereinfachen und zu beschleunigen, indem es einen Großteil des sich wiederholenden Codes, der sonst manuell geschrieben werden müsste, automatisch generiert.

siehe auch Project Lombok

Beispiel für ein Immutable mit org.projectlombok

package de.object.tests.lombok;

import lombok.Builder;
import lombok.Value;

import java.util.List;
import java.util.UUID;

@Value
@Builder
public class User {
    UUID id;
    String name;
    List getAliases();
}

Erzeugen eines Immutable-Objects

User user = User.builder()
        .id(UUID.randomUUID())
        .name("Max Mustermann")
        .aliases(List.of("Max", "Musti"))
        .build();

java.lang.Record

Mit der Einführung von Java 14 hat die Java-Plattform eine signifikante Erweiterung erhalten: Records. Diese neue Sprachkonstruktion zielt darauf ab, eine effiziente und prägnante Möglichkeit zu bieten, unveränderliche Daten zu modellieren. Records sind eine Antwort auf die langjährige Notwendigkeit, Boilerplate-Code zu reduzieren und eine klarere, semantisch reichere Alternative zu herkömmlichen Klassen für die Darstellung von reinen Datencontainern zu schaffen.

Was macht Java Records besonders?

  1. Kompakte Syntax: Ein Record wird durch das Schlüsselwort record gefolgt von einem Namen und einer Parameterliste definiert. Diese Parameterliste definiert die Eigenschaften des Records. Hierdurch wird die Notwendigkeit eliminiert, Getter-Methoden, equals(), hashCode() und toString() manuell zu implementieren.
  2. Unveränderlichkeit: Instanzen von Records sind per Definition unveränderlich. Jedes Attribut eines Records ist final, was die Thread-Sicherheit und Vorhersehbarkeit des Codes erhöht.
  3. Datenzentriert: Records sind explizit dafür entworfen, Daten zu modellieren. Sie sind nicht dafür gedacht, komplexe Geschäftslogiken zu enthalten oder erweitert zu werden.
  4. Transparente Datenmodellierung: Durch die Verwendung von Records wird der Code, der zur Datenmodellierung verwendet wird, deutlich lesbarer und einfacher zu verstehen. Dies fördert die Wartbarkeit und das Verständnis des Codes.

Einsatzgebiete von Java Records

Records eignen sich hervorragend für die Modellierung von immutablen Datenstrukturen, wie sie häufig in modernen Anwendungen zum Einsatz kommen. Typische Anwendungsfälle sind:

  • Datenübertragungsobjekte (Data Transfer Objects, DTOs)
  • Wertobjekte in Domain-Driven Design (DDD)
  • Schnelle Prototypenerstellung von Datenmodellen

siehe auch Java Records

Beispiel für einen Java Record

package de.object.tests.records;

import java.util.List;
import java.util.UUID;

public record User(UUID id, String name, List aliases) {}

Mit nur einer Zeile Code haben wir ein voll funktionsfähiges, unveränderliches Datenmodell eine Users definiert, komplett mit Getter-Methoden, equals(), hashCode(), und toString().

Aber Achtung: Ist dieser Record wirklich „Immutable“? Auf den ersten Blick scheint der User-Record mit seinen finalen Feldern für id, name und aliases unveränderlich. Jedoch birgt die List aliases eine versteckte Falle – Listen in Java sind standardmäßig veränderbar. Das bedeutet, dass, obwohl der Record selbst nicht verändert werden kann (die Referenz auf die Liste bleibt gleich), der Inhalt der Liste aliases nach der Erstellung des Records modifiziert werden kann. Dies untergräbt die Unveränderlichkeit und damit die Thread-Sicherheit des Records, da externe Änderungen an der Liste zu unerwarteten Ergebnissen führen können.

Wie ist das zu verhindern?

Bei der Initialisierung des Records kann man die übergebene Liste in eine unveränderliche Liste umwandeln. Dies stellt sicher, dass die aliases-Liste im User-Record nicht von außen verändert werden kann. Java bietet hierfür die Methode List.copyOf() an, die eine unmodifizierbare Liste erstellt. Alternativ kann auch Collections.unmodifiableList() verwendet werden, um eine Ansicht der Liste zu erstellen, die nicht modifiziert werden kann.

Das folgende Beispiel zeigt, wie ein konstruktorspezifischer Ansatz in einem Record verwendet werden kann, um die Liste aliases unveränderlich zu machen:

package de.object.tests.records;

import java.util.Collections;
import java.util.List;
import java.util.UUID;

public record User(UUID id, String name, List aliases) {
    public User(UUID id, String name, List aliases) {
        this.id = id;
        this.name = name;
        this.aliases = List.copyOf(aliases);
    }

    public List aliases() {
        return Collections.unmodifiableList(aliases);
    }
}

Erzeugen eines Immutable-Objects

User user = new User(UUID.randomUUID(), "Max Mustermann", List.of("Max", "Musti"));

Fazit

Im Abschluss unserer Betrachtung von org.immutables, Project Lombok und Java Records stechen sowohl ihre Gemeinsamkeiten als auch ihre Unterschiede deutlich hervor. Alle drei Ansätze zielen darauf ab, die Java-Entwicklung effizienter zu gestalten, indem sie den Boilerplate-Code reduzieren und die Handhabung von Datenobjekten vereinfachen. Trotz dieser gemeinsamen Zielsetzung unterscheiden sie sich in ihrer Herangehensweise und Flexibilität sowie in den spezifischen Anwendungsfällen, für die sie jeweils am besten geeignet sind.

Gemeinsamkeiten

  • Effizienzsteigerung: Alle drei Technologien reduzieren den manuellen Schreibaufwand durch Automatisierung von Standardmethoden wie equals(), hashCode(), toString() und die Implementierung von Mustern wie dem Builder.
  • Förderung der Unveränderlichkeit: Sie unterstützen die Erstellung unveränderlicher (immutable) Objekte, was zur Thread-Sicherheit und Vorhersehbarkeit des Codes beiträgt.

Unterschiede

  • Flexibilität und Kontrolle: org.immutables und Project Lombok bieten mehr Flexibilität und Kontrolle über die generierten Implementierungen. Java Records sind hingegen in ihrer Struktur festgelegt und bieten weniger Anpassungsmöglichkeiten.
  • Einführung und Kompatibilität: Java Records sind ein Sprachfeature, das ab Java 14 verfügbar ist, während org.immutables und Project Lombok als externe Bibliotheken hinzugefügt werden müssen. Dies kann insbesondere bei der Einrichtung von Build-Prozessen und der Integration in bestehende Projekte eine Rolle spielen.
  • Anwendungsbereich: Project Lombok ist nicht speziell auf Unveränderlichkeit ausgerichtet und bietet eine breite Palette von Hilfsmitteln, die über die Erstellung von Datenobjekten hinausgehen. org.immutables und Java Records hingegen sind stark auf die Modellierung unveränderlicher Datenobjekte fokussiert.

Vorteile und Nachteile

  • org.immutables bietet eine umfangreiche und flexible Lösung zur Erstellung von immutablen Objekten, kann aber eine Einarbeitungszeit erfordern und erzeugt zusätzlichen Code zur Kompilierzeit.
  • Project Lombok reduziert Boilerplate-Code effektiv mit minimaler Konfiguration, kann jedoch in einigen Entwicklungsumgebungen Probleme verursachen, da es den Kompilierungsprozess modifiziert.
  • Java Records sind einfach zu verwenden und verstärken die Lesbarkeit und Wartbarkeit von Code durch ihre klare Struktur, bieten jedoch weniger Flexibilität im Vergleich zu den anderen beiden Lösungen.

Empfehlungen

  • Für Projekte, die Unveränderlichkeit und klare Datenmodellierung priorisieren, sind Java Records eine hervorragende Wahl, sofern sie in einer Umgebung ab Java 14 eingesetzt werden können.
  • Wenn Flexibilität und Kontrolle über die Generierung von Code erforderlich sind, bieten org.immutables und Project Lombok robuste Lösungen. org.immutables ist besonders geeignet, wenn die Unveränderlichkeit im Fokus steht, während Project Lombok eine breite Palette von Hilfsmitteln für verschiedene Aspekte der Java-Entwicklung bereitstellt.
  • In bestehenden Projekten, die eine schnelle Effizienzsteigerung ohne größere Änderungen benötigen, kann die Einführung von Project Lombok eine unkomplizierte Lösung bieten.

Letztlich sollte die Wahl der Technologie sowohl von den spezifischen Anforderungen des Projekts als auch von den Präferenzen des Entwicklungsteams abhängen. Es ist nicht unüblich, dass alle genannten Optionen in einem Projekt verwendet werden.