[JAVA] Punkt 32: Kombinieren Sie Generika und Varargs mit Bedacht

32. Seien Sie vorsichtig, wenn Sie Generika mit Argumenten variabler Länge verknüpfen

Methoden und Generika mit Argumenten variabler Länge wurden gleichzeitig in Java 5 veröffentlicht, passen aber nicht gut zusammen. Das Argument variabler Länge ist eine undichte Abstraktion. https://euske.github.io/slides/sem20170627/index.html Beim Aufrufen einer Methode mit Argumenten variabler Länge wird ein Array erstellt, das die Argumente variabler Länge enthält, und dieses Array ist sichtbar. (Wenn es abstrahiert ist, scheint es als undichte Abstraktion bezeichnet zu werden, was bedeutet, dass der Benutzer sich dessen nicht bewusst sein sollte.) Infolgedessen wird der Compiler verwirrt, wenn generische oder parametrisierte Typen an Argumente variabler Länge übergeben werden. Wenn Sie eine Methode mit einem nicht überprüfbaren Typ als Argument variabler Länge deklarieren, gibt der Compiler eine Warnung aus. Die Warnung wird auch ausgelöst, wenn eine Methode aufgerufen wird, der ein Argument variabler Länge mit einem Typ übergeben wird, von dem angenommen wird, dass er nicht überprüfbar ist. Die Warnung lautet wie folgt.

warning: [unchecked] Possible heap pollution from
    parameterized vararg type List<String>

Haufenverschmutzung tritt auf, wenn sich verschiedene parametrisierte Typen auf einen anderen Typ beziehen. Durch die Verschmutzung des Haufens schlägt das automatische Casting des Compilers fehl und verletzt die typsichere Grundlage, die durch generische Typen garantiert wird.

Betrachten Sie als Beispiel die folgende Methode.

// Mixing generics and varargs can violate type safety!
static void dangerous(List<String>... stringLists) {
    List<Integer> intList = List.of(42);
    Object[] objects = stringLists;
    objects[0] = intList;             // Heap pollution
    String s = stringLists[0].get(0); // ClassCastException
}

Diese Methode hat keine eindeutige Besetzung, aber ich erhalte eine `ClassCastException```. In der letzten Zeile wurde eine implizite Umwandlung durch den Compiler durchgeführt, die fehlschlug. Dies zeigt an, dass es ** unsicher ist, Werte in generischen Argumenten variabler Länge ** zu speichern. Der Grund, warum ein Kompilierungsfehler nicht auftritt, wenn eine Methode mit einem Argument variabler Länge eines generischen Typs deklariert wird, ist, dass das Argument variabler Länge eines generischen Typs oder eines parametrisierten Typs sehr praktisch ist. Als Beispiel Arrays.asList (T ... a) , Collections.addAll (Collection <? Super T> c, T ... Elemente) , EnumSet. In der Java-Standardbibliothek gibt es Methoden wie (E first, E ... rest) `` , die typsicher sind. Vor Java6 gab es für den Methodenautor keine Möglichkeit, mit der Warnung umzugehen, die beim Aufruf einer Methode mit einem generischen Argument variabler Länge angezeigt wird. Dies bedeutete, dass der Anrufer jedes Mal `@ SuppressWarnings (" nicht markiert ")` schreiben musste. In Java7 wurde die Annotation `SafeVarargs``` hinzugefügt. Wenn Sie dies einer Methode mit einem generischen Argument mit variabler Länge vom Typ geben, erhält der Aufrufer keine Warnung. ** Die Annotation `SafeVarargsgibt an, dass der Autor der Methode verspricht, dass die Methode typsicher ist. ** **.safevarargs```In Anmerkungen ist es wirklich wichtig, typsicher zu sein, aber wie sollten wir sicher sein, dass es typsicher ist? Generische Arrays werden generiert, wenn eine Methode aufgerufen wird, die ein Array variabler Länge enthält. Bei der Verarbeitung einer Methode ist es sicher (dh nur eine einfache Übertragung vom Argument zur Methode), wenn sie nicht im Array gespeichert wird und keine Verweise von nicht vertrauenswürdigem Code auf das Array zulässt. Es kann gesagt werden, dass es sicher ist). Es sollte beachtet werden, dass selbst wenn für ein Argumentarray mit variabler Länge nichts gespeichert ist, dies die Typensicherheit gefährden kann. Das folgende Beispiel scheint in Ordnung zu sein, ist aber gefährlich.

// UNSAFE - Exposes a reference to its generic parameter array!
static <T> T[] toArray(T... args) {
    return args;
}

