Suche
Kolumne

Aus der Java-Trickkiste: Code ohne Seiteneffekte

Arno Haase

Funktionale Programmierung im üblichen Sinne hat zwei zentrale Merkmale: Funktionen und Seiteneffektfreiheit. Funktionen unterstützt Java seit der Einführung von Lambdas, aber das Programmieren ohne Seiteneffekte hat irgendwie den Sprung in die Java-Kultur noch nicht so recht geschafft. Der Artikel stellt dieses Paradigma vor und macht – hoffentlich – Lust darauf.

Unveränderliche Objekte

Von Seiteneffekten spricht man, wenn ein Stück Code den Zustand verändert oder auf ein Ausgabegerät schreibt, also zum Beispiel ein Attribut auf einen anderen Wert setzt oder System.out.println() aufruft. Puristen verwenden auch die Begriffe „Wirkung“ oder „Nebeneffekt“, aber ich verwende hier in Anlehnung an den englischen Begriff „Side Effect“ das weit verbreitete „Seiteneffekt“. In Java geht seiteneffektfreie Programmierung Hand in Hand mit Klassen, bei denen alle (beobachtbaren) Attribute final sind. Wo nur solche Klassen zum Einsatz kommen, kann Code ja gar keinen Zustand verändern.

Nehmen wir als Beispiel an, wir wollen eine Anwendung zur Planung von Wanderrouten bauen. Wir fangen mit einer Klasse GeoLocation an, die eine geografische Position repräsentiert (Listing 1).

public class GeoLocation {
  // Vereinfachende Berechnungen: Erde wird als Kugel behandelt,
  // Abflachung an den Polen vernachlässigt
  public static final double ERDUMFANG_KM = 40_000;

  private final double latitude;
  private final double longitude;

  public GeoLocation (double latitude, double longitude) {
    if (latitude > 90 || latitude < -90) {
      throw new IllegalArgumentException ();
    }

    longitude = longitude % 360;
    if (longitude < 0) longitude += 360;     if (longitude > 180) longitude -= 360;

    this.latitude = latitude;
    this.longitude = longitude;
  }

  public double getLatitude () {
    return latitude;
  }
  public double getLongitude () {
    return longitude;
  }

  public GeoLocation moveEast (double km) {
    final double atEquator = km / ERDUMFANG_KM * 360;
    final double latFactor = Math.cos (latitude * Math.PI / 180);
    return new GeoLocation (latitude, longitude + atEquator / latFactor);
  }

  public GeoLocation moveNorth (double km) {
    final double degrees = km / ERDUMFANG_KM * 360;
    return new GeoLocation (latitude + degrees, longitude);
  }

  ... // weitere nützliche Methoden
}

Sie hat Felder für die geografische Breite und Länge und beide sind final. Der Konstruktor initialisiert diese beiden Felder und führt vorher noch eine Bereichsüberprüfung für die Breite und eine Normalisierung der Länge durch, sodass sie im Bereich von -180 bis 180 liegt. Ein GeoLocation-Objekt, das einmal initialisiert ist, kann sich nie mehr ändern. Man kann es aber stattdessen als Basis für das Erzeugen neuer GeoLocations benutzen. Dazu gibt es zum Beispiel die Methode moveEast(), die die Koordinaten eines Punktes berechnet, der in einer bestimmten Distanz östlich oder westlich des Ausgangspunktes liegt und sie als neue GeoLocation zurückgibt. Ein Hinweis für Aufmerksame: Die Berechnung ist für die Praxis untauglich, weil sie die Erde als Kugel behandelt und die Abflachung an den Polen vernachlässigt. Ich habe die mathematisch korrekte Modellierung der Übersichtlichkeit des Codebeispiels geopfert. Analog dazu erzeugt die Methode moveNorth() eine neue GeoLocation, die eine bestimmte Distanz nördlich oder südlich des Ausgangspunktes legt. In einer echten Anwendung wären hier wahrscheinlich zahlreiche weitere Methoden zum Berechnen von Entfernungen, Bewegen in beliebige Richtungen etc. verfügbar.

Das zentrale Merkmal ist dabei, dass eine GeoLocation niemals ihre Koordinaten ändert, sondern bei Bedarf neue Instanzen erzeugt werden. So kann man sie guten Gewissens in einem ganzen System herumreichen, ohne dass man aufpassen müsste, ob sie irgendwo modifiziert werden.

Zusammengesetzte Klassen

