Generika in Java - Generics in Java

Generics sind eine Einrichtung der generischen Programmierung , die der Programmiersprache Java im Jahr 2004 in der Version J2SE 5.0 hinzugefügt wurde . Sie wurden entwickelt, um das Typsystem von Java zu erweitern , um "einem Typ oder einer Methode zu ermöglichen, mit Objekten verschiedener Typen zu arbeiten und gleichzeitig Typsicherheit zur Kompilierzeit zu bieten". Der Aspekt Typsicherheit zur Kompilierzeit wurde nicht vollständig erreicht, da sich 2016 gezeigt hat, dass er nicht in allen Fällen gewährleistet ist.

Das Java-Sammlungs-Framework unterstützt Generika, um den Typ von Objekten anzugeben, die in einer Sammlungsinstanz gespeichert sind.

1998 entwickelten Gilad Bracha , Martin Odersky , David Stoutamire und Philip Wadler Generic Java, eine Erweiterung der Java-Sprache, um generische Typen zu unterstützen. Generisches Java wurde mit dem Hinzufügen von Platzhaltern in Java integriert.

Hierarchie und Klassifizierung

Gemäß Java-Sprachspezifikation :

  • Eine Typvariable ist ein nicht qualifizierter Bezeichner. Typvariablen werden durch generische Klassendeklarationen, generische Schnittstellendeklarationen, generische Methodendeklarationen und durch generische Konstruktordeklarationen eingeführt.
  • Eine Klasse ist generisch, wenn sie eine oder mehrere Typvariablen deklariert. Diese Typvariablen werden als Typparameter der Klasse bezeichnet. Es definiert eine oder mehrere Typvariablen, die als Parameter fungieren. Eine generische Klassendeklaration definiert einen Satz parametrisierter Typen, einen für jeden möglichen Aufruf des Typparameterabschnitts. Alle diese parametrisierten Typen teilen sich zur Laufzeit dieselbe Klasse.
  • Eine Schnittstelle ist generisch, wenn sie eine oder mehrere Typvariablen deklariert. Diese Typvariablen werden als Typparameter der Schnittstelle bezeichnet. Es definiert eine oder mehrere Typvariablen, die als Parameter fungieren. Eine generische Schnittstellendeklaration definiert einen Satz von Typen, einen für jeden möglichen Aufruf des Typparameterabschnitts. Alle parametrisierten Typen teilen sich zur Laufzeit dieselbe Schnittstelle.
  • Eine Methode ist generisch, wenn sie eine oder mehrere Typvariablen deklariert. Diese Typvariablen werden als formale Typparameter der Methode bezeichnet. Die Form der formalen Typparameterliste ist identisch mit einer Typparameterliste einer Klasse oder eines Interfaces.
  • Ein Konstruktor kann als generisch deklariert werden, unabhängig davon, ob die Klasse, in der der Konstruktor deklariert ist, selbst generisch ist. Ein Konstruktor ist generisch, wenn er eine oder mehrere Typvariablen deklariert. Diese Typvariablen werden als formale Typparameter des Konstruktors bezeichnet. Die Form der formalen Typparameterliste ist identisch mit einer Typparameterliste einer generischen Klasse oder Schnittstelle.

Motivation

Der folgende Java-Codeblock veranschaulicht ein Problem, das auftritt, wenn Generika nicht verwendet werden. Zuerst wird ein ArrayListvom Typ deklariert Object. Dann fügt sie ein Stringzu dem ArrayList. Schließlich versucht es, das hinzugefügte abzurufen Stringund in ein Integer— ein Logikfehler umzuwandeln, da es im Allgemeinen nicht möglich ist, einen beliebigen String in eine ganze Zahl umzuwandeln.

List v = new ArrayList();
v.add("test"); // A String that cannot be cast to an Integer
Integer i = (Integer)v.get(0); // Run time error

Obwohl der Code fehlerfrei kompiliert wird, löst er java.lang.ClassCastExceptionbeim Ausführen der dritten Codezeile eine Laufzeitausnahme ( ) aus. Diese Art von Logikfehlern kann während der Kompilierung durch die Verwendung von Generika erkannt werden und ist die Hauptmotivation für deren Verwendung.

Das obige Codefragment kann mit Generics wie folgt umgeschrieben werden:

List<String> v = new ArrayList<String>();
v.add("test");
Integer i = (Integer)v.get(0); // (type error)  compilation-time error

Der type-Parameter Stringinnerhalb der spitzen Klammern deklariert ArrayList, dass der String(ein Nachkomme der ArrayListgenerischen ObjectKonstituenten von ) gebildet werden soll. Bei Generics ist es nicht mehr erforderlich, die dritte Zeile in einen bestimmten Typ umzuwandeln, da das Ergebnis von durch den vom Compiler generierten Code v.get(0)definiert Stringwird.

Die logischen Fehler in der dritten Zeile dieses Fragments werden als erkannt werden Compiler- Fehler (mit J2SE 5.0 oder höher) , weil der Compiler, der erkennt v.get(0)zurückkehrt , Stringstatt Integer. Ein ausführlicheres Beispiel finden Sie in der Referenz.

Hier ein kleiner Auszug aus der Definition der Schnittstellen Listund Iteratorim Paket java.util:

public interface List<E> { 
    void add(E x);
    Iterator<E> iterator();
}

public interface Iterator<E> { 
    E next();
    boolean hasNext();
}

Platzhalter eingeben

Ein Typargument für einen parametrisierten Typ ist nicht auf eine konkrete Klasse oder Schnittstelle beschränkt. Java ermöglicht die Verwendung von Typ-Platzhaltern als Typargumente für parametrisierte Typen. Platzhalter sind Typargumente in der Form " <?>"; wahlweise mit einem oberen oder unteren gebunden . Da der genaue Typ, der durch einen Platzhalter repräsentiert wird, unbekannt ist, werden Beschränkungen für den Methodentyp auferlegt, der für ein Objekt aufgerufen werden kann, das parametrisierte Typen verwendet.

Hier ist ein Beispiel, bei dem der Elementtyp von a Collection<E>durch einen Platzhalter parametrisiert wird:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // compile-time error
c.add(null); // allowed

Da wir nicht wissen, wofür der Elementtyp von csteht, können wir ihm keine Objekte hinzufügen. Die add()Methode akzeptiert Argumente vom Typ E, dem Elementtyp der Collection<E>generischen Schnittstelle. Wenn das tatsächliche Typargument ist ?, steht es für einen unbekannten Typ. Jeder Methodenargumentwert, den wir an die add()Methode übergeben, müsste ein Untertyp dieses unbekannten Typs sein. Da wir nicht wissen, um welchen Typ es sich handelt, können wir nichts übergeben. Die einzige Ausnahme ist null ; die ein Mitglied jedes Typs ist.

Um die obere Grenze eines Typ-Platzhalters anzugeben , wird das extendsSchlüsselwort verwendet, um anzugeben, dass das Typargument ein Untertyp der umgebenden Klasse ist. Das List<? extends Number>bedeutet, dass die angegebene Liste Objekte eines unbekannten Typs enthält, die die NumberKlasse erweitern. Die Liste könnte beispielsweise List<Float>oder lauten List<Number>. Das Lesen eines Elements aus der Liste gibt eine Number. Auch das Hinzufügen von Nullelementen ist erlaubt.

Die Verwendung von Platzhaltern oben erhöht die Flexibilität, da es keine Vererbungsbeziehung zwischen zwei beliebigen parametrisierten Typen mit konkretem Typ als Typargument gibt. Weder List<Number>noch List<Integer>ist ein Untertyp des anderen; obwohl Integerist ein Untertyp von Number. Daher List<Number>akzeptiert jede Methode, die als Parameter verwendet wird, kein Argument von List<Integer>. Wenn dies der Fall wäre, wäre es möglich, a einzufügen Number, das kein a ist Integer; was gegen die Typsicherheit verstößt. Hier ist ein Beispiel, das zeigt, wie die Typsicherheit verletzt würde, wenn List<Integer>es ein Untertyp von wäre List<Number>:

List<Integer> ints = new ArrayList<Integer>();
ints.add(2);
List<Number> nums = ints;  // valid if List<Integer> were a subtype of List<Number> according to substitution rule. 
nums.add(3.14);  
Integer x = ints.get(1); // now 3.14 is assigned to an Integer variable!

Die Lösung mit Platzhaltern funktioniert, weil sie Operationen verbietet, die die Typsicherheit verletzen würden:

List<? extends Number> nums = ints;  // OK
nums.add(3.14); // compile-time error
nums.add(null); // allowed

Um die untere Begrenzungsklasse eines Typ-Platzhalters anzugeben, wird das superSchlüsselwort verwendet. Dieses Schlüsselwort gibt an, dass das Typargument ein Supertyp der umgebenden Klasse ist. Könnte also List<? super Number>darstellen List<Number>oder List<Object>. Lesen aus einer Liste, die als List<? super Number>Rückgabeelemente des Typs definiert ist Object. Das Hinzufügen zu einer solchen Liste erfordert entweder Elemente vom Typ Number, einen beliebigen Untertyp von Numberoder null (das Mitglied jedes Typs ist).

Das mnemonische PECS (Producer Extends, Consumer Super) aus dem Buch Effective Java von Joshua Bloch bietet eine einfache Möglichkeit, sich daran zu erinnern, wann Wildcards (entsprechend Kovarianz und Kontravarianz ) in Java verwendet werden müssen.

Generische Klassendefinitionen

Hier ist ein Beispiel für eine generische Java-Klasse, die verwendet werden kann, um einzelne Einträge (Schlüssel-zu-Wert-Zuordnungen) in einer Karte darzustellen :

public class Entry<KeyType, ValueType> {
  
    private final KeyType key;
    private final ValueType value;

    public Entry(KeyType key, ValueType value) {  
        this.key = key;
        this.value = value;
    }

    public KeyType getKey() {
        return key;
    }

    public ValueType getValue() {
        return value;
    }

    public String toString() { 
        return "(" + key + ", " + value + ")";  
    }

}

Diese generische Klasse könnte beispielsweise auf folgende Weise verwendet werden:

Entry<String, String> grade = new Entry<String, String>("Mike", "A");
Entry<String, Integer> mark = new Entry<String, Integer>("Mike", 100);
System.out.println("grade: " + grade);
System.out.println("mark: " + mark);

Entry<Integer, Boolean> prime = new Entry<Integer, Boolean>(13, true);
if (prime.getValue()) System.out.println(prime.getKey() + " is prime.");
else System.out.println(prime.getKey() + " is not prime.");

Es gibt aus:

grade: (Mike, A)
mark: (Mike, 100)
13 is prime.

Diamant-Betreiber

Dank der Typinferenz ermöglichen Java SE 7 und höher dem Programmierer, ein leeres Paar spitzer Klammern ( <>, Diamantoperator genannt ) durch ein Paar spitzer Klammern zu ersetzen , die den einen oder die mehreren Typparameter enthalten, die ein hinreichend enger Kontext impliziert . Somit kann das obige Codebeispiel using Entryumgeschrieben werden als:

Entry<String, String> grade = new Entry<>("Mike", "A");
Entry<String, Integer> mark = new Entry<>("Mike", 100);
System.out.println("grade: " + grade);
System.out.println("mark: " + mark);

Entry<Integer, Boolean> prime = new Entry<>(13, true);
if (prime.getValue()) System.out.println(prime.getKey() + " is prime.");
else System.out.println(prime.getKey() + " is not prime.");

Generische Methodendefinitionen

Hier ist ein Beispiel für eine generische Methode, die die obige generische Klasse verwendet:

public static <Type> Entry<Type, Type> twice(Type value) {
    return new Entry<Type, Type>(value, value);
}

Hinweis: Wenn wir die erste <Type>in der obigen Methode entfernen , erhalten wir einen Kompilierungsfehler (kann das Symbol "Typ" nicht finden), da es die Deklaration des Symbols darstellt.

In vielen Fällen muss der Benutzer der Methode die Typparameter nicht angeben, da diese abgeleitet werden können:

Entry<String, String> pair = Entry.twice("Hello");

Die Parameter können bei Bedarf explizit hinzugefügt werden:

Entry<String, String> pair = Entry.<String>twice("Hello");

Die Verwendung von primitiven Typen ist nicht zulässig, stattdessen müssen Boxed- Versionen verwendet werden:

Entry<int, int> pair; // Fails compilation. Use Integer instead.

Es besteht auch die Möglichkeit, generische Methoden basierend auf vorgegebenen Parametern zu erstellen.

public <Type> Type[] toArray(Type... elements) {
    return elements;
}

In solchen Fällen können Sie auch keine primitiven Typen verwenden, zB:

Integer[] array = toArray(1, 2, 3, 4, 5, 6);

Generika in der Würfe-Klausel

Obwohl Ausnahmen selbst nicht generisch sein können, können generische Parameter in einer throws-Klausel vorkommen:

public <T extends Throwable> void throwMeConditional(boolean conditional, T exception) throws T {
    if (conditional) {
        throw exception;
    }
}

Probleme mit der Typlöschung

Generics werden zur Kompilierzeit auf Typkorrektheit überprüft. Die generischen Typinformationen werden dann in einem Prozess namens Typlöschung entfernt . Beispielsweise List<Integer>wird in den nicht generischen Typ konvertiert List, der normalerweise beliebige Objekte enthält. Die Überprüfung zur Kompilierzeit garantiert, dass der resultierende Code typkorrekt ist.

