Heim  >  Artikel  >  Java  >  Zusammenfassung von Java Generics (3) – Detaillierte Erläuterung der Verwendung von Platzhaltern

Zusammenfassung von Java Generics (3) – Detaillierte Erläuterung der Verwendung von Platzhaltern

黄舟
黄舟Original
2017-03-22 10:25:101508Durchsuche

Bei der Verwendung von Generika gibt es eine weitere wichtige Sache namens Platzhalter. In diesem Artikel wird die Verwendung von Platzhaltern vorgestellt. Hat einen sehr guten Referenzwert. Schauen wir uns das unten mit dem Editor an

Einführung

In den ersten beiden Artikeln wurde die grundlegende Verwendung von Generika, Typlöschung und generischen Arrays vorgestellt. Bei der Verwendung von Generika gibt es noch eine weitere wichtige Sache, die als Platzhalter bezeichnet wird. In diesem Artikel wird die Verwendung von Platzhaltern vorgestellt.

Kovarianz von Arrays

Bevor wir Platzhalter verstehen, wollen wir zunächst Arrays verstehen. Arrays in Java sind kovariant, was bedeutet das? Schauen Sie sich das folgende Beispiel an:

class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
 public static void main(String[] args) { 
 Fruit[] fruit = new Apple[10];
 fruit[0] = new Apple(); // OK
 fruit[1] = new Jonathan(); // OK
 // Runtime type is Apple[], not Fruit[] or Orange[]:
 try {
  // Compiler allows you to add Fruit:
  fruit[0] = new Fruit(); // ArrayStoreException
 } catch(Exception e) { System.out.println(e); }
 try {
  // Compiler allows you to add Oranges:
  fruit[0] = new Orange(); // ArrayStoreException
 } catch(Exception e) { System.out.println(e); }
 }
} /* Output:
java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange
*///:~

Die erste Zeile in der Hauptmethode erstellt ein Apple-Array und weist es einer Referenz auf das Fruit-Array zu. Das macht Sinn, denn Apple ist eine Unterklasse von Fruit, ein Apple-Objekt ist auch ein Fruit-Objekt, also ist ein Apple-Array auch ein Fruit-Array. Dies wird als Kovarianz von Arrays bezeichnet. Java hat Arrays so konzipiert, dass sie kovariant sind, was einige Leute für einen Fehler halten.

Obwohl Apple[] in Fruit[] „upcasted“ werden kann, ist der eigentliche Typ der Array-Elemente immer noch Apple, und wir können nur Apple oder eine Unterklasse von Apple in das Array einfügen. Im obigen Code werden Fruit-Objekte und Orange-Objekte in das Array eingefügt. Für den Compiler kann dies kompiliert werden, aber zur Laufzeit kann die JVM erkennen, dass der tatsächliche Typ des Arrays Apple[] ist. Wenn also andere Objekte zum Array hinzugefügt werden, wird eine Ausnahme ausgelöst .

Einer der Zwecke des generischen Designs besteht darin, die Erkennung solcher Laufzeitfehler zur Kompilierungszeit zu ermöglichen. Schauen Sie sich an, was passiert, wenn eine generische Containerklasse anstelle eines Arrays verwendet wird:

// Compile Error: incompatible types:
ArrayList<Fruit> flist = new ArrayList<Apple>();
Der obige Code lässt sich überhaupt nicht kompilieren. Wenn es um Generika geht, ist ArrayList463277d9ebc274bcf30ecc27cb72790a zwar ein Subtyp von Fruit, aber kein Subtyp von ArrayListe4dae6b035208b28264d9169d0b1fee3 und Generika unterstützen keine Kovarianz.

Verwenden Sie Platzhalter

Aus dem oben Gesagten wissen wir, dass Anweisungen wie

nicht kompiliert werden können, obwohl Integer ein Untertyp von Number ist. Was also, wenn wir diese „Aufwärtstransformation“-Beziehung brauchen? Hier kommen Wildcards ins Spiel. Listc8f01a3f8889dcf657849dd45bc0fc4c list = ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a

Obergrenze-Wildcard

Verwenden Sie Wildcards in der Form e2f62cfe50f2c33ec89772cfcab89a6c, um eine generische Aufwärtstransformation zu erreichen:

public class GenericsAndCovariance {
 public static void main(String[] args) {
 // Wildcards allow covariance:
 List<? extends Fruit> flist = new ArrayList<Apple>();
 // Compile Error: can&#39;t add any type of object:
 // flist.add(new Apple());
 // flist.add(new Fruit());
 // flist.add(new Object());
 flist.add(null); // Legal but uninteresting
 // We know that it returns at least Fruit:
 Fruit f = flist.get(0);
 }
}
Im obigen Beispiel ist der Typ der Liste