Für „kleine“ Klassen wie GeoLocation ist es (inzwischen) recht weit verbreitet, sie komplett final zu implementieren. Sie werden oft als Value Types o. ä. bezeichnet, weil sie sich anfühlen wie die im JDK mitgelieferten, quasi primitiven Typen wie String oder Instant.

Man kann aber auch größere, zusammengesetzte Klassen unveränderlich schreiben. Listing 2 zeigt z. B. die Implementierung einer Route als benannte Liste von GeoLocations.

public class Route {
  private final String name;
  private final AList locations;

  public Route (String name, Iterable locations) {
    this (name, AList.create (locations));
  }
  public Route (String name, AList locations) {
    this.name = name;
    this.locations = locations;
  }

  public String getName () {
    return name;
  }
  public AList getLocations () {
    return locations;
  }

  public Route rename (String newName) {
    return new Route (newName, locations);
  }

  public Route removeWaypoint (GeoLocation location) {
    return new Route (name, locations.filter (loc -> loc != location));
  }
  public Route addWaypoint (GeoLocation pointBefore, GeoLocation location) {
    final List newLocations = 
        new ArrayList<> (locations.asJavaUtilList ());
    newLocations.add (newLocations.indexOf (pointBefore), location);
    return new Route (name, AList.create (newLocations));
  }
}

Der Aufbau der Klasse ist analog zu dem von GeoLocation: Ihr Zustand liegt in final-Feldern, die im Konstruktor initialisiert werden und über Getter exportiert sind. Und es gibt auch wieder „modifizierende“ Methoden, die jeweils neue Instanzen erzeugen. Es gibt aber zwei Unterschiede.

Erstens sind der Routenname und die Liste der Wegpunkte unabhängig voneinander. Deshalb gibt es jetzt Methoden, die nur einzelne Attribute ändern, z. B. rename() zum Ändern des Namens. Das ist technisch nicht kompliziert, aber es kann eine psychologische Hürde sein. Oft sind wir es gewohnt, zusammengesetzte Datentypen als „richtige“ Beans mit Gettern und Settern zu implementieren, und viele Frameworks und Bibliotheken bauen auf diesem Stil auf (z. B. JPA, JavaFX und DOM, um einige weit verbreitete zu nennen).

Collections

Zweitens hat eine Route eine Liste von Wegpunkten. Und im JDK haben alle Collections Methoden wie add() und remove(), die Elemente hinzufügen oder entfernen. Abgesehen von der tiefsitzenden Gewohnheit, dass alle Collections veränderlich sind, bedeutet das, dass man für unveränderliche Collection-Klassen eine Third-Party-Bibliothek braucht.

Der Code in Listing 2 verwendet dafür die Klasse AList aus der Bibliothek a-foundation, die eine Portierung der wichtigsten Scala-Collection-Klassen nach Java enthält. Diese Collection-Klassen haben Methoden zum Hinzufügen und Entfernen von Elementen, die aber das alte Collection-Objekt unverändert lassen und stattdessen eine modifizierte Kopie zurückliefern. Dieses Verhalten trägt den etwas unglücklichen Namen „persistente Collection“. Andere Bibliotheken mit unveränderlichen Collection-Klassen, wie z. B. Guava von Google, frieren den Zustand komplett ein und bieten keine verändernden Methoden.

Zurück zum Code: Die Methode removeWaypoint() ruft die Methode AList.filter() auf und erzeugt eine neue Liste, die alle Wegpunkte außer dem zu entfernenden enthält. Diese neue Liste verbindet sie dann mit dem Routennamen zu einem neuen Route-Objekt. Die Methode addWaypoint() fügt einen neuen Wegpunkt ein, wobei die Position durch den davorliegenden Wegpunkt festgelegt wird; null als Vorgänger bedeutet, dass der neue Wegpunkt am Anfang der Liste eingefügt werden soll. Dazu kopiert sie die Liste in eine reguläre ArrayList, fügt den Wegpunkt dort ein und konvertiert sie anschließend wieder in eine AList. Man könnte das auch ohne Verwendung einer ArrayList implementieren, aber der Code in Listing 2 ist weniger ungewohnt – und die nächste Kolumne behandelt unveränderliche Collections ausführlicher.

Vor- und Nachteile

Man kann also auch zusammengesetzte Klassen unveränderlich implementieren – aber wieso sollte man das tun?

