通常情况下,一个编译器处理泛型有两种方式:
C#
中的泛型无论在源码中、还是运行期的 CLR 都是切实存在的,List
与 List
就是两个不同类型,会产生两份目标代码,有它们自己的虚方法表和类型数据,这种实现称为类型膨胀
。类型擦除
,是 Java 的语法糖,因此也是伪泛型。每个泛型类都只生成唯一的目标代码,该泛型类的所有实例都映射到这份代码上。class ArrayAlg {
public static T getMiddle(T... a) {
return a[a.length / 2];
}
}
// 调用时
String middle = ArrayAlg.getMiddle("]ohnM", "Q.", "Public");
Pair.java
源代码如下:
public class Pair {
public static void main(String[] args) {
Pair pair = new Pair<>();
pair.setFirst("one");
pair.setSecond("two");
System.out.println(pair.getFirst());
System.out.println(pair.getSecond());
}
private T first;
private T second;
public Pair(T a, T b) {
this.first = a;
this.second = b;
}
public Pair() {
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
分别执行命令:
javac Pair.java
javap -c Pair.class
得到反编译后内容(仅贴出部分):
public class com.webtest.test.generic.Pair {
public static void main(java.lang.String[]);
Code:
0: new #1 // class com/webtest/test/generic/Pair
3: dup
4: invokespecial #2 // Method "":()V
7: astore_1
8: aload_1
9: ldc #3 // String one
11: invokevirtual #4 // Method setFirst:(Ljava/lang/Object;)V
14: aload_1
15: ldc #5 // String two
17: invokevirtual #6 // Method setSecond:(Ljava/lang/Object;)V
20: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
23: aload_1
24: invokevirtual #8 // Method getFirst:()Ljava/lang/Object;
27: checkcast #9 // class java/lang/String
30: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
36: aload_1
37: invokevirtual #11 // Method getSecond:()Ljava/lang/Object;
40: checkcast #9 // class java/lang/String
43: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
46: return
public com.webtest.test.generic.Pair(T, T);
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."":()V
4: aload_0
5: aload_1
6: putfield #13 // Field first:Ljava/lang/Object;
9: aload_0
10: aload_2
11: putfield #14 // Field second:Ljava/lang/Object;
14: return
}
可以从 Pair 构造函数中看出 first/second 成员编译后为 Object 类型;
另 main() 中 Pair 实例对象调用方法,也都是得到 Object 类型,其中 getFirst()、getSecond() 还牵扯了 检验类型转换,
由 27 和 40 数字序列看出 checkcast 指令。
关于 ? super A 和 ?extends A 的使用,存在容易混淆的点,搞不清楚如何正确使用。
先拿 ? super A 说明(因为我觉得从 super 角度更容易让人恍然大悟,至少我之前是如此),在 List super A> 中,? super A 限定的是类型参数而已,不是直接限制的元素
。
过程是,先令类型参数 T 满足 ? super A,然后得到 List
以上两句重要的话,读三遍!!!
举例:
class Q {
}
class W extends Q{
}
class E extends W{
}
class R extends E{
}
List super E> listE = new ArrayList<>();
? super E 限定了类型参数 T 必须为 E 类或者 E 类的父类
假设 T 为 E 类,得到 List,因此集合内可以添加 E 类和 E 类的子类的元素;
假设 T 为 E 类的父类,同理集合内可以添加 E 类和 E 类的子类的元素;
因此可以有:
listE.add(new E()); // 允许
listE.add(new R()); // 允许
listE.add(new W()); // 错误,不能添加。
listE.add(new Q()); // 错误,不能添加。
错误原因是万一 ? super E 具体的类型参数 T 是 E 类呢,List 就肯定不能添加 E 类的父类对象;
同理万一 ? super E 具体的参数类型 T 是 W 类呢,List 就肯定不能添加 Q 类对象。
从以上例子可以看出,正确的理解是:? super E 是限定了参数类型 T,T 为 E 类或者 E 类的父类,因而可以添加 E 类或者 E 类子类对象到列表中。
错误的理解是:直观上的感觉 ? super E 是限定了添加到列表的元素,必须是 E 类的父类的对象。
再深入,带有超类型限定的通配符,可以为方法提供参数,但是不能使用返回值。你会发现以上的例子中 listE
调用获取元素的方法,仅会得到 Object 对象,Object object = listE.get(0);
。这是因为不能保证返回对象的类型,只能把它赋给一个 Object 对象。后面会引出 PECS 原则
接下来再拿 ? extends A 做说明,在 List extends A> 中,? extends A 限定的是类型参数而已,不是直接限制的元素
。过程是,先令类型参数 T 满足 ? extends A,然后得到 List
举例:
// Number 类是 Integer 和 Double 的父类
List extends Number> numberArray = new ArrayList(); // Number 是 Number 类型的
List extends Number> numberArray = new ArrayList(); // Integer 是 Number 的子类
List extends Number> numberArray = new ArrayList(); // Double 是 Number 的子类
以上三个操作都是合法的,但是对于 List extends Number> numberArray 对象:
我们不能添加 Number 到 numberArray 中, 因为 numberArray 有可能是List 类型
我们不能添加 Integer 到 numberArray 中, 因为 numberArray 有可能是 List 类型
我们不能添加 Double 到 numberArray 中, 因为 numberArray 有可能是 List 类型
我们无法确定 List extends A> 对象实际是个什么类型,毕竟都有可能,因此就不能确定添加的元素是否能够和 List 匹配。
但是读取元素的操作,我们是能确定返回类型的。
Number number = numberArray.get(0);
List extends Number> 类型如果是 List 类型,那么返回 Number 类型元素,
那么读取 Number 类型对象给 Number 类型引用是合法的;
List extends Number> 类型如果是 List 类型,那么返回 Integer 类型元素,
那么读取 Integer 类型对象给 Number 类型引用是合法的;
List extends Number> 类型如果是 List 类型,那么返回 Double 类型元素,
那么读取 Double 类型对象给 Number 类型引用是合法的;
如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
如果既要存又要取,那么就不要使用任何通配符。
需要记住有关 Java 泛型转换的事实:
参考:
Java的类型擦除
Java 之泛型通配符 ? extends T 与 ? super T 解惑
深入理解 Java 虚拟机:JVM 高级特性与最佳实践/周志明著