Die Grundlagen von Java Generics

1. Einführung

Java Generics wurde in JDK 5.0 eingeführt, um Fehler zu reduzieren und eine zusätzliche Abstraktionsebene über Typen hinzuzufügen.

Dieser Artikel ist eine schnelle Einführung in Generics in Java, das Ziel dahinter und deren Verwendung zur Verbesserung der Codequalität.

2. Die Notwendigkeit für Generika

Stellen wir uns ein Szenario vor, in dem wir eine Liste in Java erstellen möchten, um Integer zu speichern. wir können versucht sein zu schreiben:

List list = new LinkedList();
list.add(new Integer(1));
Integer i = list.iterator().next();

Überraschenderweise beschwert sich der Compiler über die letzte Zeile. Es ist nicht bekannt, welcher Datentyp zurückgegeben wird. Der Compiler erfordert ein explizites Casting:

Integer i = (Integer) list.iterator.next();

Es gibt keinen Vertrag, der garantieren könnte, dass der Rückgabetyp der Liste ein Integer ist. Die definierte Liste kann ein beliebiges Objekt enthalten. Wir wissen nur, dass wir eine Liste abrufen, indem wir den Kontext untersuchen. Bei der Betrachtung von Typen kann nur garantiert werden, dass es sich um ein Object__ handelt. Daher ist eine explizite Umwandlung erforderlich, um sicherzustellen, dass der Typ sicher ist.

Diese Besetzung kann ärgerlich sein, wir wissen, dass der Datentyp in dieser Liste ein Integer ist. Die Besetzung stört auch unseren Code. Es kann zu typbezogenen Laufzeitfehlern kommen, wenn ein Programmierer beim expliziten Casting einen Fehler macht.

Es wäre viel einfacher, wenn Programmierer erklären könnten, dass sie bestimmte Typen verwenden möchten, und der Compiler die Korrektheit dieses Typs sicherstellen kann. Dies ist die Kernidee von Generika.

Ändern Sie die erste Zeile des vorherigen Code-Snippets wie folgt:

List<Integer> list = new LinkedList<>();

Durch Hinzufügen des Rautenoperators <>, der den Typ enthält, schränken wir die Spezialisierung dieser Liste nur auf den Typ Integer ein, d. H. Wir geben den Typ an, der in der Liste enthalten ist. Der Compiler kann den Typ zur Kompilierzeit erzwingen.

In kleinen Programmen mag dies als triviale Ergänzung erscheinen, in größeren Programmen kann dies jedoch eine erhebliche Robustheit hinzufügen und das Lesen des Programms erleichtern.

3. Generische Methoden

Generische Methoden sind Methoden, die mit einer einzigen Methodendeklaration geschrieben werden und mit Argumenten unterschiedlicher Typen aufgerufen werden können. Der Compiler sorgt für die Richtigkeit des verwendeten Typs. Dies sind einige Eigenschaften allgemeiner Methoden:

  • Generische Methoden haben Typparameter (der Diamantoperator umgibt)

der Typ) vor dem Rückgabetyp der Methodendeklaration ** Typparameter können begrenzt werden (Grenzen werden später im Abschnitt erläutert

Artikel) ** Generische Methoden können verschiedene durch Kommas getrennte Typenparameter haben

in der Methodensignatur ** Der Methodenkörper für eine generische Methode ist wie eine normale Methode

Ein Beispiel zum Definieren einer generischen Methode zum Konvertieren eines Arrays in eine Liste:

public <T> List<T> fromArrayToList(T[]a) {
    return Arrays.stream(a).collect(Collectors.toList());
}

Im vorherigen Beispiel impliziert das <T> in der Methodensignatur, dass sich die Methode mit dem generischen Typ T befasst. Dies ist auch dann erforderlich, wenn die Methode ungültig ist.

Wie oben erwähnt, kann die Methode mit mehr als einem generischen Typ arbeiten. Wenn dies der Fall ist, müssen alle generischen Typen zur Methodensignatur hinzugefügt werden, z. B. wenn wir die obige Methode für den Typ T und den Typ ändern möchten G , sollte es so geschrieben werden:

public static <T, G> List<G> fromArrayToList(T[]a, Function<T, G> mapperFunction) {
    return Arrays.stream(a)
      .map(mapperFunction)
      .collect(Collectors.toList());
}

Wir übergeben eine Funktion, die ein Array mit den Elementen des Typs T in eine Liste mit Elementen des Typs G konvertiert. Ein Beispiel wäre die Konvertierung von Integer in seine String__-Darstellung:

@Test
public void givenArrayOfIntegers__thanListOfStringReturnedOK() {
    Integer[]intArray = {1, 2, 3, 4, 5};
    List<String> stringList
      = Generics.fromArrayToList(intArray, Object::toString);

    assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}

Es ist erwähnenswert, dass die Empfehlung von Oracle darin besteht, einen generischen Typ mit einem Großbuchstaben darzustellen und einen aussagekräftigeren Buchstaben für die Darstellung von formalen Typen zu verwenden. In Java Collections wird beispielsweise T für den Typ K für den Schlüssel und V für den Wert verwendet.

3.1. Gebundene Generika