Wir können ihn lesen als: ein Typ von Liste. Dieser Typ kann ein Typ sein, der List57019040ccef885c8e3bd8f9deb31922 Fruit erbt. Beachten Sie: Dies bedeutet nicht, dass diese Liste jede Art von Obst enthalten kann. Der Platzhalter stellt einen bestimmten Typ dar, was bedeutet „ein bestimmter Typ, aber flist gibt ihn nicht an“. Dies ist nicht leicht zu verstehen. Die spezifische Erklärung für dieses Beispiel ist, dass die Flist-Referenz auf einen bestimmten Listentyp verweisen kann. Solange der Typ von Fruit erbt, kann es sich um Fruit oder Apple handeln, z. B. im Beispiel: Für die Aufwärtstransformation zu Flist ist es Flist jedoch egal, um welchen Typ es sich handelt. new ArrayList463277d9ebc274bcf30ecc27cb72790aWie oben erwähnt, stellt das Platzhalterzeichen

eine Liste eines bestimmten Typs (Fruit oder seine Unterklasse) dar, es ist jedoch egal, um welchen Typ es sich tatsächlich handelt, es handelt sich jedenfalls um einen Subtyp von Fruit. und Frucht ist seine obere Grenze. Was können wir also mit einer solchen Liste machen? Wenn wir nicht wissen, welchen Typ diese Liste enthält, wie können wir dann sicher ein Objekt hinzufügen? Wenn Sie im obigen Code ein beliebiges Objekt zur Liste hinzufügen, sei es ein Apple-, ein Orange- oder sogar ein Fruit-Objekt, lässt der Compiler dies nicht zu. Das einzige, was hinzugefügt werden kann, ist null. Wenn wir also eine generische Aufwärtsumwandlung (

) durchführen, verlieren wir die Möglichkeit, ein beliebiges Objekt zu dieser Liste hinzuzufügen, sogar ein Objekt. List57019040ccef885c8e3bd8f9deb31922List57019040ccef885c8e3bd8f9deb31922 flist = new ArrayList463277d9ebc274bcf30ecc27cb72790a()Andererseits ist es sicher, wenn Sie eine Methode aufrufen, die eine Frucht zurückgibt. Da wir wissen, dass diese Liste unabhängig vom tatsächlichen Typ definitiv in Fruit konvertiert werden kann, ermöglicht der Compiler die Rückgabe von Fruit.

Nachdem wir die Funktionen und Einschränkungen von Platzhaltern verstanden haben, scheint es, dass wir keine Methode aufrufen können, die Parameter akzeptiert. Tatsächlich nicht wirklich, schauen Sie sich das folgende Beispiel an:

Im obigen Beispiel ist der Typ der Flist
public class CompilerIntelligence {
 public static void main(String[] args) {
 List<? extends Fruit> flist =
 Arrays.asList(new Apple());
 Apple a = (Apple)flist.get(0); // No warning
 flist.contains(new Apple()); // Argument is ‘Object&#39;
 flist.indexOf(new Apple()); // Argument is ‘Object&#39;
 //flist.add(new Apple()); 无法编译
 }
}
und der generische Parameter verwendet eingeschränkte Platzhalter, sodass wir die Möglichkeit zum Hinzufügen verlieren Beispiel für einen beliebigen Objekttyp, die letzte Codezeile wird nicht kompiliert.

但是 flist 却可以调用 contains 和 indexOf 方法,它们都接受了一个 Apple 对象做参数。如果查看 ArrayList 的源代码,可以发现 add() 接受一个泛型类型作为参数,但是 contains 和 indexOf 接受一个 Object 类型的参数,下面是它们的方法签名:

public boolean add(E e)
public boolean contains(Object o)
public int indexOf(Object o)

所以如果我们指定泛型参数为 57019040ccef885c8e3bd8f9deb31922 时,add() 方法的参数变为 ? extends Fruit,编译器无法判断这个参数接受的到底是 Fruit 的哪种类型,所以它不会接受任何类型。

然而,contains 和 indexOf 的类型是 Object,并没有涉及到通配符,所以编译器允许调用这两个方法。这意味着一切取决于泛型类的编写者来决定那些调用是 “安全” 的,并且用 Object 作为这些安全方法的参数。如果某些方法不允许类型参数是通配符时的调用,这些方法的参数应该用类型参数,比如 add(E e)。

当我们自己编写泛型类时,上面介绍的就有用了。下面编写一个 Holder 类:

