Die Grundlagen von Java Generics

1. Einleitung

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 kurze Einführung in Generics in Java, das Ziel dahinter und wie sie zur Verbesserung der Qualität unseres Codes verwendet werden können.

2. Der Bedarf an 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 wird sich der Compiler über die letzte Zeile beschweren. Es ist nicht bekannt, welcher Datentyp zurückgegeben wird. Der Compiler benötigt ein explizites Casting:

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

Es gibt keinen Vertrag, der garantieren könnte, dass der Rückgabetyp der Liste eine Ganzzahl ist. Die definierte Liste kann jedes 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 Objekt 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 eine Ganzzahl ist . Die Besetzung überfüllt auch unseren Code. Es kann typbezogene Laufzeitfehler verursachen, wenn ein Programmierer einen Fehler beim expliziten Casting macht.

Es wäre viel einfacher, wenn Programmierer ihre Absicht zum Ausdruck bringen könnten, bestimmte Typen zu verwenden, und der Compiler die Richtigkeit eines solchen Typs sicherstellen könnte. Dies ist die Kernidee hinter Generika.

Ändern wir die erste Zeile des vorherigen Codeausschnitts in:

List list = new LinkedList();

Durch Hinzufügen des Diamantoperators, der den Typ enthält, beschränken wir die Spezialisierung dieser Liste nur auf den Integer- Typ, dh wir geben den Typ an, der in der Liste enthalten sein soll. Der Compiler kann den Typ zur Kompilierungszeit erzwingen.

In kleinen Programmen scheint dies eine triviale Ergänzung zu sein. In größeren Programmen kann dies jedoch zu einer erheblichen Robustheit führen und das Lesen des Programms erleichtern.

3. Allgemeine Methoden

Generische Methoden sind Methoden, die mit einer einzelnen Methodendeklaration geschrieben werden und mit Argumenten unterschiedlichen Typs aufgerufen werden können. Der Compiler stellt die Richtigkeit des verwendeten Typs sicher. Dies sind einige Eigenschaften generischer Methoden:

  • Generische Methoden haben einen Typparameter (der Diamantoperator, der den Typ einschließt) vor dem Rückgabetyp der Methodendeklaration
  • Typparameter können begrenzt werden (Grenzen werden später in diesem Artikel erläutert).
  • Generische Methoden können unterschiedliche Typparameter haben, die in der Methodensignatur durch Kommas getrennt sind
  • Der Methodenkörper für eine generische Methode entspricht einer normalen Methode

Ein Beispiel für die Definition einer generischen Methode zum Konvertieren eines Arrays in eine Liste:

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

Im vorherigen Beispiel wurde die In der Methodensignatur bedeutet dies, dass die Methode den generischen Typ T behandelt . Dies ist auch dann erforderlich, wenn die Methode void zurückgibt.

Wie oben erwähnt, kann die Methode mehr als einen generischen Typ behandeln. In diesem Fall müssen alle generischen Typen zur Methodensignatur hinzugefügt werden, wenn wir beispielsweise die obige Methode ändern möchten, um Typ T und Typ zu behandeln G , es sollte so geschrieben werden:

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

Wir übergeben eine Funktion, die ein Array mit den Elementen vom Typ T in eine Liste mit Elementen vom Typ 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 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 Großbuchstaben zur Darstellung eines generischen Typs zu verwenden und einen aussagekräftigeren Buchstaben zur Darstellung formaler Typen zu wählen. Beispielsweise wird in Java-Sammlungen T für Typ, K für Schlüssel und V für Wert verwendet.

3.1. Begrenzte Generika

Wie bereits erwähnt, können Typparameter begrenzt werden. Begrenzt bedeutet „ eingeschränkt “. 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 (Obergrenze) oder einen Typ alle seine Oberklassen (Untergrenze) akzeptiert.

Zu erklären , einen oberen beschränkten Typen wir das Schlüsselwort erstreckt sich nach der Art von der oberen Grenze gefolgt , dass wir verwenden wollen. Zum Beispiel:

public  List fromArrayToList(T[] a) { ... } 

Das Schlüsselwort expand wird hier verwendet, um zu bedeuten, dass der Typ T bei einer Klasse die Obergrenze erweitert oder bei einer Schnittstelle eine Obergrenze implementiert.

3.2. Mehrere Grenzen

Ein Typ kann auch mehrere Obergrenzen haben:

Wenn einer der Typen, die um T erweitert werden, eine Klasse (dh eine Zahl ) ist, muss sie in der Liste der Grenzen an erster Stelle stehen. Andernfalls wird ein Fehler bei der Kompilierung verursacht.

4. Verwenden von Platzhaltern mit Generika

