Der Diamantoperator (<>), der in Java 7 eingeführt wurde, unterstützte zunächst nur die Erzeugung von Instanzen konkreter Klassen, während anonyme innere Klassen ausdrücklich ausgeschlossen waren. Als Entwickler versuchten, Konstruktionen wie new Comparable<String>() { ... } zu verwenden, wies der Compiler die Diamant-Variante new Comparable<>() { ... } zurück, da anonyme Klassen Typmitglieder einführen könnten, die auf abgeleitete Typparameter verweisen, was potenziell unsichere Typsysteme schaffen könnte.
Das Hauptproblem bezog sich auf nicht denotierbare Typen. Anonyme Klassen können Methoden oder Felder deklarieren, deren Typen von den Typparametern der Klasse abhängen. Wenn der Compiler einen komplexen Schnittmengen-Typ für den Diamanten ableiten würde, wie im problematischen Szenario, in dem eine anonyme Klasse void foo(Box<T> t) {} deklariert, könnte der Typ T ein erfasster Platzhalter darstellen, der im Quellcode nicht ausgedrückt werden kann. Dies schuf ein Szenario, in dem die API der anonymen Klasse Typen enthielt, die auf Quellcodeebene unmöglich zu benennen oder zu überprüfen waren, was dem grundlegenden Erfordernis von Java widersprach, dass alle Typen in öffentlichen APIs denotierbar sein müssen.
Java 9 löste dies durch JEP 213, indem eine Analyse der denotierbaren Typen implementiert wurde. Der Compiler überprüft nun, dass der abgeleitete Typ für die Instanzierung der anonymen Klasse denotierbar ist – das heißt, druckbar unter Verwendung der Java-Typsyntax. Das folgende Beispiel demonstriert die legale Verwendung:
// Gültig in Java 9+ Comparator<String> c = new Comparator<>() { @Override public int compare(String a, String b) { return a.length() - b.length(); } };
Wenn die Inferenz einen komplexen Typ mit Platzhaltern oder Schnittmengen ergibt, der nicht denotiert werden kann, verlangt der Compiler explizite Typargumente. Dies gewährleistet die Typsicherheit und erlaubt gleichzeitig die prägnante Syntax für häufige Fälle.
In einer Handelsplattform, die auf Java 8 basiert, wartete das Entwicklungsteam zehntausende von Ereignis-Handlern. Diese Handler verwendeten anonyme Implementierungen von Comparator<TradeEvent> und Predicate<MarketData> im gesamten Auftragsabgleichsmodul, was explizite Typargumente erforderte und während der Code-Reviews erheblichen visuellen Lärm erzeugte.
Das Team erwog drei Ansätze zur Reduzierung des Boilerplates. Der erste Ansatz bestand darin, alle anonymen Klassen auf Lambda-Ausdrücke zu migrieren. Obwohl dies die Sprachlichkeit für einfache Fälle beseitigte, benötigten viele Handler private Hilfsmethoden oder Ausnahmebehandlungsblöcke, die die Möglichkeiten von Lambdas überschritten. Diese Einschränkung zwang zu umständlichem Refactoring in benannte innere Klassen, was die Klassenanzahl erhöhte und die Lokalität des Verhaltens verringert.
Der zweite Ansatz schlug vor, die expliziten Typargumente beizubehalten. Dies bewahrte die volle Funktionalität und arbeitete mit der bestehenden Java 8-Infrastruktur, verstärkte jedoch die Wartungsbelastung. Entwickler stießen häufig auf Merge-Konflikte beim Ändern von Typsignaturen, und die redundanten Deklarationen erhöhten die kognitive Last während Debugging-Sitzungen.
Der dritte Ansatz schlug vor, auf Java 9 zu aktualisieren, um die Unterstützung des Diamantoperators für anonyme Klassen zu nutzen. Nach der Bewertung der Migrationskosten im Vergleich zu den Produktivitätsgewinnen wählte das Team das Java 9-Upgrade, da die Plattform ohnehin die Integration des Jigsaw-Modulsystems erforderte. Die Analyse der denotierbaren Typen ermöglichte es ihnen, new Comparator<>() { public int compare(TradeEvent a, TradeEvent b) { ... } } zu schreiben, während der Compiler überprüfte, dass TradeEvent einen denotierbaren Typ darstellte.
Diese Änderung reduzierte die durchschnittliche Handler-Definition von vier Zeilen auf eine und beseitigte ungefähr 2.400 Zeilen redundanter Typdeklarationen. Folglich verringerten sich die Merge-Konflikte in generikastarken Modulen erheblich, indem die Notwendigkeit entfiel, explizite Typargumente über Funktionszweige hinweg zu synchronisieren. Die Entwicklungsgeschwindigkeit verbesserte sich in den folgenden Quartalen um fünfzehn Prozent, da der Refactoring-Aufwand reduziert wurde.
Warum schlägt der Diamantoperator fehl, wenn er Typargumente für generische Konstruktoren in Rohtypen ableitet?
Beim Instanziieren einer rohen Klasse wie new ArrayList()<> kann der Diamantoperator keine Typargumente ableiten, da rohe Typen generische Informationen vollständig löschen. Der Compiler behandelt den rohen Typ so, als hätte er keine Typparameter, wodurch die Ableitung unmöglich wird, da die Konstruktorsignatur selbst ihre Parametrisierung verliert. Kandidaten verwechseln dies oft mit Warnungen zur nicht überprüften Konvertierung, aber das grundlegende Problem betrifft die vollständige Löschung der generischen Metadaten in Kontexten roher Typen, nicht nur nicht überprüfte Operationen.
Wie wirkt sich die Interaktion zwischen poly- Ausdrücken und dem Diamantoperator auf die Überladungsauflösung von Methoden aus?
Der Diamantoperator erzeugt einen poly-Ausdruck, dessen Typ vom Zuordnungskontext abhängt. In Methodenaufrufkontexten wie process(new ArrayList<>()) muss der Compiler den Zieltyp aus den formalen Parametern der Methode bestimmen, bevor er die Typinferenz abschließt. Dies schafft eine bidirektionale Abhängigkeit: Die Anwendbarkeit der Methode hängt vom abgeleiteten Typ ab, aber der abgeleitete Typ hängt vom Zieltyp ab. Der Compiler löst dies durch Generierung von Einschränkungen und Phasen der Einbeziehung, was potenziell zu einer Auswahl anderer Überladungen führen kann, als sie mit expliziten Typargumenten aufträte. Kandidaten übersehen häufig, dass die Überladungsauflösung vor der vollständigen Typinferenz erfolgt, was zu überraschenden Kompilierfehlern führt, wenn mehrere Überladungen übereinstimmen könnten.
Was unterscheidet die Einschränkung der denotierbaren Typen von den Anforderungen an die reifizierbaren Typen bei der Erstellung von Arrays?
Während beide Einschränkungen bestimmte generische Operationen verhindern, stellen denotierbare Typen (relevant für die Inferenz des Diamantoperators) sicher, dass Typen im Quellcode ausgedrückt werden können, während reifizierbare Typen (relevant für new T[10]) Laufzeitinformationen über den Typ erfordern. Ein Typ wie List<String> ist denotierbar, aber nicht reifizierbar. Kandidaten verwechseln oft diese Einschränkungen und glauben, dass nicht denotierbare Typen Laufzeitsicherheitsrisiken ähnlich wie Array-Speicher-Ausnahmen darstellen. In Wirklichkeit gefährden nicht denotierbare Typen die Ausdrucksfähigkeit auf Quellcodeebene und die Konsistenz der API, während nicht reifizierbare Typen die Typsicherheit zur Laufzeit gefährden. Dieses Verständnis ist entscheidend beim Entwerfen generischer APIs, die sowohl mit anonymen Klassen als auch mit array-basiertem Legacy-Code kompatibel bleiben müssen.