Java Language
Generics
Suche…
Einführung
Generics sind eine Möglichkeit der generischen Programmierung, die das Java-Typsystem erweitert, um es einem Typ oder einer Methode zu ermöglichen, Objekte verschiedener Typen zu bearbeiten und gleichzeitig die Sicherheit der Kompilierzeit zu gewährleisten. Insbesondere unterstützt das Java Collections Framework Generics, um den Typ von Objekten anzugeben, die in einer Collection-Instanz gespeichert sind.
Syntax
- class ArrayList <E> {} // eine generische Klasse mit dem Typparameter E
- class HashMap <K, V> {} // eine generische Klasse mit zwei Typparametern K und V
- <E> void print (E-Element) {} // eine generische Methode mit dem Typparameter E
- ArrayList <String> Namen; // Deklaration einer generischen Klasse
- ArrayList <?> Objekte; // Deklaration einer generischen Klasse mit einem unbekannten Typparameter
- new ArrayList <String> () // Instantiierung einer generischen Klasse
- neue ArrayList <> () // Instantiierung mit Typinferenz "Raute" (Java 7 oder höher)
Bemerkungen
Generics werden in Java durch Typlöschung implementiert. Dies bedeutet, dass zur Laufzeit die in der Instanziierung einer generischen Klasse angegebenen Typinformationen nicht verfügbar sind. Zum Beispiel die Anweisung List<String> names = new ArrayList<>();
erzeugt ein Listenobjekt, aus dem der Elementtyp String
zur Laufzeit nicht wiederhergestellt werden kann. Wenn jedoch die Liste in einem Feld vom Typ gespeichert ist List<String>
, oder ein Verfahren / Konstruktorparameter des gleichen Typs übergeben oder von einem Verfahren dieser Rückgabetyp zurückgegeben, dann ist die vollständige Typinformationen zur Laufzeit zurückgewonnen werden über die Java Reflection-API.
Dies bedeutet auch, dass beim Casting auf einen generischen Typ (z. B. (List<String>) list
) der Cast ein ungeprüfter Cast ist . Da der Parameter <String>
gelöscht wird, kann die JVM nicht prüfen, ob eine Umwandlung von einer List<?>
eine List<String>
korrekt ist. Die JVM sieht zur Laufzeit nur einen Cast für List
to List
.
Generische Klasse erstellen
Generics ermöglichen Klassen, Schnittstellen und Methoden, andere Klassen und Schnittstellen als Typparameter zu verwenden.
In diesem Beispiel wird die generische Klasse Param
, um einen einzelnen Typparameter T
, der durch spitze Klammern ( <>
) getrennt ist:
public class Param<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Geben Sie zum Instanziieren dieser Klasse ein Typargument anstelle von T
. Zum Beispiel Integer
:
Param<Integer> integerParam = new Param<Integer>();
Das Typargument kann ein beliebiger Referenztyp sein, einschließlich Arrays und andere generische Typen:
Param<String[]> stringArrayParam;
Param<int[][]> int2dArrayParam;
Param<Param<Object>> objectNestedParam;
In Java SE 7 und höher kann das Typargument durch einen leeren Satz von Typargumenten ( <>
) ersetzt werden, der als Raute bezeichnet wird :
Param<Integer> integerParam = new Param<>();
Im Gegensatz zu anderen Bezeichnern haben Typparameter keine Namensbeschränkungen. Ihre Namen sind jedoch in der Regel der erste Buchstabe ihres Zwecks in Großbuchstaben. (Dies gilt sogar für alle offiziellen JavaDocs.)
Beispiele sind T
für "Typ" , E
für "Element" und K
/ V
für "Schlüssel" / "Wert" .
Eine generische Klasse erweitern
public abstract class AbstractParam<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
AbstractParam
ist eine abstrakte Klasse , die mit einem Typparameter von T
deklariert wird. Beim Erweitern dieser Klasse kann dieser Typparameter durch ein in <>
geschriebenes <>
, oder der Typparameter kann unverändert bleiben. In den ersten und zweiten Beispielen ersetzen String
und Integer
den Parameter type. Im dritten Beispiel bleibt der Typparameter unverändert. Im vierten Beispiel werden keine Generics verwendet, es ist also ähnlich, wenn die Klasse einen Object
Parameter hätte. Der Compiler warnt davor, dass AbstractParam
ein Raw-Typ ist, kompiliert jedoch die ObjectParam
Klasse. Das fünfte Beispiel verfügt über 2 Typparameter (siehe "Multiple Type-Parameter" weiter unten), wobei der zweite Parameter als der an die Oberklasse übergebene Typparameter ausgewählt wird.
public class Email extends AbstractParam<String> {
// ...
}
public class Age extends AbstractParam<Integer> {
// ...
}
public class Height<T> extends AbstractParam<T> {
// ...
}
public class ObjectParam extends AbstractParam {
// ...
}
public class MultiParam<T, E> extends AbstractParam<E> {
// ...
}
Folgendes ist die Verwendung:
Email email = new Email();
email.setValue("[email protected]");
String retrievedEmail = email.getValue();
Age age = new Age();
age.setValue(25);
Integer retrievedAge = age.getValue();
int autounboxedAge = age.getValue();
Height<Integer> heightInInt = new Height<>();
heightInInt.setValue(125);
Height<Float> heightInFloat = new Height<>();
heightInFloat.setValue(120.3f);
MultiParam<String, Double> multiParam = new MultiParam<>();
multiParam.setValue(3.3);
Beachten Sie, dass die T getValue()
-Methode in der Email
Klasse so T getValue()
als hätte sie eine Signatur von String getValue()
und die void setValue(T)
-Methode so, als wäre sie für void setValue(String)
deklariert worden.
Es ist auch möglich, mit anonymen inneren Klassen mit leeren geschweiften Klammern ( {}
) zu instanziieren:
AbstractParam<Double> height = new AbstractParam<Double>(){};
height.setValue(198.6);
Beachten Sie, dass die Verwendung des Diamanten mit anonymen inneren Klassen nicht zulässig ist.
Mehrere Typparameter
Java bietet die Möglichkeit, mehrere Typparameter in einer generischen Klasse oder Schnittstelle zu verwenden. Mehrere Typparameter können in einer Klasse oder einem Interface verwendet werden, indem Sie eine durch Kommas getrennte Liste von Typen in die spitzen Klammern setzen. Beispiel:
public class MultiGenericParam<T, S> {
private T firstParam;
private S secondParam;
public MultiGenericParam(T firstParam, S secondParam) {
this.firstParam = firstParam;
this.secondParam = secondParam;
}
public T getFirstParam() {
return firstParam;
}
public void setFirstParam(T firstParam) {
this.firstParam = firstParam;
}
public S getSecondParam() {
return secondParam;
}
public void setSecondParam(S secondParam) {
this.secondParam = secondParam;
}
}
Die Verwendung kann wie folgt erfolgen:
MultiGenericParam<String, String> aParam = new MultiGenericParam<String, String>("value1", "value2");
MultiGenericParam<Integer, Double> dayOfWeekDegrees = new MultiGenericParam<Integer, Double>(1, 2.6);
Eine generische Methode deklarieren
Methoden können auch generische Typparameter haben.
public class Example {
// The type parameter T is scoped to the method
// and is independent of type parameters of other methods.
public <T> List<T> makeList(T t1, T t2) {
List<T> result = new ArrayList<T>();
result.add(t1);
result.add(t2);
return result;
}
public void usage() {
List<String> listString = makeList("Jeff", "Atwood");
List<Integer> listInteger = makeList(1, 2);
}
}
Beachten Sie, dass wir kein tatsächliches Typargument an eine generische Methode übergeben müssen. Der Compiler leitet das Typargument für uns ein, basierend auf dem Zieltyp (z. B. der Variablen, der wir das Ergebnis zuweisen) oder den Typen der tatsächlichen Argumente. In der Regel wird auf das spezifischste Typargument geschlossen, das den Aufruftypkorrigiert.
Manchmal, wenn auch selten, kann es notwendig sein, diese Typeninferenz mit expliziten Typargumenten zu überschreiben:
void usage() {
consumeObjects(this.<Object>makeList("Jeff", "Atwood").stream());
}
void consumeObjects(Stream<Object> stream) { ... }
Dies ist in diesem Beispiel notwendig, da der Compiler nach dem Aufruf von stream()
nicht nach vorne schauen kann, um zu sehen, dass Object
für T
erwünscht ist, und dass andernfalls String
basierend auf den makeList
Argumenten abgeleitet würde. Beachten Sie, dass die Java - Sprache nicht unterstützt , die Klasse oder das Objekt Weglassen , auf dem das Verfahren (genannt wird this
in dem obigen Beispiel) , wenn Typargumente explizit vorgesehen sind.
Der Diamant
Java 7 führte den Diamond 1 ein , um einige Boilerplatten für die generische Klasseninstanziierung zu entfernen. Mit Java 7+ können Sie schreiben:
List<String> list = new LinkedList<>();
Wo Sie in früheren Versionen schreiben mussten:
List<String> list = new LinkedList<String>();
Eine Einschränkung gilt für anonyme Klassen , bei denen Sie den type-Parameter in der Instanziierung noch angeben müssen:
// This will compile:
Comparator<String> caseInsensitiveComparator = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
};
// But this will not:
Comparator<String> caseInsensitiveComparator = new Comparator<>() {
@Override
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
};
Obwohl die Raute mit anonymen inneren Klassen in Java 7 und 8 nicht unterstützt wird, wird sie als neue Funktion in Java 9 aufgenommen .
Fußnote:
1 - Manche Leute nennen das <>
Nutzung der „Diamant - Operator“. Das ist falsch. Der Diamant verhält sich nicht wie ein Operator und wird nicht als Operator in der JLS oder den (offiziellen) Java-Tutorials beschrieben. In der Tat ist <>
kein eindeutiges Java-Token. Es ist eher ein <
Token, gefolgt von einem >
Token, und es ist legal (wenn auch schlecht), Leerzeichen oder Kommentare zwischen den beiden zu haben. Das JLS und die Tutorials beziehen sich stets auf <>
als "den Diamanten", und das ist daher der richtige Begriff dafür.
Mehrere obere Grenzen erforderlich ("erweitert A & B")
Sie können einen generischen Typ benötigen, um mehrere obere Grenzen zu erweitern.
Beispiel: Wir möchten eine Liste von Zahlen sortieren, aber Number
implementiert Comparable
.
public <T extends Number & Comparable<T>> void sortNumbers( List<T> n ) {
Collections.sort( n );
}
In diesem Beispiel muss T
die Number
und Comparable<T>
implementieren, die in alle "normalen" integrierten Striped64
wie Integer
oder BigDecimal
passen sollte, nicht jedoch für exotischere wie Striped64
.
Da Mehrfachvererbung nicht zulässig ist, können Sie höchstens eine Klasse als Begrenzung verwenden. Es muss die zuerst aufgelistete Klasse sein. Beispielsweise ist <T extends Comparable<T> & Number>
nicht zulässig, da Comparable eine Schnittstelle und keine Klasse ist.
Erstellen einer begrenzten generischen Klasse
Sie können die gültigen Typen einschränken, die in einer generischen Klasse verwendet werden, indem Sie diesen Typ in der Klassendefinition begrenzen. In Anbetracht der folgenden einfachen Typhierarchie:
public abstract class Animal {
public abstract String getSound();
}
public class Cat extends Animal {
public String getSound() {
return "Meow";
}
}
public class Dog extends Animal {
public String getSound() {
return "Woof";
}
}
Ohne beschränkte Generika können wir keine Containerklasse erstellen, die sowohl generisch ist als auch weiß, dass jedes Element ein Tier ist:
public class AnimalContainer<T> {
private Collection<T> col;
public AnimalContainer() {
col = new ArrayList<T>();
}
public void add(T t) {
col.add(t);
}
public void printAllSounds() {
for (T t : col) {
// Illegal, type T doesn't have makeSound()
// it is used as an java.lang.Object here
System.out.println(t.makeSound());
}
}
}
Mit generisch gebundenen Klassendefinitionen ist dies jetzt möglich.
public class BoundedAnimalContainer<T extends Animal> { // Note bound here.
private Collection<T> col;
public BoundedAnimalContainer() {
col = new ArrayList<T>();
}
public void add(T t) {
col.add(t);
}
public void printAllSounds() {
for (T t : col) {
// Now works because T is extending Animal
System.out.println(t.makeSound());
}
}
}
Dies beschränkt auch die gültigen Instanziierungen des generischen Typs:
// Legal
AnimalContainer<Cat> a = new AnimalContainer<Cat>();
// Legal
AnimalContainer<String> a = new AnimalContainer<String>();
// Legal because Cat extends Animal
BoundedAnimalContainer<Cat> b = new BoundedAnimalContainer<Cat>();
// Illegal because String doesn't extends Animal
BoundedAnimalContainer<String> b = new BoundedAnimalContainer<String>();
Entscheidung zwischen "T", "?" Super T` und `? verlängert T`
Die Syntax für Java-Generics beschränkt Wildcards, die den unbekannten Typ durch ?
ist:
? extends T
für einen Platzhalter mit Oberbegrenzung. Der unbekannte Typ steht für einen Typ, der ein Subtyp von T oder T sein muss.? super T
für einen Platzhalter mit unterer Begrenzung. Der unbekannte Typ steht für einen Typ, der ein Supertyp von T oder T sein muss.
Als Faustregel sollten Sie verwenden
-
? extends T
wenn Sie nur Lesezugriff benötigen ("Eingabe") -
? super T
wenn Sie "Schreibzugriff" benötigen ("Ausgabe") -
T
wenn Sie beides benötigen ("ändern")
Die Verwendung von extends
oder super
ist in der Regel besser, da der Code dadurch flexibler wird (z. B. die Verwendung von Subtypen und Supertypen), wie Sie unten sehen werden.
class Shoe {}
class IPhone {}
interface Fruit {}
class Apple implements Fruit {}
class Banana implements Fruit {}
class GrannySmith extends Apple {}
public class FruitHelper {
public void eatAll(Collection<? extends Fruit> fruits) {}
public void addApple(Collection<? super Apple> apples) {}
}
Der Compiler kann nun eine bestimmte fehlerhafte Verwendung erkennen:
public class GenericsTest {
public static void main(String[] args){
FruitHelper fruitHelper = new FruitHelper() ;
List<Fruit> fruits = new ArrayList<Fruit>();
fruits.add(new Apple()); // Allowed, as Apple is a Fruit
fruits.add(new Banana()); // Allowed, as Banana is a Fruit
fruitHelper.addApple(fruits); // Allowed, as "Fruit super Apple"
fruitHelper.eatAll(fruits); // Allowed
Collection<Banana> bananas = new ArrayList<>();
bananas.add(new Banana()); // Allowed
//fruitHelper.addApple(bananas); // Compile error: may only contain Bananas!
fruitHelper.eatAll(bananas); // Allowed, as all Bananas are Fruits
Collection<Apple> apples = new ArrayList<>();
fruitHelper.addApple(apples); // Allowed
apples.add(new GrannySmith()); // Allowed, as this is an Apple
fruitHelper.eatAll(apples); // Allowed, as all Apples are Fruits.
Collection<GrannySmith> grannySmithApples = new ArrayList<>();
fruitHelper.addApple(grannySmithApples); //Compile error: Not allowed.
// GrannySmith is not a supertype of Apple
apples.add(new GrannySmith()); //Still allowed, GrannySmith is an Apple
fruitHelper.eatAll(grannySmithApples);//Still allowed, GrannySmith is a Fruit
Collection<Object> objects = new ArrayList<>();
fruitHelper.addApple(objects); // Allowed, as Object super Apple
objects.add(new Shoe()); // Not a fruit
objects.add(new IPhone()); // Not a fruit
//fruitHelper.eatAll(objects); // Compile error: may contain a Shoe, too!
}
Wahl des richtigen T
? super T
oder ? extends T
ist notwendig , um die Verwendung mit Untertypen zu ermöglichen. Der Compiler kann dann die Typsicherheit gewährleisten; Sie sollten keine Umwandlung durchführen (was nicht typsicher ist und Programmierfehler verursachen kann), wenn Sie sie ordnungsgemäß verwenden.
Wenn es nicht leicht zu verstehen ist, denken Sie bitte an die PECS- Regel:
Der Produzent verwendet " E xtends" und der C onsumer " S uper".
(Der Produzent hat nur Schreibzugriff und Consumer hat nur Lesezugriff.)
Vorteile der generischen Klasse und Schnittstelle
Code, der Generics verwendet, hat viele Vorteile gegenüber nicht generischem Code. Nachfolgend sind die wichtigsten Vorteile aufgeführt
Stärkere Typprüfungen zur Kompilierzeit
Ein Java-Compiler führt eine starke Typüberprüfung für generischen Code durch und gibt Fehler aus, wenn der Code die Typsicherheit verletzt. Das Beheben von Fehlern bei der Kompilierung ist einfacher als das Beheben von Laufzeitfehlern, die schwer zu finden sind.
Eliminierung von Abgüssen
Das folgende Code-Snippet ohne Generics erfordert ein Casting:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
Wenn der Code für die Verwendung von Generics umgeschrieben wurde, ist kein Casting erforderlich:
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // no cast
Programmierern die Implementierung generischer Algorithmen ermöglichen
Durch die Verwendung von Generics können Programmierer generische Algorithmen implementieren, die mit Auflistungen verschiedener Typen arbeiten, anpassbar sind und typsicher sind und einfacher zu lesen sind.
Generischer Parameter an mehr als einen Typ binden
Generische Parameter können auch an mehrere Typen gebunden werden, indem die Syntax T extends Type1 & Type2 & ...
.
Closeable
, Sie möchten eine Klasse erstellen, deren generischer Typ sowohl Flushable
als auch Closeable
implementieren Closeable
. Sie können schreiben
class ExampleClass<T extends Flushable & Closeable> {
}
Nun akzeptiert die ExampleClass
nur generische Parameter, die sowohl Flushable
als auch Closeable
.
ExampleClass<BufferedWriter> arg1; // Works because BufferedWriter implements both Flushable and Closeable
ExampleClass<Console> arg4; // Does NOT work because Console only implements Flushable
ExampleClass<ZipFile> arg5; // Does NOT work because ZipFile only implements Closeable
ExampleClass<Flushable> arg2; // Does NOT work because Closeable bound is not satisfied.
ExampleClass<Closeable> arg3; // Does NOT work because Flushable bound is not satisfied.
Die Klassenmethoden können generische Closeable
entweder als Closeable
oder Flushable
.
class ExampleClass<T extends Flushable & Closeable> {
/* Assign it to a valid type as you want. */
public void test (T param) {
Flushable arg1 = param; // Works
Closeable arg2 = param; // Works too.
}
/* You can even invoke the methods of any valid type directly. */
public void test2 (T param) {
param.flush(); // Method of Flushable called on T and works fine.
param.close(); // Method of Closeable called on T and works fine too.
}
}
Hinweis:
Sie können den generischen Parameter nicht mit der OR ( |
) - Klausel an einen der Typen binden. Es wird nur die AND ( &
) - Klausel unterstützt. Der generische Typ kann nur eine Klasse und viele Schnittstellen erweitern. Die Klasse muss am Anfang der Liste stehen.
Instanziieren eines generischen Typs
Aufgrund der Typenlöschung funktioniert Folgendes nicht:
public <T> void genericMethod() {
T t = new T(); // Can not instantiate the type T.
}
Der Typ T
wird gelöscht. Da die JVM zur Laufzeit nicht weiß, was T
ursprünglich war, weiß sie nicht, welchen Konstruktor sie aufrufen soll.
Problemumgehungen
Übergeben der Klasse
T
beim Aufruf vongenericMethod
:public <T> void genericMethod(Class<T> cls) { try { T t = cls.newInstance(); } catch (InstantiationException | IllegalAccessException e) { System.err.println("Could not instantiate: " + cls.getName()); } }
genericMethod(String.class);
Gibt Ausnahmen aus, da nicht bekannt ist, ob die übergebene Klasse einen Standardkonstruktor für den Zugriff hat.
Übergabe einer Referenz an den Konstruktor von
T
:public <T> void genericMethod(Supplier<T> cons) { T t = cons.get(); }
genericMethod(String::new);
Verweist auf den deklarierten generischen Typ in seiner eigenen Deklaration
Wie gehen Sie vor, wenn Sie eine Instanz eines (möglicherweise weiteren) geerbten generischen Typs innerhalb einer Methodendeklaration im generischen Typ selbst verwenden, der gerade deklariert wird? Dies ist eines der Probleme, mit denen Sie konfrontiert werden, wenn Sie ein wenig tiefer in die Generika einsteigen, aber immer noch ein häufiges Problem.
Angenommen, wir haben einen DataSeries<T>
-Typ (Schnittstelle hier), der eine generische Datenreihe definiert, die Werte des Typs T
. Es ist umständlich, direkt mit diesem Typ zu arbeiten, wenn viele Vorgänge mit z. B. Doppelwerten ausgeführt werden sollen. DoubleSeries extends DataSeries<Double>
definieren wir DoubleSeries extends DataSeries<Double>
. DataSeries<T>
wir nun an, der ursprüngliche DataSeries<T>
Typ DataSeries<T>
hat eine Methode add(values)
die eine weitere Reihe derselben Länge hinzufügt und eine neue zurückgibt. Wie können wir die Art der Durchsetzung values
und die Art der Rückkehr in seine DoubleSeries
anstatt DataSeries<Double>
in unserer abgeleiteten Klasse?
Das Problem kann gelöst werden, indem ein generischer Typparameter hinzugefügt wird, der sich auf den deklarierten Typ bezieht (dieser wird hier auf ein Interface angewendet, gilt jedoch für Klassen):
public interface DataSeries<T, DS extends DataSeries<T, DS>> {
DS add(DS values);
List<T> data();
}
Hier steht T
für den Datentyp der Serie, z. B. Double
und DS
die Serie selbst. Ein vererbte Typ (oder Typen) kann nun leicht durch Substitution des oben genannten Parameters durch eine entsprechende abgeleiteten Art implementiert werden, also eine konkrete Nachgeben Double
-basierte Definition der Form:
public interface DoubleSeries extends DataSeries<Double, DoubleSeries> {
static DoubleSeries instance(Collection<Double> data) {
return new DoubleSeriesImpl(data);
}
}
In diesem Moment implementiert sogar eine IDE die obige Schnittstelle mit den richtigen Typen, die nach einiger Inhaltsfüllung folgendermaßen aussehen können:
class DoubleSeriesImpl implements DoubleSeries {
private final List<Double> data;
DoubleSeriesImpl(Collection<Double> data) {
this.data = new ArrayList<>(data);
}
@Override
public DoubleSeries add(DoubleSeries values) {
List<Double> incoming = values != null ? values.data() : null;
if (incoming == null || incoming.size() != data.size()) {
throw new IllegalArgumentException("bad series");
}
List<Double> newdata = new ArrayList<>(data.size());
for (int i = 0; i < data.size(); i++) {
newdata.add(this.data.get(i) + incoming.get(i)); // beware autoboxing
}
return DoubleSeries.instance(newdata);
}
@Override
public List<Double> data() {
return Collections.unmodifiableList(data);
}
}
Wie Sie sehen, ist die add
Methode als DoubleSeries add(DoubleSeries values)
und der Compiler ist zufrieden.
Das Muster kann bei Bedarf weiter verschachtelt werden.
Verwendung von instanceof mit Generics
Generics verwenden, um den Typ in instanceof zu definieren
Betrachten Sie die folgende generische Klasse Example
, die mit dem Formalparameter <T>
deklariert wurde:
class Example<T> {
public boolean isTypeAString(String s) {
return s instanceof T; // Compilation error, cannot use T as class type here
}
}
Dies führt immer zu einem Kompilierungsfehler, da der Compiler, sobald er die Java-Quelle in Java-Bytecode kompiliert, einen als Typ Erasure bezeichneten Prozess anwendet, der den generischen Code in nicht generischen Code umwandelt, so dass die T-Typen zur Laufzeit nicht unterschieden werden können. Der mit instanceof
verwendete Typ muss wieder verwendbar sein . Das bedeutet, dass alle Informationen zum Typ zur Laufzeit verfügbar sein müssen. Dies ist bei generischen Typen normalerweise nicht der Fall.
Die folgende Klasse stellt dar, wie zwei verschiedene Klassen von Example
, Example<String>
und Example<Number>
aussehen, nachdem Generics durch Typlöschung entfernt wurden :
class Example { // formal parameter is gone
public boolean isTypeAString(String s) {
return s instanceof Object; // Both <String> and <Number> are now Object
}
}
Da Typen weg sind, kann die JVM nicht wissen, welcher Typ T
.
Ausnahme zur vorherigen Regel
Sie können einen unbegrenzten Platzhalter (?) Immer verwenden, um einen Typ in der instanceof
wie folgt anzugeben:
public boolean isAList(Object obj) {
return obj instanceof List<?>;
}
Dies kann nützlich sein, um auszuwerten, ob eine Instanz obj
eine List
oder nicht:
System.out.println(isAList("foo")); // prints false
System.out.println(isAList(new ArrayList<String>()); // prints true
System.out.println(isAList(new ArrayList<Float>()); // prints true
Unbegrenzte Wildcards werden tatsächlich als wiederverwendbare Typen betrachtet.
Verwenden einer generischen Instanz mit instanceof
Die andere Seite der Münze ist, dass die Verwendung einer Instanz t
von T
mit instanceof
legal ist, wie im folgenden Beispiel gezeigt:
class Example<T> {
public boolean isTypeAString(T t) {
return t instanceof String; // No compilation error this time
}
}
denn nach der Typlöschung sieht die Klasse wie folgt aus:
class Example { // formal parameter is gone
public boolean isTypeAString(Object t) {
return t instanceof String; // No compilation error this time
}
}
Da die JVM, selbst wenn das Löschen des Typs trotzdem auftritt, jetzt verschiedene Typen im Speicher unterscheiden kann, auch wenn sie denselben Referenztyp ( Object
) verwenden, wie der folgende Ausschnitt zeigt:
Object obj1 = new String("foo"); // reference type Object, object type String
Object obj2 = new Integer(11); // reference type Object, object type Integer
System.out.println(obj1 instanceof String); // true
System.out.println(obj2 instanceof String); // false, it's an Integer, not a String
Verschiedene Möglichkeiten zur Implementierung einer generischen Schnittstelle (oder zur Erweiterung einer generischen Klasse)
Angenommen, die folgende generische Schnittstelle wurde deklariert:
public interface MyGenericInterface<T> {
public void foo(T t);
}
Nachfolgend sind die möglichen Implementierungsmöglichkeiten aufgeführt.
Nicht-generische Klassenimplementierung mit einem bestimmten Typ
Wählen Sie einen bestimmten Typ aus, um den MyGenericClass
Parameter <T>
von MyGenericClass
zu ersetzen, und implementieren Sie ihn wie im folgenden Beispiel:
public class NonGenericClass implements MyGenericInterface<String> {
public void foo(String t) { } // type T has been replaced by String
}
Diese Klasse befasst sich nur mit String
. Dies bedeutet, dass die Verwendung von MyGenericInterface
mit anderen Parametern (z. B. Integer
, Object
usw.) nicht kompiliert wird, wie das folgende Snippet zeigt:
NonGenericClass myClass = new NonGenericClass();
myClass.foo("foo_string"); // OK, legal
myClass.foo(11); // NOT OK, does not compile
myClass.foo(new Object()); // NOT OK, does not compile
Generische Klassenimplementierung
Deklarieren Sie eine weitere generische Schnittstelle mit dem formalen Typparameter <T>
der MyGenericInterface
wie folgt implementiert:
public class MyGenericSubclass<T> implements MyGenericInterface<T> {
public void foo(T t) { } // type T is still the same
// other methods...
}
Beachten Sie, dass möglicherweise ein anderer Parameter für den formalen Typ verwendet wurde:
public class MyGenericSubclass<U> implements MyGenericInterface<U> { // equivalent to the previous declaration
public void foo(U t) { }
// other methods...
}
Raw-Typ-Klassenimplementierung
Deklarieren Sie eine nicht-generische Klasse , die implementiert MyGenericInteface
als Ausgangstyp (nicht bei allen mit Generika), wie folgt:
public class MyGenericSubclass implements MyGenericInterface {
public void foo(Object t) { } // type T has been replaced by Object
// other possible methods
}
Auf diese Weise wird nicht empfohlen, da es nicht 100% sicher zur Laufzeit ist , weil sie roh Art mischt (die Unterklasse) mit Generika (der Schnittstelle) und es ist auch verwirrend. Moderne Java-Compiler geben bei dieser Art der Implementierung eine Warnung aus, der Code wird jedoch aus Kompatibilitätsgründen mit älteren JVM (1.4 oder früher) kompiliert.
Alle oben aufgeführten Möglichkeiten sind auch zulässig, wenn eine generische Klasse als Supertyp anstelle einer generischen Schnittstelle verwendet wird.
Verwenden von Generics zum automatischen Casting
Mit Generics ist es möglich, zurückzugeben, was der Anrufer erwartet:
private Map<String, Object> data;
public <T> T get(String key) {
return (T) data.get(key);
}
Die Methode wird mit einer Warnung kompiliert. Der Code ist tatsächlich sicherer als er aussieht, da die Java-Laufzeitumgebung bei der Verwendung eine Umwandlung durchführt:
Bar bar = foo.get("bar");
Es ist weniger sicher, wenn Sie generische Typen verwenden:
List<Bar> bars = foo.get("bars");
In diesem ClassCastException
, wenn der zurückgegebene Typ irgendeine Art von List
(dh die Rückgabe von List<String>
würde keine ClassCastException
auslösen; Sie würden sie schließlich erhalten, wenn Elemente aus der Liste entfernt werden).
Um dieses Problem zu umgehen, können Sie eine API erstellen, die eingegebene Schlüssel verwendet:
public final static Key<List<Bar>> BARS = new Key<>("BARS");
zusammen mit dieser put()
Methode:
public <T> T put(Key<T> key, T value);
Bei diesem Ansatz können Sie nicht den falschen Typ in die Map einfügen. Das Ergebnis ist also immer korrekt (es sei denn, Sie erstellen aus Versehen zwei Schlüssel mit demselben Namen, aber unterschiedlichen Typen).
Verbunden:
Rufen Sie eine Klasse ab, die zur Laufzeit die generischen Parameter erfüllt
Viele ungebundene generische Parameter, wie sie in einer statischen Methode verwendet werden, können zur Laufzeit nicht wiederhergestellt werden (siehe Andere Threads zum Löschen ). Es gibt jedoch eine gemeinsame Strategie, um auf den Typ zuzugreifen, der zur Laufzeit einen generischen Parameter einer Klasse erfüllt. Dies ermöglicht generischen Code, der vom Zugriff auf den Typ abhängt, ohne dass bei jedem Aufruf die Typinformationen eingezogen werden müssen.
Hintergrund
Die generische Parametrisierung für eine Klasse kann durch Erstellen einer anonymen inneren Klasse überprüft werden. Diese Klasse erfasst die Typinformationen. Im Allgemeinen wird dieser Mechanismus als Supertyp-Token bezeichnet , die in Neal Gafters Blogpost detailliert beschrieben werden.
Implementierungen
Drei gängige Implementierungen in Java sind:
Verwendungsbeispiel
public class DataService<MODEL_TYPE> {
private final DataDao dataDao = new DataDao();
private final Class<MODEL_TYPE> type = (Class<MODEL_TYPE>) new TypeToken<MODEL_TYPE>
(getClass()){}.getRawType();
public List<MODEL_TYPE> getAll() {
return dataDao.getAllOfType(type);
}
}
// the subclass definitively binds the parameterization to User
// for all instances of this class, so that information can be
// recovered at runtime
public class UserService extends DataService<User> {}
public class Main {
public static void main(String[] args) {
UserService service = new UserService();
List<User> users = service.getAll();
}
}