In the use of generics, there is another important thing called wildcard. This article introduces the use of wildcards. Has very good reference value. Let’s take a look at it with the editor
Introduction
The first two articles introduced the basic usage of generics, type erasure and generic arrays. In the use of generics, there is another important thing called wildcards. This article introduces the use of wildcards.
Covariance of arrays
Before understanding wildcards, let’s first understand arrays. Arrays in Java are covariant, what does it mean? Consider the following example:
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 *///:~
The first line in the main method creates an Apple array and assigns it to a reference to the Fruit array. This makes sense, Apple is a subclass of Fruit, an Apple object is also a Fruit object, so an Apple array is also an array of Fruit. This is called covariance of arrays. Java designed arrays to be covariant, which is controversial. Some people think this is a flaw.
Although Apple[] can be "upcast" to Fruit[], the actual type of the array elements is still Apple, and we can only put Apple or a subclass of Apple into the array. In the above code, Fruit objects and Orange objects are put into the array. For the compiler, this can be compiled, but at runtime, the JVM can know that the actual type of the array is Apple[], so when other objects are added to the array, an exception will be thrown.
One of the purposes of generic design is to enable such run-time errors to be discovered at compile time. Let's see what happens when a generic container class is used instead of an array:// Compile Error: incompatible types: ArrayList<Fruit> flist = new ArrayList<Apple>();The above code simply won't compile. When it comes to generics, although Apple is a subtype of Fruit, ArrayList463277d9ebc274bcf30ecc27cb72790a is not a subtype of ArrayListe4dae6b035208b28264d9169d0b1fee3, and generics do not support covariance.
Use wildcards
We know from the above that statements likeListc8f01a3f8889dcf657849dd45bc0fc4c list = ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a cannot be compiled. Although Integer is a subtype of Number. So what if we do need to have this “upward transformation” relationship? This is where wildcards come into play.
Upper boundary limiting wildcard
Use wildcard characters in the form of 57019040ccef885c8e3bd8f9deb31922 to achieve upward transformation of generics:
public class GenericsAndCovariance { public static void main(String[] args) { // Wildcards allow covariance: List<? extends Fruit> flist = new ArrayList<Apple>(); // Compile Error: can'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); } }In the above example, the type of flist is
List57019040ccef885c8e3bd8f9deb31922 We can read it as: a type of List, this type can
inheritFruit of some type. Note, This does not mean that this List can hold any type of Fruit. The wildcard represents a specific type, which means "a specific type, but flist does not specify". This is not easy to understand. The specific explanation for this example is that the flist reference can point to a certain type of List, as long as the type inherits from Fruit, it can be Fruit or Apple, such as new ArrayList463277d9ebc274bcf30ecc27cb72790a## in the example. # But in order to upward cast to flist, flist does not care what the specific type is. As mentioned above, the wildcard character
represents a List of a specific type (Fruit or its subclass), but it does not care what the actual type is. Anyway, it is a subtype of Fruit, and Fruit is its upper boundary. So what can we do with such a List? In fact, if we don't know what type this List holds, how can we safely add an object? In the above code, adding any object to flist, whether it is Apple or Orange or even Fruit object, the compiler will not allow it, the only thing that can be added is null. So if we do a generic upward transformation (List57019040ccef885c8e3bd8f9deb31922 flist = new ArrayList463277d9ebc274bcf30ecc27cb72790a()
), then we will lose the ability to add any object to this List, even if it is Neither does Object. On the other hand, if you call a method that returns Fruit, it is safe. Because we know that in this List, no matter what its actual type is, it can definitely be converted to Fruit, so the compiler allows the return of Fruit.
After understanding the functions and limitations of wildcards, it seems that we cannot call any method that accepts parameters. In fact, not really, look at the following example:
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' flist.indexOf(new Apple()); // Argument is ‘Object' //flist.add(new Apple()); 无法编译 } }
In the above example, the type of flist is
List57019040ccef885c8e3bd8f9deb31922, and the generic parameter uses restricted wildcards. So we lose the example of adding any type of object to it, and the last line of code doesn't compile. <p>但是 flist 却可以调用 contains 和 indexOf 方法,它们都接受了一个 Apple 对象做参数。如果查看 ArrayList 的源代码,可以发现 add() 接受一个泛型类型作为参数,但是 contains 和 indexOf 接受一个 Object 类型的参数,下面是它们的方法签名:</p><pre class="brush:java;">public boolean add(E e)
public boolean contains(Object o)
public int indexOf(Object o)</pre><p>所以如果我们指定泛型参数为 <code>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' 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 T
,T
是类型参数的下界。使用这种形式的通配符,我们就可以 ”传递对象” 了。还是用例子解释:
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 本身 ) 的一种。
大多数情况下泛型的使用比较简单,但是如果自己编写支持泛型的代码需要对泛型有深入的了解。这几篇文章介绍了泛型的基本用法、类型擦除、泛型数组以及通配符的使用,涵盖了最常用的要点,泛型的总结就写到这里。
The above is the detailed content of Summary of Java Generics (3) - Detailed explanation of the use of wildcards. For more information, please follow other related articles on the PHP Chinese website!