Der wichtigste Vorteil ist Robustheit. Unveränderliche Objekte kann man bedenkenlos als Parameter an beliebigen Code übergeben oder als Rückgabewert liefern. Es besteht keine Gefahr, dass fremder Code unbeabsichtigt den internen Zustand irgendwelcher Objekte verändert, egal was er mit diesen Objekten anstellt. Das gilt auch und besonders in asynchronen, nachrichtenbasierten Architekturen, z. B. mit Akka. Dort kann man unveränderliche Objekte bedenkenlos asynchron an einen oder mehrere Empfänger versenden, die damit arbeiten – potenziell in anderen Threads und gleichzeitig. Dazu muss man hundertprozentig ausschließen, dass irgendjemand das Objekt verändert. Außerdem reicht es beim Testen und Debuggen von seiteneffektfreien Methoden, Parameter und Rückgabewerte zu überprüfen. Wenn diese zueinander passen, hat der Code korrekt gearbeitet und man muss nicht untersuchen, wo und in welcher Weise er unterwegs Objekte modifiziert hat. In diesem Sinne ist seiteneffektfreier Code expliziter: Man sieht einer Methodensignatur komplett an, was die Implementierung tut.

Der Hauptnachteil sind die Reibungsverluste mit bestehenden Bibliotheken, insbesondere JPA. Das liefert Daten in veränderlichen Objekten und speichert alle Änderungen, die man an diesen Objekten vornimmt. Man kann die Verarbeitungslogik auf unveränderliche Geschäftsobjekte aufbauen, und manchmal lohnt sich das auch – für die Persistenz muss man dann die Daten aber in JPA-konforme Klassen umkopieren.

Und was ist mit der Performance? Seiteneffektfreier Code erzeugt doch viele temporäre Objekte und kopiert eine Menge? Dieser Overhead ist selbstverständlich vorhanden, ist aber deutlich geringer als man auf den ersten Blick meinen würde. Persistente Collections sind intern sehr effizient implementiert und kopieren nur einen kleinen Teil der Daten, und Hotspot entschärft durch Optimierungen wie Escape Analysis oder G1 als Garbage Collector das Erzeugen temporärer Objekte.

Ob der Overhead im konkreten Fall wichtig oder auch nur messbar ist, muss man im Einzelfall überprüfen. Bei typischen Geschäftsanwendungen geht er oft im Rauschen unter.

Fazit

Seiteneffektfreier Code in Kombination mit unveränderlichen Klassen ist in Java möglich. Das gilt auch für größere, zusammengesetzte Klassen und Anwendungslogik. Sie fristen in der Java-Kultur auch in Zeiten von Java 8 eher ein Schattendasein – zu Unrecht, wie ich meine.

Im nächsten Monat kommt ein detaillierterer Blick auf seiteneffektfreien Code im Größeren und insbesondere auf persistente Collections.

Aufmacherbild: Html Code von Shutterstock / Urheberrecht: adempercem

Geschrieben von
Arno Haase
Arno Haase
Arno Haase ist freiberuflicher Softwareentwickler. Er programmiert Java aus Leidenschaft, arbeitet aber auch als Architekt, Coach und Berater. Seine Schwerpunkte sind modellgetriebene Softwareentwicklung, Persistenzlösungen mit oder ohne relationaler Datenbank und nebenläufige und verteilte Systeme. Arno spricht regelmäßig auf Konferenzen und ist Autor von Fachartikeln und Büchern. Er lebt mit seiner Frau und seinen drei Kindern in Braunschweig.
Kommentare
  1. Manuel2015-08-31 09:57:43

    An der Methodensignatur kann ich doch nicht sehen, ob der Code Seiteneffektfrei ist.
    Java selbst macht da doch keine Zusicherungen. Ich muss schon den Code selbst überprüfen oder dem Entwickler vertrauen, wenn er behauptet, keine Seiteneffekte zu erzeugen.

    Gleiches gilt für Immutable Klassen. Alleine auf das "final" zu schauen reicht nicht aus, da sich das final ja nur auf die Referenz bezieht und nicht auf den Inhalt. Im Artikel wird das ja schön am Beispiel der ArrayList vs. AList gezeigt. Wenn ich "AList" aber nicht kenne, muss ich erst nachschauen, ob es sich dabei um eine unveränderliche Implementierung handelt oder nicht.

    Allgemein stimme ich der Aussage des Artikels natürlich zu: Immutable Klassen und Seiteeffektfreie Funktionen sind toll, auch in Java nur bietet Java hier viel zu wenig Unterstützung um es wirklich verlässlich zu machen. Java ist eben, trotz Lambdas, noch lange keine funktionale Programmiersprache.

  2. Amin Kharchi2015-09-03 17:09:44

    Java fehlt leider die Const-Correctness von C++, wo man tatsächlich schon an der Signatur erkennen könnte, ob es Seiteneffekte gibt oder nicht.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.