Entwicklung der Java-8-Unterstützung für Eclipse

Seite 2: Preis und Mehrwert

Inhaltsverzeichnis

Es ist leicht, Java für seine Komplexität zu kritisieren. Andererseits sehen Lambda-Ausdrücke durchaus einfacher aus als ihre Entsprechung mit anonymen Klassen. Bei genauerer Betrachtung gibt es in Java 8 nun drei Situationen, in denen Programmierer kürzeren Code schreiben können, da sie redundante Typinformation außen vor lassen können: Aufrufe generischer Methoden (Java 5), "diamond"-Ausdrücke (Java 7) und Lambda-Ausdrücke (Java 8). Damit trotz Weglassens noch eine vollständige Typprüfung stattfinden kann, muss der Compiler alle nicht berücksichtigten Typen aus dem Kontext ableiten. Das wird Typinferenz genannt und ist in der JLS 8 durch ein komplexes, neu geschriebenes Kapitel definiert. Diese neue Typinferenz zu
spezifizieren und zu implementieren, war eine immense Anstrengung, die sich aber gelohnt hat: Die neue Spezifikation hat einige Unklarheiten voriger Versionen bereinigt, sodass sich mit mehr Zuversicht sagen lässt, dass Java eine wohldefinierte Sprache ist. Außerdem kann der Compiler bei der Typinferenz dem Programmierer viel Arbeit abnehmen.

Schwierig wird es nur, wenn Programmierer und Compiler eine unterschiedliche "Vorstellung" von einem Programm haben: Wenn die Typinferenz keine Lösung zur Belegung der weggelassenen Typen findet, ist es schwierig, eine benutzerfreundliche Fehlermeldung zu erzeugen. Entweder sagt der Compiler nur, dass es nicht geht, oder er liefert seitenfüllende Protokolle seines gescheiterten Inferenzversuchs. Einen gesunden Mittelweg mit hilfreichen Hinweisen zu finden, bleibt als Aufgabe für zukünftige Versionen der Compiler.

Ergebnisse der Typinferenz werden in Texthovers dargestellt: unten die Parametrisierung der generischen Methode "comparing"; oben die Interface-Methode "apply", die den Typ der Methodenreferenz bestimmt.

Der Kern des Komplexitätsproblems von Java liegt aber nicht in einem einzelnen Sprachfeature. Die Komplexität jedes neuen Features multipliziert sich vielmehr mit der Komplexität aller vorhandenen. Die neue Typinferenz wäre wesentlich einfacher, wenn keine Raw Types unterstützt werden müssten. Noch deutlicher wird das am Beispiel des Overloading. Im ursprünglichen Design von Java mag es als ein konzeptionell überschaubares Feature gegolten haben. Beim Übergang zu Java 8 ist es ein immenser Kostenfaktor. Insbesondere die neue Typinferenz "verdankt" einen Großteil ihrer Komplexität der Tatsache, dass Overloading zu unterstützen ist.

Eine neutrale Kosten-Nutzen-Analyse zu einer neuen Programmiersprache mag zu dem Ergebnis kommen, dass Overloading seinen Preis nicht wert ist. Bei der Weiterentwicklung von Java stellt sich diese Frage aber nicht – jedes neue Feature muss alle früheren Entwurfsentscheidungen mit bezahlen.

In einem Punkt ist ECJ mehr als nur die getreue Implementierung der Spezifikation. Der allgemein weniger beobachtete JSR 308 (Type Annotations) mag isoliert betrachtet wertlos erscheinen. ECJ haucht diesem Feature aber eigenes Leben ein, indem das neue Konzept für lückenlose Analysen von Null-Referenzen angewendet wird. Am Beispiel der Null-Analysen lassen sich drei Stufen erkennen:

  • Die Datenflussanalyse des Compilers wird benutzt, um zu prüfen, ob eine lokale Variable den Wert null annehmen kann und ob dieselbe Variable ungeprüft dereferenziert wird (seit Eclipse 3.1). Allerdings ist diese Analyse auf den Kontext einer einzelnen Methode beschränkt, es gibt keine Informationen über die von außerhalb kommenden Werte.
  • Methodensignaturen lassen sich durch Null-Annotationen (@NonNull vs. @Nullable) um Verträge anreichern, sodass die Analyse auch über Methodengrenzen möglich wird (seit Eclipse 4.2). Allerdings kann man nur bestimmte Programmstellen annotieren; insbesondere, ob die Elemente einer Collection null sein können oder nicht, lässt sich nicht erfassen.
  • Mit Typannotationen (JSR 308) wird die Frage, wo null erlaubt ist, als Bestandteil des Typsystems aufgefasst. Damit rückt es in die erreichbare Nähe, dass ein erweiterter Typcheck lückenlos alle NullPointerExceptions bereits beim Kompilieren aufdeckt.

ECJ erkennt Null-Annotationen in allen vom JSR 308 vorgesehenen Positionen und bezieht diese in die Null-Analyse mit ein. Für eine lückenlose Untersuchung fehlt nur noch ein Baustein: Es muss möglich sein, auch für Bibliotheken anderer Anbieter, die keine Nulltyp-Annotationen haben, diese extern zu definieren.

Verwendung von Null-Typannotationen am Bespiel von Generics. Oben die sichere Verwendung von Typen mit und ohne "null". Unten: Der Compiler meldet einen Fehler, der sonst später bei der Verwendung der Ergebnisliste zu NullPointerException führen könnte.

Diese Verwendung entspricht genau der Intention des JSR 308: Durch neue Annotationen und durch Erweiterungen des Typechecking lassen sich viele typische Programmierfehler bereits beim Kompilieren vollständig erkennen, was deutlich größere Sicherheit bringt als jede heuristikbasierte statische Analyse.