Der Typ dieses Arrays hängt vom Typ der Kompilierungszeit des im Argument der Methode übergebenen Typs ab, aber der Compiler gibt nicht genügend Informationen an, um eine genaue Entscheidung zu treffen. Diese Methode gibt ein Array von Argumenten variabler Länge zurück, sodass im Aufrufstapel eine Heap-Verschmutzung auftritt. Um konkret zu denken, betrachten Sie die folgende Methode.

static <T> T[] pickTwo(T a, T b, T c) {
    switch(ThreadLocalRandom.current().nextInt(3)) {
      case 0: return toArray(a, b);
      case 1: return toArray(a, c);
      case 2: return toArray(b, c);
    }
    throw new AssertionError(); // Can't get here
}

Beim Kompilieren dieser Methode erzeugt der Compiler Code, der ein Argumentarray mit variabler Länge erzeugt, um zwei `T```-Instanzen an die` toArray -Methode zu übergeben. Der Code ordnet ein Array von `` `Object []` `` an, so dass jedes Objekt vom Aufrufer übergeben werden kann. Die `` `toArray -Methode gibt dieses Array einfach an die`` pickTwo``` -Methode zurück, und die ``pickTwo -Methode gibt dieses Array an den Aufrufer zurück. Daher gibt die Methode `` `pickTwo immer ein Array vom Typ`` Object [] ``zurück. Erwägen Sie, `` `pickTwo``` aus dem Hauptcode unten aufzurufen.

public static void main(String[] args) {
    String[] attributes = pickTwo("Good", "Fast", "Cheap");
}

In Bezug auf diesen Code wird weder ein Kompilierungsfehler noch eine Warnung angezeigt. Wenn ich ihn jedoch ausführe, erhalte ich eine ClassCastException, obwohl ich ihn nicht explizit umgewandelt habe. Dies passiert, wenn ich versuche, es `String []` zu machen, weil der Rückgabetyp der `pickTwo``` -Methode Object [] `` `ist. .. Dieses Beispiel erinnert uns daran, dass es nicht sicher ist, anderen Methoden den Zugriff auf ein generisches Argumentarray mit variabler Länge zu ermöglichen. Hiervon gibt es zwei Ausnahmen:

So verwenden Sie ein typisches sicheres generisches Argument mit variabler Länge:

// Safe method with a generic varargs parameter
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
    List<T> result = new ArrayList<>();
    for (List<? extends T> list : lists)
        result.addAll(list);
    return result;
}

safevarargsDie Entscheidung, ob eine Anmerkung hinzugefügt werden soll, ist einfach:Für alle Methoden mit Argumenten variabler Länge generischer oder parametrisierter TypensafevarargsSollte kommentiert werden.. Das bedeutet, dasstoarraySchreiben Sie keine unsicheren Argumentmethoden mit variabler Länge wie Methoden. Die sichere Argumentmethode mit variabler Länge erfüllt:

  1. Im Argument variabler Länge ist nichts gespeichert
  2. Machen Sie Argumente mit variabler Länge für unzuverlässigen Code nicht sichtbar

Die Annotation `` `SafeVarargs``` gilt nur für Methoden, die nicht überschrieben werden.

safevarargsAls eine andere Korrespondenz als die Verwendung von Anmerkungen aus item28 Argumente mit variabler LängelistEs ist denkbar, es durch zu ersetzen. DannflattenDie Methode ändert sich wie folgt.

// List as a typesafe alternative to a generic varargs parameter
static <T> List<T> flatten(List<List<? extends T>> lists) {
    List<T> result = new ArrayList<>();
    for (List<? extends T> list : lists)
        result.addAll(list);
    return result;
}

Das Schöne daran ist, dass es Typensicherheit garantiert und Sie die Annotation `` `SafeVarargs``` nicht selbst hinzufügen müssen. Die schlechte Nachricht ist, dass der clientseitige Code etwas langweilig und etwas langsamer sein kann.

Recommended Posts

Punkt 32: Kombinieren Sie Generika und Varargs mit Bedacht
Punkt 53: Verwenden Sie Varargs mit Bedacht
Punkt 67: Mit Bedacht optimieren
Punkt 55: Optionen mit Bedacht zurückgeben
Punkt 52: Verwenden Sie Überladung mit Bedacht
Punkt 45: Verwenden Sie Streams mit Bedacht
Punkt 83: Verwenden Sie die verzögerte Initialisierung mit Bedacht
[Java] Generics-Klasse und Generics-Methode
Punkt 66: Verwenden Sie native Methoden mit Bedacht
URLSession mit URLSession und Normal kombinieren
Java Generics T und? Unterschied