Platzhalter werden in Java durch das Fragezeichen dargestellt “ ? ”Und sie werden verwendet, um auf einen unbekannten Typ zu verweisen. Platzhalter sind besonders nützlich bei der Verwendung von Generika und können als Parametertyp verwendet werden. Zunächst ist jedoch ein wichtiger Hinweis zu beachten.

Es ist bekannt, dass Object der Supertyp aller Java-Klassen ist. Eine Sammlung von Object ist jedoch nicht der Supertyp einer Sammlung.

Beispielsweise ist eine Liste nicht der Supertyp von List, und das Zuweisen einer Variablen vom Typ List zu einer Variablen vom Typ List führt zu einem Compilerfehler. Dies soll mögliche Konflikte verhindern, 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 buildings) { buildings.forEach(Building::paint); }

Wenn wir uns einen Untertyp des Gebäudes vorstellen , beispielsweise ein Haus , können wir diese Methode nicht mit einer Liste von Häusern verwenden , obwohl House ein Untertyp des Gebäudes ist . Wenn wir diese Methode mit dem Typ Building und all seinen Subtypen verwenden müssen, kann der begrenzte Platzhalter die Magie ausführen:

public static void paintAllBuildings(List buildings) { ... } 

Diese Methode funktioniert nun mit dem Typ Building und allen seinen Subtypen. Dies wird als Platzhalter für die obere Grenze bezeichnet, wobei Typ Building die obere Grenze ist.

Platzhalter können auch mit einer Untergrenze angegeben werden, wobei der unbekannte Typ ein Supertyp des angegebenen Typs sein muss. Untergrenzen können mit dem Schlüsselwort super gefolgt vom jeweiligen Typ angegeben werden, z.bedeutet unbekannter Typ, der eine Oberklasse von T ist (= T und alle seine Eltern).

5. Geben Sie Erasure ein

Java wurden Generika hinzugefügt, um die Typensicherheit zu gewährleisten und um sicherzustellen, dass Generika zur Laufzeit keinen Overhead verursachen. Der Compiler wendet zur Kompilierungszeit einen Prozess namens Typlöschung auf Generika an.

Die Typlöschung entfernt alle Typparameter und ersetzt sie durch ihre Grenzen oder durch Object, wenn der Typparameter unbegrenzt ist. Somit enthält der Bytecode nach der Kompilierung nur normale Klassen, Schnittstellen und Methoden, wodurch sichergestellt wird, dass keine neuen Typen erzeugt werden. Das richtige Casting wird beim Kompilieren auch auf den Objekttyp angewendet .

Dies ist ein Beispiel für das Löschen von Typen:

public  List genericMethod(List list) { return list.stream().collect(Collectors.toList()); } 

With type erasure, the unbounded type T is replaced with Object as follows:

// for illustration public List withErasure(List list) { return list.stream().collect(Collectors.toList()); } // which in practice results in public List withErasure(List list) { return list.stream().collect(Collectors.toList()); } 

If the type is bounded, then the type will be replaced by the bound at compile time:

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

would change after compilation:

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

6. Generics and Primitive Data Types

A restriction of generics in Java is that the type parameter cannot be a primitive type.

For example, the following doesn't compile:

List list = new ArrayList(); list.add(17);

To understand why primitive data types don't work, let's remember that generics are a compile-time feature, meaning the type parameter is erased and all generic types are implemented as type Object.

As an example, let's look at the add method of a list:

List list = new ArrayList(); list.add(17);

The signature of the add method is:

boolean add(E e);

And will be compiled to:

boolean add(Object e);

Therefore, type parameters must be convertible to Object. Since primitive types don't extend Object, we can't use them as type parameters.

However, Java provides boxed types for primitives, along with autoboxing and unboxing to unwrap them:

Integer a = 17; int b = a; 

So, if we want to create a list which can hold integers, we can use the wrapper:

List list = new ArrayList(); list.add(17); int first = list.get(0); 

The compiled code will be the equivalent of:

List list = new ArrayList(); list.add(Integer.valueOf(17)); int first = ((Integer) list.get(0)).intValue(); 

Future versions of Java might allow primitive data types for generics. Project Valhalla aims at improving the way generics are handled. The idea is to implement generics specialization as described in JEP 218.

7. Conclusion

Java Generics ist eine leistungsstarke Ergänzung der Java-Sprache, da es die Arbeit des Programmierers erleichtert und weniger fehleranfällig macht. Generika erzwingen die Typkorrektheit beim Kompilieren und ermöglichen vor allem die Implementierung generischer Algorithmen, ohne dass unsere Anwendungen zusätzlichen Aufwand verursachen.

Der dem Artikel beiliegende Quellcode ist auf GitHub verfügbar.