Aufgrund der Typlöschung können Typparameter nicht zur Laufzeit bestimmt werden. Wenn beispielsweise ein ArrayListzur Laufzeit untersucht wird, gibt es keine allgemeine Möglichkeit, festzustellen, ob es vor dem Löschen des Typs ein ArrayList<Integer>oder ein ArrayList<Float>. Viele Menschen sind mit dieser Einschränkung unzufrieden. Es gibt Teilansätze. Zum Beispiel können einzelne Elemente untersucht werden, um den Typ zu bestimmen, zu dem sie gehören; Wenn beispielsweise an einen ArrayListenthält Integer, kann ArrayList mit parametrisiert worden sein Integer(jedoch kann es mit einem beliebigen Elternteil von parametrisiert worden sein Integer, z. B. Numberoder Object).

Um diesen Punkt zu demonstrieren, gibt der folgende Code "Equal" aus:

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if (li.getClass() == lf.getClass()) { // evaluates to true
    System.out.println("Equal");
}

Ein weiterer Effekt der Typlöschung besteht darin, dass eine generische Klasse die Throwable-Klasse in keiner Weise direkt oder indirekt erweitern kann:

public class GenericException<T> extends Exception

Der Grund, warum dies nicht unterstützt wird, liegt an der Typlöschung:

try {
    throw new GenericException<Integer>();
}
catch(GenericException<Integer> e) {
    System.err.println("Integer");
}
catch(GenericException<String> e) {
    System.err.println("String");
}

Aufgrund der Typlöschung weiß die Laufzeit nicht, welcher Catch-Block ausgeführt werden soll, daher wird dies vom Compiler untersagt.

Java-Generika unterscheiden sich von C++-Vorlagen . Java-Generika generieren unabhängig von der Anzahl der verwendeten Parametrisierungstypen nur eine kompilierte Version einer generischen Klasse oder Funktion. Darüber hinaus muss die Java-Laufzeitumgebung nicht wissen, welcher parametrisierte Typ verwendet wird, da die Typinformationen zur Kompilierzeit validiert werden und nicht im kompilierten Code enthalten sind. Folglich ist die Instanziierung einer Java-Klasse eines parametrisierten Typs unmöglich, da die Instanziierung einen Aufruf eines Konstruktors erfordert, der bei unbekanntem Typ nicht verfügbar ist.

Der folgende Code kann beispielsweise nicht kompiliert werden:

<T> T instantiateElementType(List<T> arg) {
     return new T(); //causes a compile error
}

Da es zur Laufzeit nur eine Kopie pro generische Klasse gibt, werden statische Variablen unabhängig von ihrem Typparameter von allen Instanzen der Klasse gemeinsam genutzt. Folglich kann der Typparameter nicht in der Deklaration von statischen Variablen oder in statischen Methoden verwendet werden.

Projekt zu Generika

Projekt Valhalla ist ein experimentelles Projekt zur Inkubation verbesserter Java-Generika und Sprachfunktionen für zukünftige Versionen möglicherweise ab Java 10. Mögliche Verbesserungen umfassen:

Siehe auch

Verweise

  1. ^ Java-Programmiersprache
  2. ^ Eine ClassCastException kann auch ohne Casts oder Nullen ausgelöst werden. "Java und Scala's Type Systems are Unsound" (PDF) .
  3. ^ GJ: Generisches Java
  4. ^ Java Language Specification, Dritte Ausgabe von James Gosling, Bill Joy, Guy Steele, Gilad Bracha – Prentice Hall PTR 2005
  5. ^ Gilad Bracha (5. Juli 2004). "Generika in der Programmiersprache Java" (PDF) . www.oracle.com .
  6. ^ Gilad Bracha (5. Juli 2004). "Generika in der Programmiersprache Java" (PDF) . www.oracle.com . P. 5.
  7. ^ Bracha, Gilad . "Platzhalter > Bonus > Generika" . Die Java™-Tutorials . Orakel. ...Die einzige Ausnahme ist null, die ein Mitglied jedes Typs ist...
  8. ^ http://docs.oracle.com/javase/7/docs/technotes/guides/language/type-inference-generic-instance-creation.html
  9. ^ Gafter, Neal (2006-11-05). "Reified Generics für Java" . Abgerufen 2010-04-20 .
  10. ^ "Java-Sprachspezifikation, Abschnitt 8.1.2" . Orakel . Abgerufen am 24. Oktober 2015 .
  11. ^ Götz, Brian. "Willkommen in Walhalla!" . OpenJDK-Mailarchiv . OpenJDK . Abgerufen am 12. August 2014 .