Wie bereits erwähnt, können Typparameter begrenzt werden. Bounded bedeutet " restricted ", wir können Typen einschränken, die von einer Methode akzeptiert werden können.

Beispielsweise können wir angeben, dass eine Methode einen Typ und alle seine Unterklassen (obere Schranke) oder einen Typ alle seine Oberklassen (untere Schranke) akzeptiert.

Um einen Typ mit Obergrenze zu deklarieren, verwenden wir nach dem Typ das Schlüsselwort extends , gefolgt von der Obergrenze, die wir verwenden möchten. Zum Beispiel:

public <T extends Number> List<T> fromArrayToList(T[]a) {
    ...
}

Das Schlüsselwort extends wird hier verwendet, um zu bedeuten, dass der Typ T im Fall einer Klasse die Obergrenze erweitert oder im Fall einer Schnittstelle eine Obergrenze implementiert. Ein Typ kann auch mehrere Obergrenzen haben, wie folgt:

<T extends Number & Comparable>

Wenn einer der durch T erweiterten Typen eine Klasse ist (d. h. Number ), muss er an erster Stelle in der Liste der Grenzen stehen.

4. Platzhalter mit Generics verwenden

Platzhalter werden in Java durch das Fragezeichen „ ? “ Dargestellt und werden verwendet, um auf einen unbekannten Typ zu verweisen. Platzhalter sind besonders nützlich, wenn Generics verwendet werden, und sie können als Parametertyp verwendet werden. Zunächst ist jedoch eine wichtige Anmerkung zu beachten.

  • Es ist bekannt, dass Object der Supertyp aller Java-Klassen ist, eine Auflistung von Object ist jedoch nicht der Supertyp einer Collection. **

Beispielsweise ist eine List <Object> nicht der Supertyp von List <String> und das Zuweisen einer Variablen vom Typ List <Object> zu einer Variablen vom Typ List <String> führt zu einem Compiler-Fehler. Dadurch werden mögliche Konflikte vermieden, die auftreten können, wenn wir derselben Sammlung heterogene Typen hinzufügen.

Die Gleiche Regel gilt für jede Sammlung eines Typs und seiner Untertypen.

Betrachten Sie dieses Beispiel:

public static void paintAllBuildings(List<Building> buildings) {
    buildings.forEach(Building::paint);
}

Wenn wir uns einen Untertyp von Building vorstellen, beispielsweise ein House , können wir diese Methode nicht mit einer Liste von House verwenden, auch wenn House ein Untertyp von Building ist. Wenn Sie diese Methode mit dem Typ Building und allen ihren Untertypen verwenden müssen, kann der gebundene Platzhalter die Magie ausführen:

public static void paintAllBuildings(List<? extends Building> buildings) {
    ...
}

Diese Methode funktioniert jetzt mit dem Typ Building und allen seinen Subtypen.

Dies wird als Platzhalter mit Obergrenze bezeichnet, wobei Typ Building die Obergrenze ist.

Platzhalter können auch mit einer unteren Schranke angegeben werden, wobei der unbekannte Typ ein Supertyp des angegebenen Typs sein muss. Untere Grenzen können mit dem Schlüsselwort super gefolgt vom spezifischen Typ angegeben werden, z. B. <? Super T> bedeutet einen unbekannten Typ, der eine Oberklasse von T ist (= T und alle seine Eltern).

5. Typ löschen

Generics wurde zu Java hinzugefügt, um die Typsicherheit zu gewährleisten und um sicherzustellen, dass Generics zur Laufzeit keinen Overhead verursacht. Der Compiler wendet zur Kompilierzeit einen Prozess mit dem Namen type erasure auf Generics an.

Beim Löschen des Typs werden alle Typparameter entfernt und durch ihre Grenzen oder durch Object ersetzt, wenn der Typparameter nicht gebunden ist. Daher enthält der Bytecode nach der Kompilierung nur normale Klassen, Schnittstellen und Methoden, um sicherzustellen, dass keine neuen Typen erzeugt werden. Das richtige Casting wird zur Kompilierzeit auch auf den Object -Typ angewendet.

Dies ist ein Beispiel für die Typlöschung:

public <T> List<T> genericMethod(T t) {
    return list.stream().collect(Collectors.toList());
}

Beim Kompilieren wird der unbegrenzte Typ T wie folgt durch Object ersetzt:

public List<Object> fromArrayToList(Object a) {
    return list.stream().collect(Collectors.toList());
}

Wenn der Typ begrenzt ist, wird der Typ zur Kompilierungszeit durch die Begrenzung wie folgt ersetzt:

public <T extends Building> void genericMethod(T t) {
    ...
}

würde sich nach der Kompilierung ändern:

public void genericMethod(Building t) {
    ...
}

6. Fazit

Java Generics ist eine leistungsstarke Erweiterung der Java-Sprache, da die Arbeit des Programmierers einfacher und weniger fehleranfällig ist. Generics erzwingen die Typgenauigkeit zur Kompilierzeit und ermöglichen vor allem die Implementierung generischer Algorithmen, ohne zusätzlichen Aufwand für unsere Anwendungen zu verursachen.

Der dem Artikel beiliegende Quellcode ist auf GitHub unter https://github.com/eugenp/tutorials/tree/master/core-java-lang-syntax [over verfügbar.