Java Immutability Checker (JIC)

Abgeschlossene Projekte

Projektteam: Oliver Haase, Benjamin Stehle, Stefan Waldraff

Moderne Desktop-Computer, aber auch Laptop, Tablets und sogar Smartphones verfügen nicht mehr über nur einen Prozessor, sondern über mehrere Prozessoren bzw. Kerne. Diese Mehrkernsysteme sind in der Lage, mehrere Programme gleichzeitig oder auch ein Programm parallelisiert, d.h. in mehreren sogenannten nebenläufigen Threads, auszuführen. Die nebenläufige Ausführung eines Programms setzt jedoch voraus, das das Programm entsprechend geschrieben wurde, d.h. dass Nebenläufigkeit bereits im Programmcode berücksichtigt ist. Dazu muss der Programmierer zwei Dinge tun: Er muss (1) diejenigen Programmteile identifizieren, die nebenläufig, also parallel ausgeführt werden können, und er muss (2) die Interaktion dieser nebenläufigen Programmteile  untereinander programmieren, damit sie ihre Zwischenergebnisse untereinander auszutauschen können. Diese Interaktion findet üblicherweise in Form von wechselseitigem Schreiben auf und Lesen von gemeinsamen Speicherbereichen statt; in der heute üblichen objektorientierten Programmierung sind dies gemeinsam genutzte Objekte. Der konkurrierende Zugriff auf gemeinsame Objekte ist jedoch ein schwieriger und fehleranfälliger Aspekt der parallelen Programmierung. Wenn die Schreib- und Leseoperationen nicht korrekt synchronisiert werden, kann es zu sogenannten Änderungsanomalien kommen, die zu inkonsistenten, d.h. korrupten Daten führen können. Unter Synchronisation versteht man in diesem Zusammenhang, dass konkurrierende Zugriffe auf gemeinsame Daten nacheinander ausgeführt werden müssen, damit sie sich nicht ungewünscht gegenseitig überschreiben. Fehlende oder fehlerhafte Synchronisierung gehört zur häufigsten Fehlerquelle in der modernen Programmierung; darüber hinaus sind diese Fehler deshalb besonders schwerwiegend, weil sie meistens nur sporadisch auftreten, deshalb schwer reproduzierbar und kaum testbar sind.

Wegen dieser Problematik kommt bei der parallelen Programmierung den unveränderbaren Objekten (engl. immutable objects) eine besondere Rolle zu; dabei handelt es sich um Objekte, die einmal erzeugt ihren Zustand, d.h. ihre Daten, nicht mehr ändern können. Solche Objekte können beliebig zwischen nebenläufigen Programmteilen ausgetauscht werden, ohne dass es zu Änderungsanomalien kommen kann. Wenn ein Programmteil den Zustand eines solchen Objektes ändern möchte, muss es stattdessen ein neues Objekt mit geändertem Zustand erzeugen und weitergeben. 

In Java, der seit Jahren populärsten objektorientierten Programmiersprache, ist es erstaunlich schwierig, unveränderbare Objekte bzw. Klassen korrekt zu programmieren. Klassen sind sozusagen die Blaupausen von Objekten; sie geben an, welche Daten und welches Verhalten die Objekte der entsprechenden Klassen aufweisen. Die Regeln, die zum Entwurf unveränderbarer Klassen beachtet werden müssen, sind unter Programmierern nicht allgemein bekannt; weiterhin bietet die Sprache Java selbst keine und die häufig verwendeten Entwicklungsumgebungen nur sehr unzureichende Mechanismen, die den Programmierer bei dieser Aufgabe unterstützen würden.

Diese Lücke zu schließen ist das Ziel des Java-Immutability-Checker-Projekts. In diesem Projekt entwickeln wir ein Analysewerkzeug in Form eines Findbugs-Plugin, das Java-Klassen darauf überprüft, ob sie alle Regeln erfüllen, die von unveränderbaren Klassen gefordert sind. Für Findbugs haben wir uns entschieden, weil es einfach in die bekannten Java-Entwicklungsumgebungen Eclipse, IntelliJ und Netbeans integriert werden kann, und unser Analysewerkzeug damit Teil eines automatisierten Build-Prozesses werden kann. 

Die Eigenschaften, die jede unveränderbare Klassen erfüllen muss, sind die folgenden:   

  1. Alle Felder (Instanzvariablen) müssen final sein.
  2. Instanzen der Klasse müssen ordentlich erzeugt werden, d.h. die ‘this’-Referenz darf nicht während der Konstruktion nach außen gelangen.
  3. Die Zustände der Instanzen müssen unveränderbar sein. Diese Eigenschaft setzt sich aus den folgenden Teileigenschaften zusammen:
  •   Referenzen auf veränderbare Daten müssen privat sein.
  • Referenzen auf veränderbare Daten, die als Argumente in Konstruktoren eingehen, dürfen Feldern der ‘this’-Instanz nicht direkt zugewiesen werden, sondern müssen vorher tief kopiert werden.
  • Umgekehrt dürfen Referenzen auf veränderbare Daten nicht nach außen gelangen; stattdessen müssen defensive Kopien angelegt und publiziert werden.
  • Es darf keine Mutatoren, d.h. zustandsändernde Methoden geben.  

Das JIC-Findbugs-Plugin überprüft diese Eigenschaften für Klassen, die mit der @Immutable-Annotation des Pakets net.jcip.annotations  als unveränderbar markiert sind. Die 2. oben aufgelistete Eigenschaft zur ordentlichen Objekterzeugung wird auch für nicht markierte Klassen überprüft, weil jede korrekt implementierte Klasse nur vollständig erzeugte Objekte publizieren sollte.  

Die Überprüfung der Unveränderbarkeitsregeln findet nicht auf Ebene des Java-Quellcodes, sondern auf Ebene des zugehörigen Java-Bytecodes statt. Jede Java-Klasse wird vor ihrer Ausführung vom Java-Compiler in ein maschinennäheres Format, nämlich Java-Bytecode übersetzt. Dieser Bytecode ist für den Menschen wesentlich schwerer zu lesen, dafür ist aber der Sprachumfang geringer, was die automatische Analyse vereinfacht. Außerdem hat dieses Vorgehen den weiteren Vorteil, dass das JIC-Werkzeug prinzipiell auch für andere auf Java-Bytecode basierende Programmiersprachen wie z.B. Groovy, Scala und Clojure nutzbar ist.

Das JIC-Findbugs-Plugin ist hier unter der Open Source Apache Licence 2.0 verfügbar.

Der Quellcode des Findbugs-Plugins ist auf github gehostet.