public class Holder<T> {
 private T value;
 public Holder() {}
 public Holder(T val) { value = val; }
 public void set(T val) { value = val; }
 public T get() { return value; }
 public boolean equals(Object obj) {
 return value.equals(obj);
 }
 public static void main(String[] args) {
 Holder<Apple> Apple = new Holder<Apple>(new Apple());
 Apple d = Apple.get();
 Apple.set(d);
 // Holder<Fruit> Fruit = Apple; // Cannot upcast
 Holder<? extends Fruit> fruit = Apple; // OK
 Fruit p = fruit.get();
 d = (Apple)fruit.get(); // Returns ‘Object&#39;
 try {
  Orange c = (Orange)fruit.get(); // No warning
 } catch(Exception e) { System.out.println(e); }
 // fruit.set(new Apple()); // Cannot call set()
 // fruit.set(new Fruit()); // Cannot call set()
 System.out.println(fruit.equals(d)); // OK
 }
} /* Output: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~

在 Holer 类中,set() 方法接受类型参数 T 的对象作为参数,get() 返回一个 T 类型,而 equals() 接受一个 Object 作为参数。fruit 的类型是 Holder57019040ccef885c8e3bd8f9deb31922,所以set()方法不会接受任何对象的添加,但是 equals() 可以正常工作。

下边界限定通配符

通配符的另一个方向是 “超类型的通配符“: ? super TT是类型参数的下界。使用这种形式的通配符,我们就可以 ”传递对象” 了。还是用例子解释:

public class SuperTypeWildcards {
 static void writeTo(List<? super Apple> apples) {
 apples.add(new Apple());
 apples.add(new Jonathan());
 // apples.add(new Fruit()); // Error
 }
}

writeTo 方法的参数 apples 的类型是 List72b4226105aa1d07ec3b6e98f565c59e  它表示某种类型的 List,这个类型是 Apple 的基类型。也就是说,我们不知道实际类型是什么,但是这个类型肯定是 Apple 的父类型。因此,我们可以知道向这个 List 添加一个 Apple 或者其子类型的对象是安全的,这些对象都可以向上转型为 Apple。但是我们不知道加入 Fruit 对象是否安全,因为那样会使得这个 List 添加跟 Apple 无关的类型。

在了解了子类型边界和超类型边界之后,我们就可以知道如何向泛型类型中 “写入” ( 传递对象给方法参数) 以及如何从泛型类型中 “读取” ( 从方法中返回对象 )。下面是一个例子:

public class Collections { 
 public static <T> void copy(List<? super T> dest, List<? extends T> src) 
 {
 for (int i=0; i<src.size(); i++) 
 dest.set(i,src.get(i)); 
 } 
}

src 是原始数据的 List,因为要从这里面读取数据,所以用了上边界限定通配符:d203bb1ae585225d4838a2b7e3d0503e,取出的元素转型为 T。dest 是要写入的目标 List,所以用了下边界限定通配符:117c5a0bdb71ea9a9d0c2b99b03abe3e,可以写入的元素类型是 T 及其子类型。

无边界通配符

还有一种通配符是无边界通配符,它的使用形式是一个单独的问号:List6b3d0130bba23ae47fe2b8e8cddf0195,也就是没有任何限定。不做任何限制,跟不用类型参数的 List 有什么区别呢?

List6b3d0130bba23ae47fe2b8e8cddf0195 list表示 list 是持有某种特定类型的 List,但是不知道具体是哪种类型。那么我们可以向其中添加对象吗?当然不可以,因为并不知道实际是哪种类型,所以不能添加任何类型,这是不安全的。而单独的 List list ,也就是没有传入泛型参数,表示这个 list 持有的元素的类型是 Object,因此可以添加任何类型的对象,只不过编译器会有警告信息。

总结

通配符的使用可以对泛型参数做出某些限制,使代码更安全,对于上边界和下边界限定的通配符总结如下:

  • 使用 Listb75ffcf7a0002c20418af7f7df067052 list 这种形式,表示 list 可以引用一个 ArrayList ( 或者其它 List 的 子类 ) 的对象,这个对象包含的元素类型是 C 的子类型 ( 包含 C 本身)的一种。

  • 使用 List41a5f90752881873ec48ff3be0e4f0a1 list 这种形式,表示 list 可以引用一个 ArrayList ( 或者其它 List 的 子类 ) 的对象,这个对象包含的元素就类型是 C 的超类型 ( 包含 C 本身 ) 的一种。

大多数情况下泛型的使用比较简单,但是如果自己编写支持泛型的代码需要对泛型有深入的了解。这几篇文章介绍了泛型的基本用法、类型擦除、泛型数组以及通配符的使用,涵盖了最常用的要点,泛型的总结就写到这里。

Das obige ist der detaillierte Inhalt vonZusammenfassung von Java Generics (3) – Detaillierte Erläuterung der Verwendung von Platzhaltern. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn