|
|||||||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||||||
|
Java 7 - JSR 334 - Project Coin - Sprachneuerungen im Zusammenhang mit Generics
|
||||||||||||||||||||||||||||||||||||
Die Version 7 von Java hat eine Reihe von kleineren Sprachergänzungen
gebracht, die unter dem Namen "Project Coin" entwickelt wurden. Wir
haben im vorangegangenen Beitrag unserer Reihe über Java7-Neuerungen
einen Teil dieser Neuerungen angesehen. Dieses Mal wollen wir die
neuen Sprachmittel vorstellen, die Verbesserungen im Zusammenhang mit Generics
betreffen.
Sprachverbesserungen im Zusammenhang mit GenericsNachdem die Generics mit Java 5 freigegeben wurden und sie nun seit ca. 6 Jahren benutzt werden, sind einige Defizite aufgefallen, die der javac-Compiler mit vertretbarem Aufwand beheben kann. Dabei geht es um(a) eine verbesserte Type Inference für Konstruktoren generischer Typen und umBeginnen wir mit den Details zu Punkt (a): dem sogenannten Diamond -Operator, der im Zusammenhang mit der Type Inference für Konstruktoren generischer Typen erfunden wurde. Der Diamond -OperatorBei der Konstruktion von Objekten eines parametrisierten Typs musste bislang in der new -Expression der Typ des Objekts vollständig mit allen Typparametern angegeben werden:Beispiel (kompletter Typename): ThreadLocal<Map<String, List<Integer>>> ref = new ThreadLocal<Map<String, List<Integer>>>();Die meisten Entwickler finden diese Schreibweise redundant und lästig. Wozu muss man den gesamten Typ auf der rechten Seite noch einmal hinschreiben, wenn er doch schon auf der linken Seite steht? Sollte man aus Bequemlichkeit oder Unachtsamkeit die Schreibweise vereinfacht haben, bekommt man vom Compiler allerlei Warnungen. Beispiel (Raw Type): ThreadLocal<Map<String, List<Integer>>> ref = new ThreadLocal();Hier wird auf der rechten Seite - versehentlich oder absichtlich - der Raw Type verwendet. Der Compiler beschwert sich darüber und gibt all die unchecked -Warnungen aus, die im Zusammenhang mit Raw Types angemessen sind. In Java 7 gibt es nun den sogenannten Diamond -Operator, der die Schreibweise vereinfacht und den Compiler veranlasst, sich die fehlende Information selber aus dem Kontext des Konstruktoraufrufs zu überlegen. Beispiel ( Diamond -Operator): ThreadLocal<Map<String, List<Integer>>> ref = new ThreadLocal<>();Anstelle des kompletten Typparameters muss nur noch ein leeres Paar von spitzen Klammern geschrieben werden. Den Rest deduziert der Compiler aus dem Typ der linken Seite der Zuweisung. Das leere Klammer-Paar wird als Diamond Operator bezeichnet, obwohl er kein Operator im eigentlichen Sinne der Sprache ist, sondern nur ein syntaktisches Konstrukt, das den Raw Type vom parametrisierten Typ ohne Typparameter unterscheidet. Die Deduktion der fehlenden Typparameter aus dem Kontext nennt man Type Inference . Type Inference gibt es schon, seit es die Java Generics gibt; sie wird auch im Zusammenhang mit dem Aufruf generischer Methoden verwendet. Die Type Inference ist nun für Java 7 auf Konstruktoraufrufe ausgedehnt worden (siehe /JSR334/ und /DIAMND/). Wie so häufig im Zusammenhang mit Generics ist nicht alles so einfach, wie man es sich wünschen würde. Die Type Inference führt nämlich gelegentlich zu überraschenden Ergebnissen. Beispiel ("interessante" Type Inference): Set<Number> s3 = new HashSet<>(Arrays.asList(0L,0L)); // errorDer Compiler hätte für die Type Inference prinzipiell die Wahl, ob er die fehlenden Typparameter für den HashSet<> aus dem Typ des Ausdrucks auf der linken Seite der Zuweisung ableiten will ( Set<Number> => E:=Number ) oder ob er für die Deduktion den Typ des Konstruktorarguments heranzieht. Der Typ des Konstruktorarguments ist in diesem Beispiel der Returntyp der Arrays.asList() -Methode; diesen Returntyp muss der Compiler auch erst einmal per Type Inference bestimmen, weil die Arrays.asList() -Methode eine generische Methode ist. Bei der Deduktion für die asList() -Methode kommt E:=Long heraus, so dass das Argument für den Konstruktor vom Typ List<Long> ist. Der Compiler würde also bei Berücksichtigung des Konstruktorarguments für die Type Inference beim Diamond -Operator E:=Long deduzieren ( List<Long> => E:=Long ). Welche Strategie wählt nun der Compiler? Das ist klar geregelt: Für den Compiler haben die Konstruktorargumente Vorrang; er deduziert also E:=Long und dann gibt es eine Fehlermeldung, weil der Set<Number> auf der linken Seite inkompatibel zum HashSet<Long> auf rechten Seite ist. Das ist vielleicht nicht ganz die Deduktion, die man sich gewünscht hätte, aber es ist eine definierte und auch sinnvolle Deduktionsstrategie. Die Strategie bei der Type Inference ist nämlich für generische Methoden und den Diamond -Operator identisch: Der Compiler schaut sich immer erst die Typen der Konstruktor- bzw. Methodenargumente an. Nur wenn er daraus nichts deduzieren kann, schaut er die linke Seite der Zuweisung an. Letzteres tut er natürlich nur, wenn der Konstruktor- bzw. Methodenaufruf im Kontext einer Zuweisung steht. Und was ist, wenn der Diamond -Operator nicht im Kontext einer Zuweisung auftaucht? Das wollen wir jetzt an dieser Stelle nicht vertiefen. Aber auch dafür gibt es Regeln. Wer sich für die Details der Type Inference interessiert, kann sie in den Generics FAQs (siehe /GENFAQ1/) nachlesen. Insgesamt führt der Diamond -Operator zu spürbar weniger Code. Gelegentlichen gibt es Überraschungseffekten, wie oben erläutert. Neben dem Code-Overhead bei Konstruktoraufrufen von generischen Typen hat man in Java 7 versucht, auch die Probleme zu beheben, die sich aus der Kombination von Varargs und Generics ergeben. Beide Sprachmittel, die variable Argumentenliste und die generischen Typen, sind mit Java 5 zur Sprache hinzugekommen. Augenscheinlich hat man die Kombination der beiden neuen Sprachmittel nicht bis in Detail durchdacht, denn bei gewissen Funktionsaufrufen gibt es Probleme. Varargs und GenericsUnter Varargs versteht man das in Java 5 eingeführte Sprachmittel der variablen Argumentenliste für Methoden und Konstruktoren. Man findet im JDK zahlreiche Beispiele für deren Verwendung, zum Beispiel die Methode asList() in der Klasse java.util.Arrays :public static <T> List<T> asList(T... a);Dort, wo T... spezifiziert ist, kann eine beliebig lange Liste von Argumenten des Typs T übergeben werden, wobei T ein Typparameter, also ein "unbekannter Typ" ist. Der Compiler erzeugt aus der variablen Argumentenliste ein Array und übergibt dieses Array an die Methode. In Wirklichkeit, d.h. im Byte Code, ist asList() also eine Methode mit dem Argumenttyp T[] . Wenn die Aufrufargumente einer Methode mit einer variablen Argumentenliste von einem nicht-reifizierbaren Typ (siehe /GENFAQ2/) sind, dann produziert der Compiler eine unchecked-Warnung. Nicht-reifizierbare Typen sind Typen, die aufgrund der Type Erasure im Byte-Code keine exakte Darstellung mehr haben. Zu den nicht-reifizierbaren Typen gehören praktisch alle parametrisierten Typen (wie List<String> , Map<? extends Number,String> , Future<Long> , usw.) sowie alle Typvariablen (wie etwa T im obigen Beispiel). Schauen wir uns ein Beispiel für die problematische Kombination von Varargs und Generics an: List<ThreadLocal<String>> list2 = Arrays.asList(new ThreadLocal<String>());Die Warnung hängt damit zusammen, dass der Compiler aus den übergebenen variablen Argumenten ein Array konstruieren muss. Dieses Array ist vom Typ ThreadLocal<String>[] . Der Type ThreadLocal<String> ist nicht-reifizierbar, denn im Byte Code bleibt nach der Type Erasure nur noch ThreadLocal übrig. In Java sind aber Arrays mit Elementen eines nicht-reifizierbaren Typs unzulässig. Warum dies so ist, ist in der Randbemerkung "Heap Pollution im Zusammenhang mit Arrays" beschrieben. Der Compiler erzeugt also ein Array von einem unzulässigen Typ, das er mit einer Fehlermeldung zurückweisen würde, wenn wir selber versuchen würden, ein solches Array zu erzeugen. Er macht also etwas, das eigentlich nicht sein sollte und das er „normalerweise“ mit einer Fehlermeldung quittieren würde. In diesem Dilemma gibt er dann eine unchecked-Warnung anstelle einer Fehlermeldung aus.
Oft ist die Warnung des Compilers unberechtigt, weil die aufgerufene Methode so implementiert ist, dass sie mit dem compiler-generierten Array nichts Problematisches tut. Ausgegeben wird die Warnung aber immer. Dummerweise kann nun ausgerechnet der Aufrufer einer Varargs-Methode am allerwenigsten beurteilen, ob die Warnung berechtigt ist oder nicht; das kann eigentlich nur der Implementierer der Methode beurteilen. Die Änderung in Java 7 besteht nun darin, dass der Compiler die Warnung nicht mehr nur beim Aufruf der Varargs-Methode gibt, sondern auch schon bei der Definition der Methode (siehe /JSR334/ und /VARARG/). Hier ist ein Beispiel: public static <E> void addAll(List<E> list, E... array) {Die neue Warnung bei der Methodendefinition soll den Implementierer der Methode dazu anregen, darüber nachzudenken, ob seine Methode für den Benutzer unproblematisch ist oder ob sie womöglich zur Heap Pollution führen könnte. Die obige Varargs-Methode ist in der Tat unproblematisch, weil das vom Compiler aus E... erzeugte Array vom Typ E[] nur gelesen, aber nicht modifiziert wird und es auch außerhalb der Methode überhaupt nicht sichtbar und nicht zugänglich ist. Wenn sich der Implementierer seiner Sache sicher ist, dann kann er an seine Methode die Annotation @SafeVarargs schreiben – mit dem Effekt, dass auch bei der Benutzung der Methode keine Warnung mehr kommt. Der Implementierer einer Varargs-Methode kann also mit Hilfe dieser Annotation dafür sorgen, dass der Aufrufer seiner Methode nicht mehr mit Warnungen belästigt wird, die er ohnehin nicht beurteilen kann. Nun sollte der Implementierer die Annotation @SafeVarargs natürlich nur dann benutzen, wenn er sicher weiß, dass auch wirklich keine Typprobleme auftreten können. Wann ist eine vom Compiler monierte Varargs-Methode problematisch bzw. unproblematisch? Das ist nicht immer ganz einfach zu beurteilen. Hier ist ein offensichtliches Beispiel: Pair<String,String>[] method ( Pair<String,String>... lists) { //1: Warnung: possible heap pollutionZu Zeile //1 kommt die Warnung wegen der möglichen Heap-Pollution; in Zeile //2 wird das vom Compiler erzeugte Array mit einem Element von einem unpassenden Typ befüllt. Das führt unweigerlich zur Heap Pollution und zur unerwarteten ClassCastException , wie man an einem einfachen Benutzungsbeispiel zeigen kann: public static void main(String[] args) {Zu Zeile //3 kommt die Warnung wegen der Erzeugung eines unzulässigen generischen Arrays; in Zeile //4 kommt dann eine ClassCastException . Eine @SafeVarargs -Annotation wäre hier gänzlich unangebracht, denn die Varargs-Methode führt die Heap Pollution mutwillig herbei, indem sie das vom Compiler generierte Array ändert und ein unpassendes Element in das Array einfügt. Aber nicht immer ist die Situation so eindeutig. Wie ist es hier? Ein weniger offensichtliches Beispiel: <T> T[] method_2 ( T... args) { //1: Warnung: possible heap pollutionIst die Varargs-Method in Ordnung? Immerhin ändert sie das compiler-generierte Array nicht und steckt auch keine unpassenden Elemente hinein. Trotzdem ist Vorsicht angebracht. Obwohl die Methode harmlos aussieht, ist sie keineswegs unproblematisch. Im nachfolgenden, ebenfalls völlig harmlos aussehenden Beispiel zeigen sich die Probleme bereits: <T> T[] method_1(T t1, T t2) {Um zu verstehen, was hier passiert, muss man sich die Type Erasure ansehen. Nach der Type Erasure sieht das obige Beispiel so aus: Object[] method_2( Object[] args) { //1: Warnung: possible heap pollutionAus T... wird ein T[] , das in Wirklichkeit, d.h. im Byte-Code, ein Object[] ist. Die Methode method_1() erwartet aber, dass sie ein String[] von der Methode method_2() bekommt, wenn sie beim Methodenaufruf Strings als Argumente übergibt. Das stimmt aber leider nicht, was sich später in einer ClassCastException äußert. Fazit: Auch hier wäre die Verwendung einer @SafeVarargs -Annotation an der Varargs-Methode method_2() gänzlich unangebracht. Leider ist es in diesem Falle aber überhaupt nicht offensichtlich.
Was also nützt die Neuerung in Java 7 im Zusammenhang mit Varargs
und Generics? Wenn der Implementierer einer Methode mit einer variablen
Argumentenliste absolut sicher ist, dass seine Methode keine Heap Pollution
verursachen kann, dann kann er mit der
@SafeVarargs
-Annotation
dafür sorgen, dass der Aufrufer seiner Methode nicht mehr wie bisher
mit unchecked-Warnungen belästigt wird, die er ohnehin nicht beurteilen
kann. Ob der Implementierer einer Varargs-Methode jedoch stets in
der Lage ist, zuverlässig zu beurteilen, wann er die Annotation zu
Recht verwendet und wann er einfach nur irrtümlich eine durchaus berechtigte
Warnung unterdrückt, ist eine ganz andere Frage. An dem zugrundeliegenden
Problem, dass nämlich variable Argumentenlisten zur Erzeugung problematischer
Array mit nicht-reifizierbarem Elementtyp führen, hat sich auch in
Java 7 nichts geändert.
Zusammenfassung und AusblickIn diesem Beitrag haben wir die Spracherweiterungen in Java 7 vorgestellt, die das Arbeiten mit Generics vereinfachen. Der Diamond-Operator liefert die automatische Deduktion von Typparametern bei der Konstruktion von Objekten eines generischen Typs. Die Möglichkeit, Warnung im Zusammenhang mit Varargs zu unterdrücken, vereinfacht in gewissen Fällen den Aufruf von Methoden mit variabler Argumentenliste.Literaturverweise
Die gesamte Serie über Java 7:
|
|||||||||||||||||||||||||||||||||||||
© Copyright 1995-2013 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/59.Java7.Coin2/59.Java7.Coin2.html> last update: 24 Jan 2013 |