1 泛型
1.0 前言——为什么要死磕Java泛型
不知道阅读这篇文章的你是否曾跟我一样,在没有阅读《Java核心技术》前查阅了大量的Java泛型文章,但是在实际使用泛型的过程中,总是觉得对泛型的理解不够深透。在撰写这篇文章之前,我自问了自己下述的问题,结果我自己却不能回答上来:
泛型类中能用类型参数去定义static成员变量和static方法的返回值和形式参数吗?
泛型方法的类型参数命名可以跟泛型类的类型参数重名吗?
泛型方法可以是成员方法、static方法、final方法、private方法吗?
泛型类的子类要怎么被处理?是不是还有泛型接口和泛型抽象方法?
Raw Use会怎么样,例如ArrayList list = new ArrayList(),是不是默认将T指定为Object?
调用泛型方法时,必须加尖括号必须有”类.“或"this/对象."吗?
因此我下定决心,一定要把Java泛型的所有细节知识点都给弄明白,于是我仔细地查阅了《Java核心技术》中关于泛型的知识点,并结合自己的理解进行总结概括,同时也查阅了其他大神撰写的Java泛型相关的文章,最终写下了这篇3万字+的Java泛型详解文章,希望也能给困惑中的你带来一些帮助。
1.1 泛型是什么
JDK1.5引入泛型的最根本目的:只要在编译时期没有出现警告,那么运行时期就不会出现ClassCastException异常。泛型就是把类型明确的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型。
参数化类型是指把类型当做是参数一样来传递。泛型的写法为<引用类型>
,例如<String>
。
Java泛型的相关术语:
- ArrayList<E>中的E称为类型参数或类型变量,即Type Parameter;
- ArrayList<Integer>中的Integer称为实际类型参数,即Actual Type Argument,或Type Argument;
- ArrayList<E>或ArrayList<Integer>中的ArrayList称为泛型类,在类型擦除的语境下ArrayList也称为原始类型,即参数化类型ArrayList<Object>;
- 整个ArrayList<E>称为泛型类型;
- 整个ArrayList<Integer>称为参数化类型,即Parameterized Type;
- ArrayList<E extends Comparable>中的Comparable称为限定类型。
1.2 引入泛型前后的区别
1.2.1 引入泛型前的情况
Java集合有个缺点:把一个对象存入集合里之后, 集合就会"忘记"这个对象的数据类型,当再次取出该对象时,该对象的编译时类型就变成了Object类型(其运行时类型没变),Java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性,但这样做导致Java集合存在以下的两个缺陷:
- 集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存String对象的集合,但如果开发者编码时不小心把Integer对象存入进去,这种编码在编译时也不会报错;
- 集合丢失了对象的编译时类型的信息,可能引起ClassCastException异常。由于把对象存入集合时,集合丢失了对象的编译时类型的信息,集合只知道它存放的是Object ,因此取出集合元素后通常还需要进行强制类型转换,这种强制类型转换既增加了编程的复杂度,也可能引起ClassCastException异常。
ArrayList filesArrayList = new ArrayList(); //开发者新建了一个未用泛型限定的ArrayList,期望用于参数File类型的对象
filesArrayList.add(new File("...")); //按照期望存入了一个File对象
filesArrayList.add("A string"); //不小心出错存入了一个String对象,因为没有泛型限制,故编译时该行代码不报错
File firstFile = (File) filesArrayList.get(0); //将运行时类型为File,但编译时类型为Object的第0个元素强制转换为File类型,符合强制转换的要求,故不会报错
File secondFile = (File) filesArrayList.get(1); //将运行时类型为String,但编译时类型为Object的第0个元素强制转换为File类型,不符合强制转换的要求,故引起ClassCastException异常
1.2.2 引入泛型后的优势
1.2.2.1 可读性
泛型通过泛型参数来指示集合元素的类型,这使得代码具有更好的可读性,例如下述声明的ArrayList<String>
类型,开发者只要看到这个类型声明就会知道在使用set()和get()方法时,存入和取出的元素都是String类型的。
ArrayList<String> files = new ArrayList<String>(); //Java7及以后,等号右侧ArrayList构造函数中可省略泛型类型
1.2.2.2 安全性和健壮性
泛型的另一个优势就是代码的安全性和健壮性,例如在调用ArrayList<String>
类型集合的set()方法时,编译器可以在编译时检查set()方法传入的参数是否为String类型,如果不是则无法通过编译,这样的话出现编译错误比在运行时出现强制类型转换异常ClassCastException要好得多;而在调用ArrayList<String>
类型集合的get()方法时,不需要开发者显式编码来强制类型转换,编译器就会自动且隐式地将get()方法的返回值强制转换为String类型,并同时进行强制类型转换的安全检查,再也不是直接返回Object类型的返回值了。这就避免了开发者显式编码来强制类型转换可能造成强制类型转换异常ClassCastException的问题。
ArrayList arrayList = new ArrayList();
arrayList.add("string");
arrayList.add(123); //不会有编辑警告和编译报错
//arrayList是未使用类型参数的ArrayList泛型类,因此此处需要开发者显式编码来强制类型转换
String string1 = (String) arrayList.get(0);
//stringArrayList是使用泛型类型的ArrayList类型,即ArrayList<String>类型
ArrayList<String> stringArrayList = new ArrayList<>();
stringArrayList.add("string");
arrayList.add(123); //会有编辑警告和编译报错:java: no suitable method found for add(int),method java.util.List.add(java.lang.String) is not applicable(argument mismatch; int cannot be converted to java.lang.String)
//编译器就会自动且隐式地将get()方法的返回值强制转换为String类型,并同时进行强制类型转换的安全检查
String string2 = stringArrayList.get(0);
上述的代码String string2 = stringArrayList.get(0);
省去了强制转换,因为编译器就会自动且隐式地将get()方法的返回值强制转换为String类型,并可以在编译时检查类型安全。
其实我们也可以显式地在这行代码上加上(String)
进行强制类型转换,即:
String string2 = (String) stringArrayList.get(0);
但是编译器会对(String)
进行高亮并有编译警告:
Casting 'stringArrayList.get(...)' to 'String' is redundant
这段编译警告的文案翻译过来就是:Java编译器会自动且隐式地将get()方法的返回值的表达式前加上(String)
进行强制类型转换,而开发者在这显式编码的(String)
是多余的,可以去掉。
另外,我们也可以通过反编译上述代码的字节码来核实Java编译器是否会自动且隐式地将get()方法的返回值强制转换为String类型:
Classfile /Users/didi/Desktop/git_test/Generics/out/production/Generics/GenericsTest.class
Last modified 2021-5-19; size 818 bytes
MD5 checksum 12078b605efb15635d8b7c0e1e6b2dbc
Compiled from "GenericsTest.java"
public class GenericsTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#31 // java/lang/Object."<init>":()V
#2 = Class #32 // java/util/ArrayList
#3 = Methodref #2.#31 // java/util/ArrayList."<init>":()V
#4 = String #33 // string
#5 = Methodref #2.#34 // java/util/ArrayList.add:(Ljava/lang/Object;)Z
#6 = Methodref #2.#35 // java/util/ArrayList.get:(I)Ljava/lang/Object;
#7 = Class #36 // java/lang/String
#8 = Class #37 // GenericsTest
#9 = Class #38 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 LGenericsTest;
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 args
#20 = Utf8 [Ljava/lang/String;
#21 = Utf8 arrayList
#22 = Utf8 Ljava/util/ArrayList;
#23 = Utf8 string1
#24 = Utf8 Ljava/lang/String;
#25 = Utf8 stringArrayList
#26 = Utf8 string2
#27 = Utf8 LocalVariableTypeTable
#28 = Utf8 Ljava/util/ArrayList<Ljava/lang/String;>;
#29 = Utf8 SourceFile
#30 = Utf8 GenericsTest.java
#31 = NameAndType #10:#11 // "<init>":()V
#32 = Utf8 java/util/ArrayList
#33 = Utf8 string
#34 = NameAndType #39:#40 // add:(Ljava/lang/Object;)Z
#35 = NameAndType #41:#42 // get:(I)Ljava/lang/Object;
#36 = Utf8 java/lang/String
#37 = Utf8 GenericsTest
#38 = Utf8 java/lang/Object
#39 = Utf8 add
#40 = Utf8 (Ljava/lang/Object;)Z
#41 = Utf8 get
#42 = Utf8 (I)Ljava/lang/Object;
{
public GenericsTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LGenericsTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String string
11: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
14: pop
15: aload_1
16: iconst_0
17: invokevirtual #6 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
20: checkcast #7 // class java/lang/String
23: astore_2
24: new #2 // class java/util/ArrayList
27: dup
28: invokespecial #3 // Method java/util/ArrayList."<init>":()V
31: astore_3
32: aload_3
33: ldc #4 // String string
35: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
38: pop
39: aload_3
40: iconst_0
41: invokevirtual #6 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
44: checkcast #7 // class java/lang/String
47: astore 4
49: return
LineNumberTable:
line 5: 0
line 6: 8
line 7: 15
line 9: 24
line 10: 32
line 11: 39
line 13: 49
LocalVariableTable:
Start Length Slot Name Signature
0 50 0 args [Ljava/lang/String;
8 42 1 arrayList Ljava/util/ArrayList;
24 26 2 string1 Ljava/lang/String;
32 18 3 stringArrayList Ljava/util/ArrayList;
49 1 4 string2 Ljava/lang/String;
LocalVariableTypeTable:
Start Length Slot Name Signature
32 18 3 stringArrayList Ljava/util/ArrayList<Ljava/lang/String;>;
}
SourceFile: "GenericsTest.java"
我们可以在上述反编译的字节码中看到这两行代码:
//...
20: checkcast #7 // class java/lang/String
//...
44: checkcast #7 // class java/lang/String
//...
Java字节码类型检查指令checkcast:用于检查类型强制转换是否可以进行。如果可以进行则进行强制类型转换,同时checkcast指令不会改变操作数栈;如果不可以进行,那么它会抛出ClassCastException异常。
由上述父Java字节码类型检查指令checkcast的定义,我们可以知道上述反编译的字节码中的checkcast指令就对应我们Java代码中的显式和隐式的类型转换:
//...
String string1 = (String) arrayList.get(0);
//...
String string2 = stringArrayList.get(0);
由此我们便论证了Java编译器会自动且隐式地将get()方法的返回值强制转换为String类型。
1.2.2.3 可使用forEach循环遍历集合
在创建集合时,如果我们通过泛型明确了集合的类型,那么就能使用forEach循环来遍历集合。
ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
for (String string : list) {
System.out.println(string);
}
1.3 泛型类
一个泛型类(generic class)就是具有一个或多个类型变量的类,例如下面的Pair类和Map类:
public class Pair<T> {
private T firstMember;
private T secondMember;
public Pair() {
firstMember = null;
secondMember = null;
}
public Pair(T firstMember, T secondMember) {
this.firstMember = firstMember;
this.secondMember = secondMember;
}
public T getFirstMember() {
return firstMember;
}
public T getSecondMember() {
return secondMember;
}
public void setFirstMember(T newValue) {
firstMember = newValue;
}
public void setSecondMember(T newValue) {
secondMember = newValue;
}
}
class Map<K, V> {
//...
}
如上述代码所示,Pair类引入了类型变量T,用尖括号<>包括起来,并放在类名的后面。泛型类可以有多个类型变量,而这些类型变量可用于指定方法的形式参数类型和返回值类型,以及成员变量和局部变量的类型。
类型变量使用单个大写字母来表示,这是很常见的。在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字和值的类型。T(需要时还可以用临近的字母U和S)表示“任意类型”。
用具体的类型替换类型变量就可以实例化泛型类型,例如Pair<String>
,可以将结果想象成带构造器的普通类:Pair<String>()
、Pair<String>(String, String)
,和方法:String getFirstMember()、String getSecondMember()、void setFirstMember(String)、void setSecondMember(String)。
换句话说,泛型类可以看做普通类的工厂。下述代码便是实例化的Pair泛型类:
Pair<String> initializedPair = new Pair<String>();
Java7及以后,上述代码等号右侧ArrayList的构造函数中可省略泛型类型,如下所示:
Pair<String> initializedPair = new Pair<>();
1.4 泛型方法
前面已经介绍了如何定义一个泛型类。实际上还可以定义一个带有类型参数的泛型方法:
class ArrayUtil {
//其中`T... a`考查Java可变形参的知识点,其中形式参数a的类型为`T[]`数组类型
public static <T> T getMiddleElement(T... a) {
return a[a.length / 2];
}
}
可以从尖括号和类型变量看出,这是个在普通类中定义的泛型方法。注意,带尖括号的类型变量放在方法修饰符(这里是public static)的后面,方法返回值类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。当调用一个泛型方法时,可在泛型方法名前的尖括号中指定具体的类型:
String middleElement = ArrayUtil.<String>getMiddleElement("John", "Q.", "Public");
注意,除了泛型方法,在调用泛型类的实例方法时,也可以在实例方法名前的尖括号中指定具体的类型:
public class GenericMethodClass<T> {
/** 泛型类跟静态泛型方法的类型参数T重名,但作用域无交集,因此它们不冲突 */
public static <T> void printClassByStaticMethod(T t) {
System.out.println(t.getClass());
}
/**
* 编译器警告:Type parameter 'T' hides type parameter 'T',
* 这是因为泛型类跟其实例泛型方法的类型参数T重名,作用域有交集,
* 因此在该实例泛型方法体中,该实例泛型方法的类型参数T生效。
*/
public <T> void printClassByInstanceMethod(T t) {
System.out.println(t.getClass());
}
public T returnItself(T t) {
return t;
}
public void emptyMethod() {}
public static void main(String[] args) {
GenericMethodClass<String> genericMethodClass = new GenericMethodClass<>();
//编译提示:Explicit type arguments can be inferred,即我们显式指定的<Object>可以不用写
//,因为编译器可通过上下文推断出这个泛型方法的实际类型参数
GenericMethodClass.<Object>printClassByStaticMethod(new Object());
//可以用对象来调用静态泛型方法,但是不推荐这么做,因为这种做法不符合泛型方法的设计初衷
genericMethodClass.<Object>printClassByInstanceMethod(new String());
//也可以在实例方法中显式指定实际类型参数,但是编译器会提示:Type arguments are redundant for the non-generic method call
//,也就是说对于非泛型方法显式指定实际类型参数是无效的,因为非泛型方法的实际类型参数是在该对象初始化时就指定了,而不是像泛型方法需要
// 显式指定或者系统通过上下文推断,即使像下面代码指定了Integer,但returnItself()方法的返回值还是String类型
//,因为非泛型方法的实际类型参数在创建对象时就已经显式指定了
String string = genericMethodClass.<Integer>returnItself(new String());
//也可以在无类型参数的实例方法中显式指定实际类型参数,但这也是无效的,原因同上
//,因此编译器也会提示:Type arguments are redundant for the non-generic method call
genericMethodClass.<String>emptyMethod();
}
}
大多数情况下,调用泛型方法时可以省略类型参数,例如上述代码中的<String>
,因为编译器有足够的信息推断出所调用的泛型方法的类型参数,它用"John", "Q.", "Public"
的String[]类型与泛型类型T[]进行匹配,并推断出T一定是String类型。也就是说上述代码等价于:
String middleElement = ArrayUtil.getMiddleElement("John", "Q.", "Public");
但是,编译器偶尔也会报错,这就说明编译器推断出的泛型方法的形式参数或返回值的类型不符合预期,那么此时开发者需要查看并解读报错日志。看看下面这个示例:
Double middleElement = ArrayUtil.getMiddleElement(3.14, 1729, 0);
如果在IntelliJ IDEA运行上述代码会有下述报错日志:
java: 不兼容的类型: 推断类型不符合上限
推断: java.lang.Number&java.lang.Comparable<? extends java.lang.Number&java.lang.Comparable<?>>
上限: java.lang.Double,java.lang.Object
Incompatible types: Number & Comparable<? extends Number & Comparable<?>> is not convertible to Double
我们来分析一下这段报错的代码以及报错日志,首先编译器会自动装箱上述代码中的ArrayUtil类的getMiddleElement()方法的三个值为字面量的实际参数(即3.14, 1729, 0
),变成1个Double对象和2个Integer对象,然后寻找到这3个对象继承的共同直接父类Number和实现的共同直接接口Comparable,并将这些共同直接父类和共同直接接口作为最终推断出的参数类型,这也是为什么上述报错日志中写到的推断类型为Number&Comparable。
上述代码中的ArrayUtil.getMiddleElement(3.14, 1729, 0)
并未在泛型方法名前用尖括号指定具体的类型,因此对上述代码中的ArrayUtil类的getMiddleElement()方法的三个实际参数并未进行类型限制,但是编译器通过该段代码等式左侧的Double middleElement
引用变量推断出等式右侧表达式的类型上限应该为Double,这也就解释了为什么上述报错日志中写到的上限类型为Double&Object,其中包含Object是因为Object是默认的上限类型。
通过上述分析,我们理解了编译报错的原因:由于未在泛型方法名前用尖括号指定具体的类型,因此编译器推断该段代码等式右侧表达式返回值的编译时类型应该是Number或者Comparable,但是开发者在该段代码等式左侧显式指定了引用变量的编译时类型为Double,而Double并非Number或者Comparable的直接或间接父类,故该段代码的赋值操作是不合法的,进而导致了编译报错。
如果开发者想知道编译器对一个泛型方法最终推断出的类型,Java开发者Peter von der Ahé推荐了这样一个窍门:有目的地引入一个错误,并研究其产生的错误日志。例如在下述代码中,类E和类F继承的共同直接父类是D,类E和类F实现的共同直接接口是interfaceD和interfaceA:
class A {} class B extends A {} class C extends B {} class D extends C {} class E extends D implements interfaceE, interfaceC, interfaceA {} class F extends D implements interfaceD, interfaceB, interfaceA {} interface interfaceA {} interface interfaceB {} interface interfaceC extends interfaceB{} interface interfaceD extends interfaceC{} interface interfaceE extends interfaceD{} interface interfaceF extends interfaceD{} public class ArrayUtil { public static <T> T getMiddle(T... a) { return a[a.length / 2]; } public static void main(String[] args) { F middle = ArrayUtil.getMiddle(new F(), new E()); } }
执行上述代码的报错日志如下:
java: 不兼容的类型: 推断类型不符合上限 推断: D&interfaceD&interfaceA 上限: F,java.lang.Object no instance(s) of type variable(s) exist so that E conforms to F
1.5 类型变量的限定
有时,类或方法需要对类型变量加以约束。下面是个典型的例子,例子中的静态方法min()用于计算数组中的最小元素:
class ArrayUtil {
public static <T> T min(T[] a) {
if (a == null || a.length == 0) {
return null;
}
T smallest = a[0];
for (int i = 1; i < a.length; i++) {
//整段代码会在这里报错:Cannot resolve method 'compareTo' in 'T'
if(smallest.compareTo(a[i]) > 0) {
smallest = a[i];
}
}
return smallest;
}
}
但是上述代码有一个问题:代码中的变量smallest类型为T,这意味着它可以是任何一个类的对象,怎么才能确信T所属的类有compareTo()方法呢?解决这个问题的方案是将T限制为实现了Comparable接口(该接口只含一个方法compareTo()的标准接口)的类,而这可以通过对类型变量T设置限定(bound)来解决:
class ArrayUtil {
//为T设置限定后,编译器仍然会给出警告:Raw use of parameterized class 'Comparable'
public static <T extends Comparable> T min(T[] a) {
if (a == null || a.length == 0) {
return null;
}
T smallest = a[0];
for (int i = 1; i < a.length; i++) {
//编译器仍然会给出警告:Unchecked call to 'compareTo(T)' as a member of raw type 'java.lang.Comparable'
if(smallest.compareTo(a[i]) > 0) {
smallest = a[i];
}
}
return smallest;
}
}
实际上Comparable接口本身就是一个泛型类型,而上述代码中并未指定Comparable接口的类型参数,而是使用了Comparable接口的原始类型,这也是为什么编译器针对上述上述代码给出了两个警告。目前我们先忽略其复杂性和编译器产生的警告。《Java核心技术卷I第十版》的第8.8节将会讨论如何在Comparable接口中适当地使用类型参数。
public interface Comparable<T> {
public int compareTo(T o);
}
按照上述代码为T设置限定后,调用静态方法min()时传入的实际参数只能是实现Comparable接口的对象的数组。例如将一个Object对象组成的数组传入静态方法min(),则会导致编译报错:
java: 无法将类TypeVariableLimitation中的方法min应用到给定类型;
需要: T[]
找到: java.lang.Object[]
原因: 推断类型不符合上限
推断: java.lang.Object
上限: java.lang.Comparable
Java开发新手可能感到奇怪:为什么使用关键字extends而不是implements?毕竟,Comparable是一个接口。这是因为<T extends BoundingType>
中的T应该是绑定类型的子类型(subtype),并且Java的设计者也不打算在语言中再添加一个新的关键字(例如sub)。一个类型变量或者通配符可以有多个限定,例如:<T extends Double & Comparable & Serializable, U extends Integer & Comparable>
,限定类型之间用&分隔,而逗号用来分隔类型变量。
类似于Java规定的至多有一个父类和多个父接口,每个类型变量或者通配符的限定类型也是至多有一个类和多个接口,如果其限定类型中含有类,那么该类必须是限定类型列表中的第一个。例如<T extends Double & Comparable & Serializable, U extends Integer & Comparable>
,其中的Double和Integer是类,所以它们必须放在限定类型列表中的第一个;而Comparable和Serializable是接口,在限定类型列表中只要它们都在Double和Integer等类的后面即可。例如<T extends Comparable & Double>
的写法的报错日志为:Interface expected here
,并给予了修复该问题的操作提示:Move bound java.lang.Double to the beginning of the bounds list of type parameter 'T'
。
1.6 泛型代码和虚拟机
虚拟机中没有泛型类型对象——所有对象都属于普通类。在泛型实现的早期版本中,甚至能够将使用泛型的程序编译为在V1.0虚拟机上运行的类文件。但这个向后兼容性在Java泛型开发的后期被放弃了。
1.6.1 类型擦除
无论何时定义一个泛型类型,编译器都会自动为该泛型类型提供一个对应的原始类型(raw type)。原始类型的名字就是删去尖括号以及其中的类型参数后的泛型类型名,而原始类型中的内容就是将泛型类型中的类型变量进行擦除(erase),并替换为限定类型(如果没有显式指定限定类型,则用默认的限定类型Object)。
例如泛型类型Pair<T>的原始类型便是Pair,原始类型Pair的转化过程分为两个步骤:
- 将泛型类型
Pair<T>
中的<T>
删除,变为Pair
; - 将泛型类型代码中的T全部替换为Object。
最终,其原始类型的完整代码如下所示:
public class Pair {
private Object firstMember;
private Object secondMember;
public Pair(Object firstMember, Object secondMember) {
this.firstMember = firstMember;
this.secondMember = secondMember;
}
public Object getFirstMember() {
return firstMember;
}
public Object getSecondMember() {
return secondMember;
}
public void setFirstMember(Object newValue) {
firstMember = newValue;
}
public void setSecondMember(Object newValue) {
secondMember = newValue;
}
}
在代码中可以使用包含不同类型变量的Pair<T>
,例如Pair<String>
或Pair<LocalDate>
,而擦除类型后就都变成了原始类型Pair。例如下述代码中编译时类型分别为ArrayList、ArrayList<String>和ArrayList<Integer>的三个引用变量,擦除类型后,它们的运行时类型便都变成了ArrayList。
ArrayList arrayList = new ArrayList();
ArrayList<String> stringArrayList = new ArrayList<String>();
ArrayList<Integer> integerArrayList = new ArrayList<Integer>();
System.out.println(arrayList.getClass()); //输出class java.util.ArrayList
System.out.println(stringArrayList.getClass()); //输出class java.util.ArrayList
System.out.println(integerArrayList.getClass()); //输出class java.util.ArrayList
原始类型用限定类型列表中的第一个限定类型来替换,如果没有显式指定限定类型就用Object替换。例如Pair<T>
中的类型变量T没有显式的限定,因此其原始类型就用Object替换T。假定有如下一个不同的泛型类型:
public class Interval<T extends Comparable & Serializable> implements Serializable {
private T lower;
private T upper;
public Interval(T first, T second) {
if (first.compareTo(second) <= 0) {
lower = first;
upper = second;
} else {
lower = second;
upper = first;
}
}
}
那么其原始类型如下所示:
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
public Interval(Comparable first, Comparable second) {
//...
}
}
因为其限定类型列表中的第一个是Comparable,所以将该泛型类型代码中的T全部替换为Comparable,最终作为其原始类型的代码。
那么如果想使用原始类型Interval,则需要注意:在原始类型Interval中,用到了类型变量T的成员变量、局部变量、传入方法的实际参数和方法的返回值都应该为Comparable类型。
Java开发新手可能想知道:按照
class Interval<T extends Serializable & Comparable>
的写法来指定该泛型类型会发生什么。如果这样做,原始类型用Serializable替换T,而编译器在必要时要向Comparable插入强制类型转换。为了提高效率,应该将标签接口放在限定类型列表的末尾。其中标签接口是指Tagging Interface,即没有方法的接口,例如Serializable。上述为《Java核心技术卷I第十版》的第8.5.1节末尾处的原文,但是博主自测时并未发现“编译器在必要时要向Comparable插入强制类型转换”。例如有下述的代码:
import java.io.Serializable; class Interval<T extends Serializable & Comparable> implements Serializable { private T lower; private T upper; public Interval(T first, T second) { if (first.compareTo(second) <= 0) { lower = first; upper = second; } else { lower = second; upper = first; } } } public class GenericsRawType { public static void main(String[] args) { Interval interval = new Interval(new Serializable() {}, new Serializable() {}); //下面的代码会导致编译报错:java: 不兼容的类型: <匿名java.lang.Comparable>无法转换为java.io.Serializable Interval interval2 = new Interval(new Comparable() { @Override public int compareTo(Object o) { return 0; } }, new Comparable() { @Override public int compareTo(Object o) { return 0; } }); Interval<ImplementClass> implementClassInterval = new Interval<>(new ImplementClass(), new ImplementClass()); } } class ImplementClass implements Comparable, Serializable { @Override public int compareTo(Object o) { return 0; } }
执行上述代码便会出现报错日志:java: 不兼容的类型: <匿名java.lang.Comparable>无法转换为java.io.Serializable,并未出现所谓的“编译器在必要时要向Comparable插入强制类型转换”现象。
1.6.2 翻译泛型表达式
当调用泛型方法时,如果擦除返回值类型,编译器会自动且隐式地插入强制类型转换。例如下述代码:
Pair<Employee> buddies = new Pair<>();
buddies.add(new Employee());
Employee buddy = buddies.getFirstMember();
擦除getFirstMember()方法的返回值类型后,该泛型方法将返回Object类型(因为泛型类型Pair<T>
没有显式指定限定类型就用Object替换)。编译器自动且隐式地插入Employee的强制类型转换。也就是说,编译器把这行调用泛型方法的代码翻译为两条虚拟机指令:
- 调用原始类型Pair的getFirstMember()方法;
- 将返回的Object类型隐式地强制转换为Employee类型。
当直接读取一个泛型成员变量时,编译器也会自动且隐式地插入强制类型转换。假设Pair类的成员变量firstMember和secondMember的访问修饰符都是public(这种编码风格严重不符合Java的编码规范,但从Java语法的角度来说是合法的),例如下述代码:
Employee buddy = buddies.firstMember;
也会在结果字节码中插入强制类型转换。
1.6.3 翻译泛型方法
类型擦除也会出现在泛型方法中。程序员通常认为这样的泛型方法public static <T extends Comparable> T min(T[] a)
是一个完整的方法族,而擦除类型之后,只剩下一个方法:
public staic Comparable min(Comparable[] a)
注意,类型参数T已经被擦除了,只留下了限定类型Comparable。
1.6.4 类型擦除引入的问题
泛型方法被擦除类型后带来了两个复杂问题,看看下面的示例。
1.6.4.1 泛型类型子类重写父类方法时的多态冲突
我们首先创建一个继承自Pair<String>的类StringPair,并输入注解@Override
,希望根据IDE的提示来重写父类Pair<String>的方法setSecondMember(T),IDE帮我们重写并自动生成的代码如下所示:
class StringPair extends Pair<String> {
@Override
public void setSecondMember(String newValue) {
super.setSecondMember(newValue);
}
}
我们此时可能有点疑惑,为什么IDE帮我们重写并自动生成的setSecondMember()方法里面的形参类型是String而不是Object呢?这是因为IDE帮我们重写并自动生成代码时处于编译前阶段,而在编译前阶段没有发生类型擦除,那么编译器为了遵循Java泛型类型约束的规则,就会帮我们按照开发者指定的类型变量(如上述代码中指定的String)去检查和约束类型。按照这个理论,我们从头梳理一遍这个逻辑,StringPair继承自Pair<String>,那么编译前未擦除类型的Pair<String>的对应方法为setSecondMember(String),而我们又指定了重写的注解@Override
,因此IDE为了遵循Java重写的规则,去帮我们生成了相同方法名和相同形式参数列表的setSecondMember(String)方法。
接着我们重新编辑这个setSecondMember(String)方法,我们希望传入的参数newValue比原来的成员变量secondMember更"大":
class StringPair extends Pair<String> {
public StringPair(String firstString, String secondString) {
super(firstString, secondString);
}
@Override
public void setSecondMember(String newValue) {
if (newValue != null && getSecondMember() != null && newValue.compareTo(getSecondMember()) >= 0) {
super.setSecondMember(newValue);
}
}
}
接下来,我们对上述代码进行编译,看看会发生什么。编译时最重要的也就是泛型擦除,即StringPair就继承自原型类型Pair,而不是Pair<String>,那么这时StringPair就会额外从原型类型Pair隐式继承setSecondMember(Object)方法,也就是说编译时StringPair就拥有了一对有相同方法名但形式参数列表分别为String和Object的setSecondMember()方法,那么这就是重载的概念,而不是重写的概念。
我们再来看一个测试用例:
Pair<String> pair = new StringPair("firstString", "secondString");
System.out.println(pair.getSecondMember()); //输出secondString
pair.setSecondMember("newSecondString");
System.out.println(pair.getSecondMember()); //理论上我们认为输出newSecondString,但实际输出secondString
针对上述测试用例中的代码pair.setSecondMember("3rdString")
,开发者是希望对setSecondMember()方法的调用具有多态的特性,即当变量pair指向Pair类型的对象时,应当调用Pair对象的setSecondMember(Object)方法;而当变量pair指向StringPair类型的对象时,应当调用StringPair对象的setSecondMember(String)方法。
但是按照上述提到的情况,是重载而不是重写,那么按照多态的规则,由于是变量pair指向StringPair类型的对象并调用了setSecondMember()方法,那么首先在Pair对象中找到了setSecondMember(Object)方法,接着又在StringPair对象中找到有无重写的setSecondMember(Object)方法,结果因为StringPair只有重载的setSecondMember(String),因此最后变量pair应当调用Pair对象的setSecondMember(Object)方法。也就是说我们之前指定重写的注解@Override
,以及IDE帮我们生成的重写的方法在编译时都失去了重写的特性,那么这样就不能利用多态的特性来调用StringPair对象的setSecondMember(String)方法了,这也就是《Java核心技术卷I第十版》的第5章中提到的类型擦除与多态发生了冲突,我个人认为也可以理解为类型擦除与重写发生了冲突,进而推断出类型擦除与多态发生了冲突。
按照上述冲突的理论,我们来预测上述代码的输出结果,因为最后变量pair应当调用Pair对象的setSecondMember(Object)方法,那么secondMember被更新为"newSecondString",因此最后的输出结果为"newSecondString"。因为如果调用StringPair对象的setSecondMember(String)方法,因为"newSecondString"的字典序比原来值为"secondString"的成员变量secondMember要小,所以"newSecondString"无法覆盖"secondString"变成成员变量secondMember的新数值,那么最后的输出结果为"secondString"。
接着我们来执行上述代码,获得的输出结果为:
secondString
secondString
实际的输出结果竟然跟我们的预期相反了,这是为什么?其实这个问题的原因就是Java编译器在StringPair类中隐式地生成了一个桥方法(bridge method):
public void setSecondMember(Object V) {
setSecondMember((String) V);
}
而正是这个桥方法重写了Pair类的setSecondMember(Object),同时解决了上文提到了类型擦除与多态冲突的问题。有了这个桥方法,我们再次复盘一下上述代码的输出结果为何跟预期的不一致。按照多态的规则,首先在Pair对象中找到了setSecondMember(Object)方法,接着又在StringPair对象中找到有重写的隐式桥方法setSecondMember(Object)方法,而这个桥方法又调用了StringPair对象的setSecondMember(String)方法,被传入的"newSecondString"的字典序比原来值为"secondString"的成员变量secondMember要小,所以"newSecondString"无法覆盖"secondString"变成成员变量secondMember的新数值,那么最后的输出结果为"secondString"。
1.6.4.2 引入桥方法后,方法签名的唯一性失效
我们刚介绍了解决争议性多态冲突问题的桥方法,但是引入桥方法后会导致方法签名的唯一性失效,例如下面这个例子,即StringPair类又重写了Pair类的getSecondMember()方法:
class StringPair extends Pair<String> {
public StringPair(String firstString, String secondString) {
super(firstString, secondString);
}
@Override
public void setSecondMember(String newValue) {
if (newValue != null && getSecondMember() != null && newValue.compareTo(getSecondMember()) >= 0) {
super.setSecondMember(newValue);
}
}
@Override
public String getSecondMember() {
return super.getSecondMember();
}
}
按照泛型桥方法的定义,Java编译器实际上也会为我们在StringPair类中隐式添加一个对应的桥方法:
public Object getSecondMember() {
getSecondMember(); //这里调用的是返回值类型为String的getSecondMember()方法
}
我们也可以在StringPair类被反编译后的字节码中看到这个桥方法:
public java.lang.Object getSecondMember();
descriptor: ()Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #2 // Method getSecondMember:()Ljava/lang/String;
4: areturn
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LStringPair;
我们知道,在一个Java类中是不能存在方法签名相同的两个或多个类的,因为我们显式编码的getSecondMember()和隐式添加的桥方法getSecondMember()的方法签名是完全一致的,即使它们的返回值不相同,而这就违背了Java的基本语法。但是Java编译器是用方法名称、参数类型和返回值类型来一个方法的唯一性的,所以即使Java编译器隐式添加了这个桥方法,但是它也能正常地识别并处理这种情况。
桥方法不仅用于泛型类型,Java的重写特性也用到了桥方法。首先再次介绍一下Java重写的规则:
- ⽅法名和参数列表必须相同,返回值类型小于等于父类,抛出的异常范围⼩于等于⽗类,访问修饰符范围⼤于等于⽗类;
- 如果⽗类⽅法访问修饰符为 private/final/static 则⼦类就不能重写该⽅法,但是被 static 修饰
的⽅法能够被再次声明;- 构造⽅法⽆法被重写。
《Java核心技术卷I第十版》的第5章也提到过,在一个方法重写另一个方法时,可以为重写方法的返回值指定一个相同或者更严格的类型。例如:
public class Employee implements Cloneable { public Employee clone throws CloneNotSupportedException {} }
另外我们知道Object类的clone()方法定义如下:
protected native Object clone() throws CloneNotSupportedException;
在上述代码中,Object类的clone()方法的返回值类型为Object,而Employee类重写了Object类的clone()方法,并且返回了更严格的返回值类型Employee。
Object类和Employee类的clone()方法被定义为具有协变的返回类型(covariant return types)。
通过被反编译后的字节码能看到,Employee是有下述两个clone()方法:
Employee clone() {} Object clone() {} //Java编译器隐式生成的桥方法,重写了Object类的clone()方法
也就是说,Java编译器认定的重写是父类和子类的方法的方法名称、参数类型和返回值类型必须一致,但是上述Employee类中指定了更严格的返回值类型的clone()方法之所以符合Java重写的规则,是因为Java编译器隐式地添加了桥方法,而这个桥方法的方法体里调用了那个指定了更严格的返回值类型的clone()方法。
1.6.4.3 泛型类型中的方法冲突
//在泛型类型Pair<T>中编码一个下述的equals()方法
public class Pair<T> {
//编译报错:'equals(T)' in 'Test' clashes with 'equals(Object)' in 'java.lang.Object'; both methods have same erasure, yet neither overrides the other
public boolean equals(T value){
return (first.equals(value));
}
}
这样看似乎没有问题的代码连编译器都通过不了:编译器说你的方法与Object中的方法冲突了。这是为什么?
开始我也不太明白这个问题,觉得好像编译器帮助我们使得equals(T)这样的方法覆盖上了Object中的equals(Object)。经过大家的讨论,我觉得应该这么解释这个问题?
首先、我们都知道子类方法要覆盖,必须与父类方法具有相同的方法签名(方法名+参数列表)。而且必须保证子类的访问权限>=父类的访问权限。这是大家都知道的事实。
然后、在上面的代码中,当编译器看到Pair<T>中的equals(T)方法时,第一反应当然是equals(T)没有覆盖住父类Object中的equals(Object)了。
接着、编译器将泛型代码中的T用Object替代(擦除)。突然发现擦除以后equals(T)变成了equals(Object),糟糕了,这个方法与Object类中的equals一样了。基于开始确定没有覆盖这样一个想法,编译器彻底的疯了(精神分裂)。然后得出两个结论:①坚持原来的思想:没有覆盖。但现在一样造成了方法冲突了。 ②写这程序的程序员疯了(哈哈)。
再说了,拿Pair对象和T对象比较equals,就像牛头对比马嘴,哈哈,逻辑上也不通呀。
1.6.5 验证桥方法是否存在
1.6.5.1 反编译字节码
不知道你是否跟博主有一样的想法,《Java核心技术卷I第十版》的第5章提到解决类型擦除与多态冲突的问题的原因是Java编译器在StringPair类中隐式地生成了一个桥方法,但是我们怎么去感知这个桥方法是否是真实存在的呢?首先,我们可以通过反编译字节码的方式来看见这个隐式的桥方法。在StringPair.class文件所在路径的终端输入命令:javap -verbose -c -l StringPair.class
,即可生成字节码被反编译后的代码:
Classfile /Users/didi/Desktop/git_test/Generics/out/production/Generics/StringPair.class
Last modified 2021-5-15; size 857 bytes
MD5 checksum 5e4ef08f51e8dc53ee25d50bb232d13c
Compiled from "StringPair.java"
class StringPair extends Pair<java.lang.String>
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #8.#28 // Pair."<init>":(Ljava/lang/Object;Ljava/lang/Object;)V
#2 = Methodref #7.#29 // StringPair.getSecondMember:()Ljava/lang/Object;
#3 = Class #30 // java/lang/String
#4 = Methodref #3.#31 // java/lang/String.compareTo:(Ljava/lang/String;)I
#5 = Methodref #8.#32 // Pair.setSecondMember:(Ljava/lang/Object;)V
#6 = Methodref #7.#33 // StringPair.setSecondMember:(Ljava/lang/String;)V
#7 = Class #34 // StringPair
#8 = Class #35 // Pair
#9 = Utf8 <init>
#10 = Utf8 (Ljava/lang/String;Ljava/lang/String;)V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 LStringPair;
#16 = Utf8 firstString
#17 = Utf8 Ljava/lang/String;
#18 = Utf8 secondString
#19 = Utf8 setSecondMember
#20 = Utf8 (Ljava/lang/String;)V
#21 = Utf8 newValue
#22 = Utf8 StackMapTable
#23 = Utf8 (Ljava/lang/Object;)V
#24 = Utf8 Signature
#25 = Utf8 LPair<Ljava/lang/String;>;
#26 = Utf8 SourceFile
#27 = Utf8 StringPair.java
#28 = NameAndType #9:#36 // "<init>":(Ljava/lang/Object;Ljava/lang/Object;)V
#29 = NameAndType #37:#38 // getSecondMember:()Ljava/lang/Object;
#30 = Utf8 java/lang/String
#31 = NameAndType #39:#40 // compareTo:(Ljava/lang/String;)I
#32 = NameAndType #19:#23 // setSecondMember:(Ljava/lang/Object;)V
#33 = NameAndType #19:#20 // setSecondMember:(Ljava/lang/String;)V
#34 = Utf8 StringPair
#35 = Utf8 Pair
#36 = Utf8 (Ljava/lang/Object;Ljava/lang/Object;)V
#37 = Utf8 getSecondMember
#38 = Utf8 ()Ljava/lang/Object;
#39 = Utf8 compareTo
#40 = Utf8 (Ljava/lang/String;)I
{
public StringPair(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3
0: aload_0
1: aload_1
2: aload_2
3: invokespecial #1 // Method Pair."<init>":(Ljava/lang/Object;Ljava/lang/Object;)V
6: return
LineNumberTable:
line 3: 0
line 4: 6
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this LStringPair;
0 7 1 firstString Ljava/lang/String;
0 7 2 secondString Ljava/lang/String;
public void setSecondMember(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_1
1: ifnull 30
4: aload_0
5: invokevirtual #2 // Method getSecondMember:()Ljava/lang/Object;
8: ifnull 30
11: aload_1
12: aload_0
13: invokevirtual #2 // Method getSecondMember:()Ljava/lang/Object;
16: checkcast #3 // class java/lang/String
19: invokevirtual #4 // Method java/lang/String.compareTo:(Ljava/lang/String;)I
22: iflt 30
25: aload_0
26: aload_1
27: invokespecial #5 // Method Pair.setSecondMember:(Ljava/lang/Object;)V
30: return
LineNumberTable:
line 7: 0
line 8: 25
line 10: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 this LStringPair;
0 31 1 newValue Ljava/lang/String;
StackMapTable: number_of_entries = 1
frame_type = 30 /* same */
public void setSecondMember(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #3 // class java/lang/String
5: invokevirtual #6 // Method setSecondMember:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LStringPair;
}
Signature: #25 // LPair<Ljava/lang/String;>;
SourceFile: "StringPair.java"
我们可以在上述反编译字节码的代码末尾处看到有一个方法为public void setSecondMember(java.lang.Object)
,而这个方法就是所说的Java编译器在StringPair类中隐式地生成的桥方法。我们可能还会疑问,怎么确定这个方法并不是StringPair类的父类Pair的setSecondMember(Object)方法呢?其实这点我们可以在上述反编译字节码的代码中该桥方法的Code处来辨别。
首先该桥方法有一个类型为Object形式参数V,桥方法的Code又有checkcast指令来检查并进行强制类型转换,接着便调用了StringPair对象的setSecondMember:(Ljava/lang/String;)方法,并传入V作为该方法的实际参数。
Java字节码类型检查指令checkcast:用于检查类型强制转换是否可以进行。如果可以进行则进行强制类型转换,同时checkcast指令不会改变操作数栈;如果不可以进行,那么它会抛出ClassCastException异常。
通过观察桥方法的Code,我们可以确信反编译字节码的代码末尾处的该方法是桥方法,并且与《Java核心技术卷I第十版》的第5章中所描述的隐式桥方法的Java代码相似:
public void setSecondMember(Object V) {
setSecondMember((String) V);
}
1.6.5.2 显式编码桥方法
我们都知道Java编译器在StringPair类中隐式地生成的桥方法,如果我们显式地把桥方法编码出来会怎么样呢,新版StringPair类代码如下:
class StringPair extends Pair<String> {
public StringPair(String firstString, String secondString) {
super(firstString, secondString);
}
@Override
public void setSecondMember(String newValue) {
if (newValue != null && getSecondMember() != null && newValue.compareTo(getSecondMember()) >= 0) {
super.setSecondMember(newValue);
}
}
public void setSecondMember(Object V) {
setSecondMember((String) V);
}
}
当我们运行上述代码时,果不其然报错了:
'setSecondMember(T)' in 'Pair' clashes with 'setSecondMember(Object)' in 'StringPair'; both methods have same erasure, yet neither overrides the other.
把上述英文报错文案翻译成中文,如下所示:
在类型擦除后,Pair对象的setSecondMember(T)方法和StringPair对象的setSecondMember(Object)拥有相同的方法签名,但是每个方法都不能重写而覆盖掉对方。
其中,上述报错提到的方法签名是指Java的方法签名。Java的方法签名是由方法名称和方法的形参列表共同组成的,它主要用于确定Java方法的唯一性。
但是上述的报错原因还是无法让人理解,总不能说显式桥方法和隐式桥方法冲突了,因为如果我们把上述StringPair对象重写的setSecondMember(String)方法删除掉,上述报错还是会存在的,修改后的代码如下:
class StringPair extends Pair<String> {
public StringPair(String firstString, String secondString) {
super(firstString, secondString);
}
public void setSecondMember(Object V) {
setSecondMember((String) V);
}
}
如果我们上网去搜索上述的报错文案,大都会找到这篇文章《泛型类型擦除与重载和覆盖问题》,这篇文章对这个报错现象的解释为:
public class Father { void test(Object o){}}class Son<T> extends Father{ void test(T o){}//编译错误!}
泛型类型在编译后会做类型擦除,只剩下原生类型。如参数列表中的T类型会编译成Object,但是会有一个Signature。尽管两个test()方法具有相同的字节码,但是类型参数信息用一个新的签名(signature)属性记录在类模式中。JVM在装载类时记录这个签名信息,并在运行时通过反射使它可用。这就导致了这个方法既不能作为覆盖父类test()方法的方法,也不能作为test()方法的重载。
其实我觉得上述的解释文案很牵强,例如在泛型擦除后这两个方法的方法签名本来就是一样的,因为它们的方法名称和形参列表本来就是一样的。
1.6.5.3 验证桥方法是否存在——显式调用桥方法
如果我们不按照上述提到的显式编码桥方法,而是显式调用桥方法,代码如下:
Pair<String> pair = new StringPair("firstString", "secondString");pair.setSecondMember((Object) "newSecondString"); //编译报错:java: 不兼容的类型: java.lang.Object无法转换为java.lang.String
结果代码异常地报错了,这并不符合我们的预期,因为既然代码pair.setSecondMember("newSecondString")
能被正常调用,则说明桥方法是被调用过的,但是为什么我们显式调用桥方法却编译报错了呢?
1.6.5.4 泛型桥方法特性总结
通过上述的案例,我们可以发现:泛型桥方法是存在的,但是它只能被Java编译器隐式编码和调用,而不能被显式编码和调用。
在类型擦除后,一条继承链上的类中,我们定义两个集合:形式参数被类型擦除的方法的方法签名集合和形式参数没有被类型擦除的方法的方法签名集合,这两集合没有交集,那么就不会出现both methods have same erasure, yet neither overrides the other
的报错文案,我个人认为这是Java编译器为了规避隐式引入桥方法从而导致冲突而设置的规则。
1.6.6 调用遗留代码
设计Java泛型类型时,主要目标是允许泛型代码和遗留代码之间能够互操作。下面看一个具体的示例。要想设置一个JSlider标签,可以使用方法:
void setLabelTable(Dictionary table)
在这里,Dictionary是一个原始类型,因为实现JSlider类时Java中还不存在泛型。不过,填充字典时,要使用泛型类型:
import javax.swing.*;
import java.awt.*;
import java.util.Dictionary;
import java.util.Hashtable;
public class LegacyCode {
public static void main(String[] args) {
Dictionary<Integer, Component> labelTable = new Hashtable<>();
labelTable.put(0, new JLabel(new ImageIcon("nine.gif")));
labelTable.put(20, new JLabel(new ImageIcon("ten.gif")));
JSlider slider = new JSlider();
slider.setLabelTable(labelTable); //虽然《Java核心技术卷I第十版》说有编译警告,但在IntelliJ实际运行这行代码时没有编译警告
labelTable = slider.getLabelTable();
}
}
将Dictionary<Integer, Component>对象labelTable传递给setLabelTable()方法时,编译器不会发出警告,因为把具有参数类型的对象赋值给原始类型的引用变量时不会有任何问题:
slider.setLabelTable(labelTable); //虽然《Java核心技术卷I第十版》说有编译警告,但在IDE实际运行时没有编译警告
但是也正是因为Dictionary没有泛型中的限定类型特性来约束Dictionary对象的键值对的值的上限类型,所以可能会导致IDE在编译时检查不出错误,例如调用setLabelTable()方法传入错误的Dictionary对象,最后导致程序在运行时抛出异常。例如有这样一个例子:
Dictionary<Double, Object> labelTable2 = new Hashtable<>();
labelTable2.put(30.0, new Object());
JSlider slider = new JSlider();
slider.setLabelTable(labelTable2); //编译时没有任何警告和报错,而程序运行到这行代码时会抛出异常
其中上述代码的报错文案如下:
Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to javax.swing.JComponent
at javax.swing.JSlider.updateLabelUIs(JSlider.java:856)
at javax.swing.JSlider.setLabelTable(JSlider.java:830)
at LegacyCode.main(LegacyCode.java:17)
我们看一下JSlider类的updateLabelUIs()方法的源码:
protected void updateLabelUIs() {
Dictionary labelTable = getLabelTable();
if (labelTable == null) {
return;
}
Enumeration labels = labelTable.keys();
while ( labels.hasMoreElements() ) {
JComponent component = (JComponent) labelTable.get(labels.nextElement()); //这行强制转换类型的代码导致程序抛出异常
component.updateUI();
component.setSize(component.getPreferredSize());
}
}
由上述代码我们可以发现,因为Dictionary的源码是在JDK1.0时定义的,所以Dictionary的源码不能用泛型来约束Dictionary对象的键值对的值的上限类型,而是直接进行强制类型转换,这样的坏处就是Java编译器不能在编译时检查出这个错误,而是只能通过程序在运行时通过抛出异常来发现这个错误。这个案例也告诉了我们泛型的重要性。
但是如果反过来,把原始类型的对象赋值给含参数类型的引用变量,这就会产生编译警告:
labelTable = slider.getLocalTable(); //产生编译警告:Unchecked assignment: 'java.util.Dictionary' to 'java.util.Dictionary<java.lang.Integer,java.awt.Component>' ;有的编译器有如下的警告:Unchecked conversion from raw to generics
把这句编译警告文案“Unchecked conversion from raw to generics”翻译一下,即:Java编译器并未对这行将原型类型转换为泛型类型的代码进行安全性检查,为什么Java编译器不进行检查呢,因为Java编译器并不能检查该集合元素的运行时类型是否符合泛型类型的约束。
因为getLocalTable()方法返回的是JDK1.0版本的没有泛型约束的Dictionary对象,即编译器无法保证等式右边得到的原始类型的Dictionary对象的键值对元素的类型是<Integer, Component>,所以编译器才会产生这个编译警告。
如果不想看见这个编译警告,我们可以使用注解@SuppressWarning
来抑制这个编译警告的提示,这个注释可以放在包含这行代码的方法的方法头前:
public class LegacyCode {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Dictionary<Integer, Component> labelTable = new Hashtable<>();
labelTable.put(0, new JLabel(new ImageIcon("nine.gif")));
labelTable.put(20, new JLabel(new ImageIcon("ten.gif")));
JSlider slider = new JSlider();
slider.setLabelTable(labelTable);
labelTable = slider.getLabelTable();
}
}
1.7 泛型的约束与局限性
在下面几节中,将阐述使用Java泛型时存在的一些限制。大部分限制都是由类型擦除引起的。
1.7.1 不能使用基本类型实例化类型参数
不能用类型参数代替基本类型。因此没有Pair<double>,只有Pair<Double>。当然其原因是类型擦除。因为类型擦除后,Pair类含有Object类型的域,而Object不能存储double值。这的确让人烦恼,但是这样做与Java语言中基本类型的独立状态相一致,这并不是个致命的缺陷——因为只有8种基本类型,当包装器类型(wrapper type)不能接受替换时,可以使用独立的类和方法来处理它们。
1.7.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询值产生原始类型。例如:
Object object = new Object();
if (object instanceof Pair<String>) {} //编译报错:Illegal generic type for instanceof
实际上仅仅测试object是否是任意类型的一个Pair,下面的测试用例同样有误:
if (object isntanceof Pair<T>) {} //编译报错:Cannot resolve symbol 'T'
或者强制类型转换:
Pair<String> p = (Pair<String>) object; //编译警告:Casting 'object' to 'Pair<String>' will produce 'ClassCastException' for any non-null value
为了提醒Java开发者关于“运行时类型查询只适用于原始类型”的规则,当试图用instanceof查询一个对象是否属于某个泛型类型时,会导致编译报错;如果使用强制类型转换会得到一个警告。
同理,getClass()方法综合返回原始类型,例如:
Pair<String> stringPair = new Pair<>("one", "two");
Pair<Integer> doublePair = new Pair<>(1, 2);
System.out.println(stringPair.getClass() == doublePair.getClass()); //输出true,甚至运行前编译器会提示:Condition 'stringPair.getClass() == doublePair.getClass()' is always 'true'
其比较结果是true,这是因为两次调用getClass()都讲返回Pair.class。
1.7.3 不能创建参数化类型的数组
不能实例化参数化类型的数组,例如:
Pair<String>[] table = new Pair<String>[10]; //编译器报错:Generic array creation
在了解为什么不能用上述代码创建泛型数组之前,我们来看看下面这个测试用例:
Pair<String>[] table = new Pair[10];
Object[] objectArray = table;
objectArray[0] = "String"; //因为Java数组是型变的,所以编译器产生警告但不报错:Storing element of type 'java.lang.String' to array of 'Pair' elements will produce 'ArrayStoreException',但这行代码运行时会抛出异常ArrayStoreException
objectArray[1] = new Pair<Integer>(1, 2); //因为Java数组是型变的,所以这里也不会导致编译报错,但比较奇怪这里没有编译警告
System.out.println(table[1].getFirstMember()); //这行代码编译器不警告也不报错,但运行时会抛出异常:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
在上述代码中,我们创建了一个Pair[]类型的数组并用Pair<String>[]类型的引用变量table引用它,因为Java数组是型变的,所以我们可以创建一个Object[]类型的引用变量objectArray引用它,这时我们通过objectArray向数组里存放一些不同于原始Pair类型的元素,在编译时Java编译器只会产生警告说这样的代码有可能在运行时抛出ArrayStoreException异常,而不会编译报错阻止运行,而是在运行时这行代码确实就抛出ArrayStoreException异常了。这个理论可由上述的代码objectArray[0] = "String";
的执行结果来论证。
那么再看看上述代码块的最后两行代码,首先,因为Java数组是型变的,所以objectArray[1] = new Pair<Integer>(1, 2);
这行代码不会导致编译报错。另外因为数组元素table[1]所在数组被引用的变量类型是Pair<String>[],因此Java编译器会对table[1].getFirstMember()
的表达式前自动且隐式地加上(String)
进行强制类型转换,而我们还知道数组元素table[1]的实际类型为Pair<Integer>,但是为什么在编译时这行代码既没有编译警告也没有编译报错呢?
这是因为Java编译器并不能完美地联系上下文代码,来确定table[1].getFirstMember()
的表达式会返回Integer类型的对象,它只知道getFirstMember()
是泛型类中返回值类型为没有类型限制的参数化类型,那么编译器就默认为getFirstMember()
应该是返回Object类型,接着它又隐式地将其转换为String类型,这是符合编译器的逻辑的,这也就是为什么编译器针对这行代码没有警告和报错的原因,但是Java虚拟机在运行时发现getFirstMember()
会返回Integer类型的对象,从而无法进行Integer到String的类型转换,从而抛出异常ClassCastException。
上面的两个案例说明了:在已知Java数组型变特性的缺陷的前提下,使用声明类型为泛型数组的变量来引用原型类型数组所带来的问题。那么Java不允许创建泛型数组是不是跟这个有关呢?其实是的,如果Java允许创建Java泛型数组,也会因为Java数组型变的特性从而存在类似的缺陷。在知乎的问题《java为什么不支持泛型数组?》中,阿里巴巴员工ylxfc是这样回答的:
因为这样做会破坏类型安全。在Java中,Object[]数组可以是任何数组的父类,或者说,任何一个数组都可以向上转型成它在定义时指定元素类型的父类的数组,这个时候如果我们往里面放不同于原始数据类型 但是满足后来使用的父类类型的话,编译不会有问题,但是在运行时会检查加入数组的对象的类型,于是会抛ArrayStoreException:
Object[] objArray = new String[20];objArray[0] = new Integer(1); // throws ArrayStoreException at runtime
因为Java的范型会在编译后将类型信息抹掉,这样如果Java允许我们使用类似
Map<Integer, String>[] mapArray = new Map<Integer, String>[20];
这样的语句的话,我们在随后的代码中可以把它转型为Object[]然后往里面放Map<Double, String>实例。这样做不但编译器不能发现类型错误,就连运行时的数组存储检查对它也无能为力,它能看到的是我们往里面放Map的对象,我们定义的<Integer, String>在这个时候已经被抹掉了,于是而对它而言,只要是Map,都是合法的。想想看,我们本来定义的是装Map<Integer, String>的数组,结果我们却可以往里面放任何Map,接下来如果有代码试图按原有的定义去取值,后果便是运行时抛出异常。
但我个人认为Java不允许创建Java泛型数组,仅仅是因为Java的设计者制定了这样的规则而已,而这个规则只是恰好规避了显式创建泛型数组带来的问题。就像上述的两个测试用例,即使Java不允许创建Java泛型数组,Java数组型变的特性的缺陷带来的问题还是存在的。另外,在下文中我们可以学习到:通过可变形参还是可以隐式的创建泛型数组的。
需要说明的是,只是不允许创建这些数组,而声明类型为Pair<String>[]
的变量仍是合法的,只是说不能用new Pair<String>[10]
实例化一个参数化类型的数组。另外我们需要注意到Java允许声明类型为类似Pair<String>[]
的泛型数组类型的变量,但这并不代表它不会存在问题,上述代码中由数组型变引入的问题还是存在的,这个问题不会因为禁止泛型数组,允许泛型数组类型的引用变量而消失。
可以声明通配类型的数组,然后进行类型转换:
Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];
但这种形式的table数组仍然不是安全的,它还是能存入其他不符合参数化类型的元素,例如在table[0]中存入一个
Pair<Integer>
。然后对table[0].getFirst()调用一个String类有而Integer类没有的方法,那么会导致ClassCastException。《Java核心技术卷I第十版》提示我们,如果需要收集参数化类型对象,只有一种安全而有效的方法,即使用ArrayList来替代数组,例如
ArrayList<Pair<String>>
。即应该集合能确保作为参数化类型对象的集合元素的类型安全性,而不是数组。
总的来说,这一小节主要是详细阐述了泛型和数组结合后的语法规则:
- Java语法规定不允许创建泛型数组;
- Java允许声明泛型数组类型的变量,但数组型变特性的缺陷还是存在的。
其实我个人认为讲解了这么多了泛型和数组结合后的语法规则,最终的目的应该就是引导Java开发者在脑海中有这样的概念:就像《Java核心技术卷I第十版》在这节结尾提到的,尽量避免在代码中结合使用泛型和数组,而是应该用更安全的泛型集合。
1.7.4 Varargs警告
其中本节中的Varargs是指Java的可变形参(Variable number of argumens)。这一节我们再讨论一个相关问题:向参数个数可变的方法传递一个泛型类型的实例。
考虑下面这个简单的泛型方法,它的参数个数是可变的:
public static <T> void addAll(Collection<T> collection, T... tArray) { for (int i = 0; i < tArray.length; i++) { collection.add(tArray[i]); }}
如果对Java可变形参的知识点较为熟悉的话,应该知道上面方法的形参tAttay的类型是T[],一个包含所有实际参数的数组。现在有下述要执行的代码:
public class Varargs {
public static void main(String[] args) {
Collection<Pair<String>> stringPairCollection = new ArrayList<>();
Pair<String> stringPair1 = new Pair<>();
Pair<String> stringPair2 = new Pair<>();
addAll(stringPairCollection, stringPair1, stringPair2);
}
}
为了调用上述代码中的addAll()方法,Java虚拟机就必须建立一个Pair<String>数组来存储该方法的实际参数stringPair1, stringPair2
,但这就违反了Java泛型的规则——不能创建参数化类型的数组。不过对于这种情况,Java编译器的检查规则会有所放松,即会产生一个编译警告,而不是一个编译错误:
Unchecked generics array creation for varargs parameter
把这句编译警告文案“Unchecked generics array creation for varargs parameter”翻译一下,即:针对因可变形参而破例创建泛型数组的这行代码,Java编译器并未进行安全性检查。为什么Java编译器明知这行代码有风险但又不进行检查呢,因为在可变形参的特殊情况下,Java编译器是破例创建了泛型数组,而Java数组型变特性的缺陷导致Java编译器不能检查出泛型数组的问题。
另外如果我们细心的话,也可以发现Java编译器对addAll()方法也进行了警告:
Possible heap pollution from parameterized vararg type
把这句编译警告文案“Possible heap pollution from parameterized vararg type”翻译一下,即:因可变形参而破例创建泛型数组可能会造成堆污染。
我们先来了解一下什么是堆污染:
在Java语言中,当一个参数化的类型的变量引用一个其对应的原始类型时,堆污染就可能会发生。
看完Java堆污染的定义,我们会觉得很抽象,那我们结合上述的addAll()方法的代码来实际分析一下堆污染是什么。
不过,我们可以采用两种方法来抑制这些警告。一种方法是为调用addAll()的方法增加注释@SuppressWarnings(“unchecked”),例如上述代码中的main()方法,那么可以写成:
public class Varargs {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
//...
addAll(stringPairCollection, stringPair1, stringPair2);
}
}
按照上述的写法,编译器就不再对main()方法中调用的addAll()方法进行编译警告。
另一种写法就是在Java7及以上版本中,可以用@SafeVarargs直接标注addAll()方法:
@SafeVarargs
public static <T> void addAll(Collection<T> collection, T... tArray) {
//...
}
按照上述的写法,编译器就不再对addAll()方法和其他地方调用的addAll()方法进行编译警告。
但是,我们需要注意的是,以上通过注解来消除编译警告的问题并没有解决堆污染的问题,所以我们应该有这样的意识:尽量不要创建包含泛型可变形参的方法,也尽量不要调用包含泛型可变形参的方法,因为这会产生堆污染的隐患,正确的做法是用泛型集合来代替泛型可变形参和泛型数组。
1.7.5 不能实例化类型变量(类型参数)
不能使用像new T(…),new T[…]或者T.class这样的表达式中的类型变量。例如,像下面的Pair<T>构造方法就是非法的:
public Pair() {
first = new T(); //编译直接报错:Type parameter 'T' cannot be instantiated directly
second = new T();
}
类型擦除将T改变成Object,而且开发者编码new T()
的本意肯定不希望调用new Object()。在Java8及以后该问题最好的解决办法是让调用者提供一个构造器表达式。例如我们在Pair泛型类里额外增加一个静态方法makePair():
import java.util.function.Supplier;
class Pair<T> {
//...
public static <U> Pair<U> makePair(Supplier<U> supplier) {
return new Pair<>(supplier.get(), supplier.get());
}
}
makePair()静态方法接受一个Supplier<T>类型的形参,Supplier<T>是一个函数式接口,它有一个T get()
抽象方法,表示一个无参数而且返回类型为T的函数。接着我们编码一个测试用例来调用makePair()静态方法:
Pair<String> p = Pair.makePair(String::new);
上面编码String::new
涉及到Java8新特性:函数式接口Supplier、Lambda表达式、方法以及构造方法引用的知识点,可以参考文章《Java 8 函数式接口、lambda表达式、方法以及构造器引用》和《Java8学习之Supplier与函数式接口总结》。如果看不懂上述的代码,下面的代码跟上述代码是等价的:
Supplier<String> stringSupplier = new Supplier<String>() { @Override public String get() { return new String(); }};Pair<String> p = Pair.makePair(stringSupplier);
上述的代码案例就是告诉我们,在Java8及以后的Java版本中,我们可以通过实例化Supplier接口,重写它的get()方法来近似地实例化一个类型变量并将其作为该方法的返回值。
另外,比较传统的解决方法是通过反射调用Class.newInstance方法来构造泛型对象。但遗憾的是细节有点复杂,且不能调用,例如我们在Pair泛型类中定义一个无参的setFirstMember()方法:
class Pair<T> {
private T firstMember;
//...
public void setFirstMember(T newValue) {
firstMember = newValue;
}
public void setFirstMember() {
firstMember = T.class.getConstructor().newInstance(); //编译报错:Cannot select from a type variable
}
}
上述代码中的表达式T.class
是不合法的,因为这个表达式在类型擦除后会变成Object.class,必须适当地设计API以便得到一个Class对象。例如我们重写上述Pair泛型类的静态方法makePair():
public class Pair<T> {
//...
public static <U> Pair<U> makePair(Class<U> classU) {
try {
return new Pair<>(classU.getConstructor().newInstance(),
classU.getConstructor().newInstance());
} catch (Exception e) {
return null;
}
}
}
对于上述的静态方法makePair(),我们可以这样调用:
Pair<String> stringPair = Pair.makePair(String.class);
注意,Class类本身是泛型。例如,String.class是一个Class<String>的实例(事实上它是唯一的实例)。因此makePair()方法能够推断出stringPair的类型。
1.7.6 不能构造泛型数组
就像不能实例化一个泛型实例一样,也不能实例化泛型数组。不过原因有所不同,毕竟数组会填充null值,构造时看上去是安全的。不过,数组本身也有类型,用来监控存储在虚拟机中的数组。这个类型会被擦除。例如,考虑下面的例子:
public static <T extends Comparable> T[] minmax(T[] a) {
T[] mn = new T[2];
}
类型擦除特性会让这个方法永远构造Comparable[2]数组。
如果数组仅仅作为一个类的私有实例域,就可以将这个数组声明为Object[],并且在获取元素时进行类型转换。例如,ArrayList类可以这样实现:
public class ArrayList<E> {
private Object[] elements;
@SuppressWarnings("unchecked")
public E get(int n) {
return (E) elements[n];
}
public void set(int n, E e) {
elements[n] = e;
}
}
实际的实现没有这么清晰:
public class ArrayList<E> {
private E[] elements;
public ArrayList() {
elements = (E[]) new Object[10];
}
}
这里,强制类型转换E[]是一个假想,而类型擦除使其无法察觉。
1.7.7 泛型类的静态上下文中类型变量无效
不能在静态变量或静态方法中引用泛型类的类型变量。例如,下述代码将无法通过编译:
public class Singleton<T> {
private static T singleInstance; //编译报错:'Singleton.this' cannot be referenced from a static context
//编译报错:'Singleton.this' cannot be referenced from a static context
public static T getSingleInstance() {
//由于实际代码无法实现,故该行用伪代码的注释表示,即构建一个参数类型为T的SingleInstance实例
return singleInstance;
}
}
如果这个程序能够运行,就可以声明一个Singleton<Random>共享随机数生成器,声明一个Singleton<JFileChooser>共享文件选择器对话框。但是这个程序运行时会导致编译报错。类型擦除之后,只剩下Singleton类,它只包含一个Object类型的实例变量singleInstance。因此,不能在静态变量或静态方法中引用泛型类的类型变量。
泛型类的类型变量只能在泛型类的实例变量和实例方法中使用,而静态变量和静态方法是被该类和该类所有实例对象所共享的,如果允许在静态变量或静态方法中引用泛型类的类型变量,那么应用不同实际类型参数的各个参数化类型将矛盾地限制静态变量和静态方法的参数化类型,例如有下述伪代码:
String stringSingleInstance = new Singleton<String>();
Integer integerSingleInstance = new Singleton<Integer>();
Object obscureObject = Singleton.getSingleInstance();
如果上述伪代码能正常执行,那么按照泛型的规则,Singleton类的静态方法getSingleInstance()的返回值到底是强制转换为String类型还是Integer类型呢?在这种情况下,泛型类和静态这两概念发生了冲突,因此Java语法规定:不能在静态变量或静态方法中引用泛型类的类型变量。
但是我们需要知道的是,泛型方法可以是静态或者非静态方法的。结合泛型方法的知识点,我们对比下它们两者的区别:
-
泛型类的类型参数可以修饰实例变量的类型、实例方法的形参类型、实例方法体中局部变量的类型、实例方法的返回值类型,而不能在静态变量和静态方法的任何位置进行修饰。
-
泛型方法的类型参数可以修饰泛型方法的形参类型、泛型方法体中局部变量的类型、泛型方法的返回值类型,其中泛型方法可以是静态或非静态的。
这两者区别的示例代码:
public class GenericMethodClass<T> {
/** 泛型类跟静态泛型方法的类型参数T重名,但作用域无交集,因此它们不冲突 */
public static <T> void printClassByStaticMethod(T t) {
System.out.println(t.getClass());
}
/**
* 编译器警告:Type parameter 'T' hides type parameter 'T',
* 这是因为泛型类跟其实例泛型方法的类型参数T重名,作用域有交集,
* 因此在该实例泛型方法体中,该实例泛型方法的类型参数T生效。
*/
public <T> void printClassByInstanceMethod(T t) {
System.out.println(t.getClass());
}
public T returnItself(T t) {
return t;
}
public void emptyMethod() {}
public static void main(String[] args) {
GenericMethodClass<String> genericMethodClass = new GenericMethodClass<>();
//编译提示:Explicit type arguments can be inferred,即我们显式指定的<Object>可以不用写
//,因为编译器可通过上下文推断出这个泛型方法的实际类型参数
GenericMethodClass.<Object>printClassByStaticMethod(new Object());
//可以用对象来调用静态泛型方法,但是不推荐这么做,因为这种做法不符合泛型方法的设计初衷
genericMethodClass.<Object>printClassByInstanceMethod(new String());
//也可以在实例方法中显式指定实际类型参数,但是编译器会提示:Type arguments are redundant for the non-generic method call
//,也就是说对于非泛型方法显式指定实际类型参数是无效的,因为非泛型方法的实际类型参数是在该对象初始化时就指定了,而不是像泛型方法需要
// 显式指定或者系统通过上下文推断,即使像下面代码指定了Integer,但returnItself()方法的返回值还是String类型
//,因为非泛型方法的实际类型参数在创建对象时就已经显式指定了
String string = genericMethodClass.<Integer>returnItself(new String());
//也可以在无类型参数的实例方法中显式指定实际类型参数,但这也是无效的,原因同上
//,因此编译器也会提示:Type arguments are redundant for the non-generic method call
genericMethodClass.<String>emptyMethod();
}
}
1.7.8 不能抛出或捕获泛型类的实例
泛型类继承Throwable或Exception都是不合法的。例如,以下代码就不能正常编译:
//编译报错:a generic class may not extend java.lang.Throwable
public class GenericException<T> extends Exception {}
//编译报错:a generic class may not extend java.lang.Throwable
class GenericThrowable<T> extends Throwable {}
因为泛型类不能继承Throwable或Exception,因此既不能抛出也不能捕获泛型类对象。
另外try-catch语句中的catch子句不能使用类型变量。例如,以下代码就不能正常编译:
public static <V extends Throwable> void catchTypeVariable() {
try {
}
//Cannot catch type parameters
catch (V v) {
}
}
但是直接或间接继承Throwable或Exception的类型变量可被正常抛出。以下代码是合法的:
public static <U extends Throwable> void throwsTypeVariable(U u) throws U {
try {
} catch (Throwable throwable) {
u.initCause(throwable);
throw u;
}
}
1.7.9 可以消除对受检查异常的检查
Java异常处理的一个基本原则是:必须为所有受检查异常提供一个异常处理器。不过可利用泛型消除这个限制,例如有一个静态方法throwAs():
@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T {
//如果没有上面的注解@SuppressWarnings("unchecked"),则此处编译器会警告:Unchecked cast: 'java.lang.Throwable' to 'T',即未经检查的、不安全的强制类型转换
throw (T) e;
}
针对下述代码中的GenericException.<RunnableException>throwAs(ioException)
,编译器会认为ioException是一个不受检查异常,即以下代码会把不管什么异常类型的ioException都转换为编译器所认为的不受检查异常:
import java.io.IOException;
public class GenericException<T extends Exception> {
public static void main(String[] args) {
IOException ioException = new IOException();
try {
throw ioException;
} catch (Throwable throwable) {
GenericException.<RuntimeException>throwAs(ioException);
}
}
@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T {
throw (T) e;
}
}
如果在上述代码main()方法的try-catch中不用GenericException.<RuntimeException>throwAs()方法
,而是直接抛出受检查异常ioException,那么必须在main()方法的头部声明受检查异常:
import java.io.IOException;
public class GenericException<T extends Exception> {
//如果不在main()方法头部声明受检查异常,则编译器报错:Unhandled exception: java.io.IOException
public static void main(String[] args) throws IOException {
IOException ioException = new IOException();
try {
throw ioException;
} catch (Throwable throwable1) {
throw ioException;
}
}
@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T {
throw (T) e;
}
}
通过上述的两个代码案例,我们可以发现:一般情况下,如果在某个方法中进行了IO操作,那么就有可能产生受检查异常IOException,因此必须在这个方法头部声明受检查异常。而在上述代码中,我们GenericException.<RuntimeException>throwAs()
方法通过类型擦除的特性欺骗编译器,让它认为ioException是一个不受检查异常,因此我们就不用在main()方法头部声明受检查异常IOException了。
总的来说,通过使用泛型类、类型擦除的特性和@SuppressWarnings注解,就能消除对受检查异常的检查限制。个人觉得《Java核心技术》的作者介绍这个知识点是希望Java开发者知晓泛型的引入给Java异常机制所造成的影响,并在后续的开发工作中避免这类可能导致问题的编码。
1.7.10 注意类型擦除后的冲突
当泛型类型被擦除后,不允许创建引发冲突的条件。例如在上述案例中的泛型类型Pair<T>
中增加下述的一个equals(T)方法:
public class Pair<T> {
private T firstMember;
private T secondMember;
//编译器报错:'equals(T)' in 'Pair' clashes with 'equals(Object)' in 'java.lang.Object'; both methods have same erasure, yet neither overrides the other
public boolean equals(T value) {
return firstMember.equals(value) && secondMember.equals(value);
}
//...
}
我们先不关注这个euqals(T)的方法体中判断相等的代码是否符合逻辑,这个equals(T)方法在类型擦除后就变成了equals(Object),这就跟Object的equals(Object)方法的签名相同了。我们可能认为这就是类型擦除后重写了Object的equals(Object)方法,但是编译器针对这行代码是会报错的:在类型擦除后,Pair对象的equals(T)方法和StringPair对象的equals(Object)拥有相同的方法签名,但是每个方法都不能重写而覆盖掉对方。
如果我们在泛型类型Pair<T>
中不是增加上述的equals(T)方法,而是增加一个equals(Object)方法,这个便是合法的:
public class Pair<T> {
private T firstMember;
private T secondMember;
//编译器报错:'equals(T)' in 'Pair' clashes with 'equals(Object)' in 'java.lang.Object'; both methods have same erasure, yet neither overrides the other
public boolean equals(Object object) {
return firstMember.equals(object) && secondMember.equals(object);
}
//...
}
因此我们根据这个现象,归纳了这样一个规则:在类型擦除后,在一条继承链上的所有类中,我们定义两个集合:形式参数中有类型变量的方法被类型擦除后的方法签名集合和形式参数中没有类型变量的方法的方法签名集合,这两集合没有交集,那么就不会创建引发冲突的条件,也不会导致出现both methods have same erasure, yet neither overrides the other
的报错文案,我个人认为这是Java编译器为了规避隐式引入桥方法从而导致冲突而设置的规则。
其中,《Java核心技术》第8.6.10的章节中也提到:泛型规范嗨引用了另外一个原则:“为了支持擦除转换,我们要施加一个限制:倘若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类。”例如下面的代码是非法的:
class Employee implements Comparable<Employee> {
@Override
public int compareTo(Employee o) {
return 0;
}
}
//编译器报错:'java.lang.Comparable' cannot be inherited with different type arguments: 'Employee' and 'Manager'
class Manager extends Employee implements Comparable<Manager> {
@Override
public int compareTo(Manager o) {
return 0;
}
}
这个限制性的原则跟类型擦除的关系并不太明显。毕竟,以下非泛型版本的代码是合法的:
class Employee implements Comparable {
@Override
public int compareTo(Object o) {
return 0;
}
}
class Manager extends Employee implements Comparable {
@Override
public int compareTo(Object o) {
return 0;
}
}
class SeniorManager extends Manager {}
那么为什么直接实现Comparable<Manager>和间接实现Comparable<Employee>的Manager类会被编译器认定为不合法的呢?这是因为实现了Comparable<T>的类会获得一个隐式桥方法:
public int compareTo(Object other) {
return compareTo((T) other);
}
而Manager类直接和间接地实现了两个Comparable<T>接口类型,因此Manager类拥有了两个相同的隐式桥方法,而Java语法规定:对于实现了多个不同的实际类型参数的相同泛型接口的Manager类来说,不能有多个这样相同的隐式桥方法。
1.8 泛型类型的继承规则
在使用泛型类时,需要了解有关继承和子类型的一些规则。例如有一个Employee类和继承Employee类的Manager类,那么Pair<Manager>不是Pair<Employee>的子类。例如下面的代码不能成功编译:
//java: incompatible types: Pair<Manager> cannot be converted to Pair<Employee>
Pair<Employee> employeePair = new Pair<Manager>();
//java: incompatible types: java.util.ArrayList<java.lang.String> cannot be converted to java.util.ArrayList<java.lang.Object>
ArrayList<Object> objectList = new ArrayList<String>();
这看起来是个很严格的限制,不过对于类型安全来说是非常必要的。假设允许将Pair<Manager>转换为Pair<Employee>,考虑以下的代码:
Pair<Manager> managerPair = new Pair<Manager>(new Manager(), new Manager());
//下面这行代码实际运行时会报错incompatible types,但我们先假设其能正常运行
Pair<Employee> employeePair = managerPair;
employeePair.setFirstMember(new Employee());
如果上述代码的第二行代码可正常执行,那么第三行代码也能正常执行,而我们第三行代码中将Employee和Manager组成一对,这对于Pair<Manager>类型的managerPair来说是不合法的。
前面看到的是泛型类型与Java数组之间的一个重要区别。可以将一个Manager[]数组赋给一个类型为Employee[]的变量:
Manager[] managerArray = {new Manager(), new Manager()}; Employee[] employeeArray = managerArray;
不过,数组有特别的保护。如果试图将一个低级别的员工存储到employeeArray[0],Java虚拟机将会抛出ArrayStoreException异常。
另外我们知道,可以将参数化类型转换为一个原始类型。例如,Pair<Employee>是原始类型Pair的一个子类型。在与遗留代码交互时,这个转换非常必要。而转换成原始类型后可能会产生类型错误,例如下述代码的最后一行代码会抛出ClassCastException异常:
Pair rawPair = new Pair<Manager>(new Manager(), new Manager());
//此时编译器不会报错,只会警告:Unchecked call to 'setFirstMember(T)' as a member of raw type 'Pair'
rawPair.setFirstMember(new File(""));
//下面这行代码在允许时,Java虚拟机会报错:Exception in thread "main" java.lang.ClassCastException: java.io.File cannot be cast to Manager
Manager manager = (Manager) rawPair.getFirstMember();
最后,泛型类可以扩展或实现其他的泛型类。例如ArrayList<T>类实现了List<T>接口,这意味着,一个ArrayList<Manager>可以转换为一个List<Employee>。
1.9 通配符类型(Wildcard Types)
严格的泛型类型系统使用起来并不方便,Java设计者发明了一种巧妙的解决方案:通配符类型(Wildcard Types)。
1.9.1 通配符概念(通配符的子类型限定,下界通配符(Lower Bounds Wildcards))
在通配符类型中,允许类型参数发生变化。例如,子类型限定(subtype bound)的通配符类型Pair<? Extends Employee>表示任何泛型Pair类型,它的类型参数是Employee及其子类的类型,如Pair<Employee>或Pair<Manager>,但不是Pair<String>。
假设要编写一个打印"员工对"的方法,如下所示:
public static void printBuddies(Pair<Employee> p) {
Employee firstMember = p.getFirstMember();
Employee secondMember = p.getSecondMember();
System.out.println(firstMember.getName() + " and " + secondMember.getName() + " are buddies.");
}
正如前面章节讲到的,不能将Pair<Manager>传递给这个方法,因为Pair<Employee>和Pair<Manager>没有继承关系,这一点很有限制。不过解决的方法很简单,即可以使用一个通配符类型:
public static void printBuddies(Pair<? extends Employee> p) {
//...
}
这时可以认为Pair<Manager>是Pair<? Extends Employee>的子类型,因此可以有以下的写法:
printBuddies(new Pair<>(new Employee(), new Employee()));
printBuddies(new Pair<>(new Manager(), new Employee()));
printBuddies(new Pair<>(new Manager(), new Manager()));
但这会引入另一个问题:使用通配符会通过Pair<? Extends Employee>的引用破坏Pair<Manager>吗?
Pair<? extends Employee> wildcardEmployeePair = new Pair<>(new Manager(), new Employee());
//编译报错:java: incompatible types: Employee cannot be converted to capture#1 of ? extends Employee
wildcardEmployeePair.setFirstMember(new Employee());
Employee employee = wildcardEmployeePair.getFirstMember();
通过上述代码的运行结果可以发现:使用通配符不会通过Pair<? Extends Employee>的引用破坏Pair<Manager>。这是因为Java编译器只知道unknownEmployeePair.setFirstMember()
的实参应该是Employee的某个子类型,但不知道具体是什么类型,因此它拒绝传入的实参为任何特定的类型,而我们在使用new关键词时无法创建一个通配符类型的对象,只能是某个特定类型的对象,因此unknownEmployeePair.setFirstMember()
根本无法正常调用,因为任何合法的对象都不符合该方法对实参的要求。
但是调用wildcardEmployeePair.getFirstMember()
方法就不存在这个问题:将getFirstMember()的返回值赋值给一个Employee引用时完全合法的。
这就是引入有限定的通配符的关键之处。现在已经有办法区分安全的getter方法和不安全的setter方法了。
1.9.2 通配符的子类型限定的使用规则
通配符的子类型限定主要有三种用法:修饰变量的类型、方法的形参类型和返回值类型,但是它们都有一定的限制。
子类型限定的通配符在修饰变量的类型时,变量引用的参数化类型对象的实际类型参数只能是限定类型及其子类型。例如:
//编译器自动推断参数化类型对象"new Pair<>(new Manager(), new Employee())"的隐式实际类型参数为Employee,即"new Pair<Employee>(new Manager(), new Employee())",符合通配符的子类型限定的规则
Pair<? extends Employee> wildcardEmployeePair = new Pair<>(new Manager(), new Employee());
//推断的隐式实际类型参数为Manager,符合通配符的子类型限定的规则
Pair<? extends Employee> wildcardEmployeePair2 = new Pair<>(new Manager(), new Manager());
//推断的隐式实际类型参数为Manager,符合通配符的子类型限定的规则
Pair<? extends Employee> wildcardEmployeePair3 = new Pair<>(new SeniorManager(), new Manager());
//编译报错:Cannot infer arguments,即无法自动隐式推断,因为推断的隐式实际类型参数为Object,而Object不是限定类型Employee及其子类型
Pair<? extends Employee> wildcardEmployeePair4 = new Pair<>(new String(), new Employee());
//编译报错:java: incompatible types: Pair<java.lang.Object> cannot be converted to Pair<? extends Employee>
Pair<? extends Employee> wildcardEmployeePair5 = new Pair<Object>(new Object(), new Employee());
//编译报错:Wildcard type '? extends Employee' cannot be instantiated directly,即通配符类型不可被直接实例化
Pair<? extends Employee> wildcardEmployeePair6 = new Pair<? extends Employee>(new Object(), new Employee());
我们知道上述变量wildcardEmployeePair有对应的setter和getter方法:
public viod setFirstMember(? extends Employee firstMember) {
this.firstMember = firstMember;
}
public ? extends Employee getFirstMember() {
return firstMember;
}
子类型限定的通配符在修饰方法的形参时,该方法的形参不能引用任何类型的对象(但可以引用null),即该方法无法正常使用。例如:
Pair<? extends Employee> wildcardEmployeePair = new Pair<>(new Manager(), new Employee());
wildcardEmployeePair.setFirstMember(null);
//编译报错:java: incompatible types: java.lang.Object cannot be converted to capture#1 of ? extends Employee。因为"wildcardEmployeePair.getFirstMember()"的运行时类型为Object,所以它被当做Object类型,而不是"? extends Employee"类型
wildcardEmployeePair.setFirstMember(wildcardEmployeePair.getFirstMember());
//编译报错:java: incompatible types: Object cannot be converted to capture#1 of ? extends Employee
wildcardEmployeePair.setFirstMember(new Object());
//编译报错:java: incompatible types: Employee cannot be converted to capture#1 of ? extends Employee
wildcardEmployeePair.setFirstMember(new Employee());
//编译报错:java: incompatible types: Manager cannot be converted to capture#1 of ? extends Employee
wildcardEmployeePair.setFirstMember(new Manager());
//编译报错:java: incompatible types: SeniorManager cannot be converted to capture#1 of ? extends Employee
wildcardEmployeePair.setFirstMember(new SeniorManager());
子类型限定的通配符在修饰方法的返回值时,引用该方法返回值的变量的类型只能是限定类型及其父类型。例如:
Pair<? extends Employee> wildcardEmployeePair = new Pair<>(new Manager(), new Employee());
Object object = wildcardEmployeePair.getFirstMember();
Employee employee = wildcardEmployeePair.getFirstMember();
//编译报错:java: incompatible types: capture#1 of ? extends Employee cannot be converted to Manager
Manager manager = wildcardEmployeePair.getFirstMember();
//编译报错:java: incompatible types: capture#1 of ? extends Employee cannot be converted to SeniorManager
SeniorManager seniorManager = wildcardEmployeePair.getFirstMember();
1.9.3 通配符类型可修饰的变量和方法
通配符类型可用于修饰局部变量、成员变量、静态变量、成员方法、静态方法、方法的形参和返回值。下述代码没有任何有意义的业务逻辑,仅用于验证通配符类型可修饰的变量和方法:
import java.util.ArrayList;
import java.util.List;
class WildcardTypeTest {
private List<? extends String> mWildcardStringList;
private static List<? extends String> staticWildCardStringList;
WildcardTypeTest(List<? extends String> wildcardStringList) {
this.mWildcardStringList = wildcardStringList;
}
public List<? extends String> getWildcardStringList() {
return mWildcardStringList;
}
public void setWildcardStringList(List<? extends String> mWildcardStringList) {
this.mWildcardStringList = mWildcardStringList;
}
public static List<? extends String> returnWildCardClass(List<? extends String> wildcardStringList) {
if (staticWildCardStringList != null) {
return staticWildCardStringList;
} else if (wildcardStringList != null) {
return wildcardStringList;
} else {
List<? extends String> localWildcardStringList = new ArrayList<>();
return localWildcardStringList;
}
}
}
1.9.4 通配符的超类型限定(上界通配符Upper Bounds Wildcards)
通配符限定与类型变量限定十分类似,但是还有个附加能力,即可指定一个超类型限定(supertype bound),例如? super Manager
,这个通配符限制为Manager的所有超类型(已有的super关键字十分准确地描述了这种关系)。
为什么要这么做呢?带有超类型限定的通配符的行为与? extends Manager
相反。可以为方法提供参数,但不能使用返回值。例如,有一个List<? super String>
类型的变量mWildcardSuperStringList,那么它的getter和setter方法如下:
Pair<? super Manager> wildcardSuperEmployeePair = new Pair<>(new Manager(), new Employee());
wildcardSuperEmployeePair.setFirstMember(new Employee());
//编译报错:java: incompatible types: capture#1 of ? super Employee cannot be converted to Employee
Employee employee = wildcardSuperEmployeePair.getFirstMember();
首先Java编译器不知道上述代码中setFirstMember()方法的形参的具体类型,因此不能为该方法传递Employee或Object类型的对象作为实参,只能传递Manager及其子类型的对象。另外,由于不能保证上述代码中getFirstMember()方法的返回值的类型,只能用一个Object类型的变量引用该方法的返回值。
下面是超类型限定的另一种应用。Comparable接口本身是一个泛型类型,其代码如下:
public interface Comparable<T> {
public int compareTo(T other);
}
在上述代码中,形参other的类型被指定为类型变量T。那么如果String类实现了Comparable<String>接口,那么它实现的方法为:
public int compareTo(String other) {
//...
}
下述是第1.5章节中ArrayUtil类的min()方法的代码:
class ArrayUtil {
//为T设置限定后,编译器仍然会给出警告:Raw use of parameterized class 'Comparable'
public static <T extends Comparable> T min(T[] a) {
if (a == null || a.length == 0) {
return null;
}
T smallest = a[0];
for (int i = 1; i < a.length; i++) {
//编译器仍然会给出警告:Unchecked call to 'compareTo(T)' as a member of raw type 'java.lang.Comparable'
if(smallest.compareTo(a[i]) > 0) {
smallest = a[i];
}
}
return smallest;
}
}
上述代码中形参数组a的每个元素实现compareTo()方法如下:
public int compareTo(Object other) {
//...
}
由于Comparable是一个泛型类型,也许可以把上述第1.5章节中ArrayUtil类的min()方法优化一下:
public static <T extends Comparable<T>> T min(T[] a) {
//...
}
那么上述代码中形参数组a的每个元素实现compareTo()方法如下:
public int compareTo(T other) {
//...
}
上述代码比只使用T extends Comparable
更彻底,并且对许多类来讲这样工作得更好。例如,如果计算一个String数组的最小值,T就是类型String,而String是Comparable<String>的一个子类型。但是处理一个LocalDate对象的数组时,我们会遇到一个问题。LocalDate实现了ChronoLocalDate接口,而ChronoLocalDate接口继承了Comparable<ChronoLocalDate>,因此LocalDate实现的是Comparable<ChronoLocalDate>而不是Comparable<LocalDate>。在这种情况下,可以利用超类型限定的通配符来解决:
public static <T extends Comparable<? super T>> T min(T[] a) {
//...
}
那么上述代码中形参数组a的每个元素实现compareTo()方法如下:
public int compareTo(? super T other) {
//...
}
这时的compareTo()方法接受的形参other可以是T及其超类型的对象,无论如何向compareTo()方法传递一个T类型的对象时最安全的。
对于Java初学者来说。类似<T extends Comparable<? super>>
的类型定义看起来很吓人。但这个类型定义的本意就是帮助应用程序员去除对调用参数的不必要的限制。对泛型没有兴趣的应用程序员可能很快就会略过这些声明,想当然地认为库程序员做的都是正确的。如果你是一名库程序员,一定要熟悉通配符,否则就会受到用户的责备,他们还要在代码中随机地添加强制类型转换直至代码可以编译。
超类型限定的通配符的另一个常见的用法是作为一个函数式接口的参数类型。例如,Collection接口有一个方法:
default boolean removeIf(Predicate<? super E> filter)
这个方法会删除所有满足给定谓词条件的元素。例如,如果你不喜欢散列码为奇数的员工,就能用下述代码将他们删除:
ArrayList<Employee> employeeArrayList = new ArrayList<>(); employeeArrayList.add(new Employee()); employeeArrayList.add(new Manager()); Predicate<Object> oddHashCodePredicate = new Predicate<Object>() { @Override public boolean test(Object object) { return object.hashCode() %2 != 0; } }; employeeArrayList.removeIf(oddHashCodePredicate);
你希望能够传入一个Predicate<Object>,而不只是Predicate<Employee>。super通配符可以使这个愿望成真。
1.9.5 通配符的超类型限定的使用规则
通配符的超类型限定主要也有三种用法:修饰变量的类型、方法的形参类型和返回值类型,但是它们都有一定的限制。
超类型限定的通配符在修饰变量的类型时,变量引用的参数化类型对象的实际类型参数只能是限定类型及其父类型。例如:
//编译报错:java: incompatible types: Pair<SeniorManager> cannot be converted to Pair<? super Manager>
Pair<? super Manager> superWildcardEmployeePair0 = new Pair<SeniorManager>(new SeniorManager(), new SeniorManager());
//编译器自动推断参数化类型对象"new Pair<>(new SeniorManager(), new SeniorManager())"的隐式实际类型参数为Manager,即"new Pair<Manager>((Manager) new SeniorManager(), (Manager) new SeniorManager())",符合通配符的父类型限定的规则
Pair<? super Manager> superWildcardEmployeePair1 = new Pair<>(new SeniorManager(), new SeniorManager());
//推断的隐式实际类型参数为Manager,符合通配符的父类型限定的规则
Pair<? super Manager> superWildcardEmployeePair2 = new Pair<>(new Manager(), new SeniorManager());
//推断的隐式实际类型参数为Manager,符合通配符的父类型限定的规则
Pair<? super Manager> superWildcardEmployeePair3 = new Pair<>(new Manager(), new Manager());
//推断的隐式实际类型参数为Employee,符合通配符的父类型限定的规则
Pair<? super Manager> superWildcardEmployeePair4 = new Pair<>(new Employee(), new SeniorManager());
//推断的隐式实际类型参数为Employee,符合通配符的父类型限定的规则
Pair<? super Manager> superWildcardEmployeePair5 = new Pair<>(new Employee(), new Employee());
//推断的隐式实际类型参数为Object,符合通配符的父类型限定的规则
Pair<? super Manager> superWildcardEmployeePair6 = new Pair<>(new Object(), new SeniorManager());
//推断的隐式实际类型参数为Object,符合通配符的父类型限定的规则
Pair<? super Manager> superWildcardEmployeePair7 = new Pair<>(new String(), new SeniorManager());
超类型限定的通配符在修饰方法的形参时,该方法形参引用的对象的类型只能是限定类型及其父类型。例如:
Pair<? super Manager> superWildcardEmployeePair = new Pair<>(new SeniorManager(), new SeniorManager());
//编译报错:java: incompatible types: java.lang.Object cannot be converted to capture#1 of ? super Manager。因为"superWildcardEmployeePair.getFirstMember()"的运行时类型为Object,所以它被当做Object类型,而不是"? super Manager"类型
superWildcardEmployeePair.setFirstMember(superWildcardEmployeePair.getFirstMember());
superWildcardEmployeePair.setFirstMember(new SeniorManager());
superWildcardEmployeePair.setFirstMember(new Manager());
//编译报错:java: incompatible types: Employee cannot be converted to capture#1 of ? super Manager
superWildcardEmployeePair.setFirstMember(new Employee());
//编译报错:java: incompatible types: java.lang.Object cannot be converted to capture#1 of ? super Manager
superWildcardEmployeePair.setFirstMember(new Object());
超类型限定的通配符在修饰方法的返回值时,引用该方法返回值的变量的类型只能是Object类型。例如:
Pair<? super Manager> superWildcardEmployeePair = new Pair<>(new SeniorManager(), new SeniorManager());
//编译报错:java: incompatible types: capture#1 of ? super Manager cannot be converted to SeniorManager
SeniorManager seniorManager = superWildcardEmployeePair.getFirstMember();
//编译报错:java: incompatible types: capture#1 of ? super Manager cannot be converted to Manager
Manager manager = superWildcardEmployeePair.getFirstMember();
//编译报错:java: incompatible types: capture#1 of ? super Manager cannot be converted to Employee
Employee employee = superWildcardEmployeePair.getFirstMember();
//超类型限定的通配符在修饰方法的返回值的引用变量的类型只能是Object类型
Object object = superWildcardEmployeePair.getFirstMember();
1.9.6 对比下界通配符和上界通配符
我们再回顾一下extends
通配符。作为方法参数,<? extends T>
类型和<? super T>
类型的区别在于:
<? extends T>
允许调用读方法T get()
获取T
的引用,但不允许调用写方法set(T)
传入T
的引用(传入null
除外);<? super T>
允许调用写方法set(T)
传入T
的引用,但不允许调用读方法T get()
获取T
的引用(获取Object
除外)。
一个是允许读不允许写,另一个是允许写不允许读。先记住上面的结论,我们来看Java标准库的Collections
类定义的copy()
方法:
/** 将一个列表中的所有元素复制到另一个列表中。操作完成后,目标列表中每个被复制元素的索引将与其在源列表中的索引相同。目标列表必须至少与源列表一样长。如果它更长,则目标列表中的其余元素不受影响。
* 此方法以线性时间运行。
* @param <T> 列表中对象的类
* @param dest 目标列表。
* @param src 源列表。
* @throws IndexOutOfBoundsException 如果目标列表太小而不能包含整个源列表,则抛出IndexOutOfBoundsException。
* @throws UnsupportedOperationException,如果目标列表的列表迭代器不支持set操作。
*/
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
它的作用是把一个List
的每个元素依次添加到另一个List
中。它的第一个参数是List<? super T>
,表示目标List
,第二个参数List<? extends T>
,表示要复制的List
。我们可以简单地用for
循环实现复制。在for
循环中,我们可以看到,对于类型<? extends T>
的变量src
,我们可以安全地获取类型T
的引用,而对于类型<? super T>
的变量dest
,我们可以安全地传入T
的引用。
这个copy()
方法的定义就完美地展示了extends
和super
的意图:
copy()
方法内部不会读取dest
,因为不能调用dest.get()
来获取T
的引用;copy()
方法内部也不会修改src
,因为不能调用src.add(T)
。
这是由编译器检查来实现的。如果在方法代码中不小心或故意地修改src
,或者意外读取了dest
,就会导致一个编译错误:
public class Collections {
//把src列表的每个元素复制到dest列表中
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
//...,假设我们在Collections类的copy()方法中额外增加了下述代码
T t = dest.get(0); //编译报错:java: incompatible types: capture#1 of ? super T cannot be converted to T
src.add(t); //编译报错:java: no suitable method found for add(T)
}
}
使用了下界和上界通配符的copy()
方法的另一个好处是可以安全地把一个List<Integer>
添加到List<Number>
,但是无法反过来添加:
List<Number> numList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
//Explicit type arguments can be inferred ,即尖括号中的Integer可以自动推断,不需要显式编码
Collections.<Integer>copy(numList, intList);
Collections.<>copy(numList, intList);
Collections.<Integer>copy(intList, intList);
Collections.<Number>copy(numList, numList);
//编译报错:java: incompatible types: java.util.List<java.lang.Integer> cannot be converted to java.util.List<? super java.lang.Number>
Collections.<Number>copy(intList, numList);
//编译报错:java: incompatible types: java.util.List<java.lang.Number> cannot be converted to java.util.List<? extends java.lang.Integer>
Collections.<Integer>copy(intList, numList);
//编译报错:java: incompatible types: java.util.List<java.lang.Integer> cannot be converted to java.util.List<? super java.lang.Object>
Collections.<Object>copy(intList, numList);
//编译报错:no instance(s) of type variable(s) exist so that Number conforms to Integer,即编译器无法自动推断出合法的实际类型参数,无论是Number、Integer或Object,任何类型都不合法
Collections.copy(intList, numList);
而这些都是通过super
和extends
通配符,并由编译器强制检查来实现的。
我们从头再思考一下,如果让我们自己编码copy()方法,将一个列表中的所有元素复制到另一个列表中的,我们是不是大都写成这样:
-
非泛型方法,目标List和源List的类型都为原始类型List
public static void copy(List destinationList, List sourceList) {}
这种写法的缺点:
- 未使用泛型特性,变量destinationList和变量sourceList的类型都为原始类型List,它们的setter()方法可以随意存入任意类型的对象,没有泛型的编译时类型检查的特性;它们的getter()方法的返回值类型为Object,没有泛型的隐式类型转换的特性,需要程序员显式地强制转换类型,并有ClassCastException异常的风险;
- 理论上copy()方法只读sourceList且只写destinationList就能完成将一个列表中的所有元素复制到另一个列表中的任务,但是如果程序员在该copy()方法中意外读取destinationList或写入sourceList都是符合语法且能正常编译运行的,因此这会有destinationList的数据被暴露、sourceList被写入脏数据的风险。
-
泛型方法,目标List和源List的类型都为参数化类型List<T>
public static <T> void copy(List<T> destinationList, List<T> sourceList) {}
这种写法的优点:
- 使用了泛型特性,变量destinationList和变量sourceList的setter()方法存入的对象类型只能是实际类型参数及其子类型,它们的getter()方法的返回值类型一定是实际类型参数的类型。
这种写法的缺点:
-
destinationList和sourceList的泛型类型和实际类型参数都必须一致,destinationList的实际类型参数不能是sourceList的实际类型参数及其父类。例如下面的写法会报错:
class WildcardTypeTest { public static <T> void copy(List<T> destinationList, List<T> sourceList) {} public static void main(String[] args) { List<Number> numList = new ArrayList<>(); List<Integer> intList = new ArrayList<>(); //不会报错 Collections.<Integer>copy(numList, intList); //编译报错:Incompatible equality constraint: Integer and Number, inferred type does not conform to equality constraint(s),即不相容的等式约束:Integer和Number,推断类型不符合等式约束 WildcardTypeTest.copy(numList, intList); } }
-
存在destinationList的数据被暴露、sourceList被写入脏数据的风险。
-
泛型方法,目标List和源List的类型分别为通配符类型List<? super T>和List<? extends T>
public static <T> void copy(List<? super T> destinationList, List<? extends T> sourceList) {}
这种写法的优点:
-
使用了泛型特性,变量destinationList和变量sourceList的setter()方法存入的对象类型只能是实际类型参数及其子类型,它们的getter()方法的返回值类型一定是实际类型参数的类型;
-
destinationList和sourceList的泛型类型必须一致,但destinationList的实际类型参数可以是sourceList的实际类型参数及其父类。例如下面的写法:
List<Number> numList = new ArrayList<>(); List<Integer> intList = new ArrayList<>(); Collections.<>copy(numList, intList); Collections.<>copy(intList, intList); Collections.<>copy(numList, numList); //编译报错:java: incompatible types: java.util.List<java.lang.Integer> cannot be converted to java.util.List<? super java.lang.Number> Collections.<Number>copy(intList, numList);
-
在copy()方法的作用域内,sourceList可读不可写,而destinationList可写不可读,如果程序员硬编码写入或读取会导致编译错误,这样即可避免destinationList的数据被暴露、sourceList被写入脏数据的风险。
-
1.9.7 PECS原则(Producer Extends Consumer Super)
何时使用extends
,何时使用super
?为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super。
即:如果需要返回T
,它是生产者(Producer),要使用extends
通配符;如果需要写入T
,它是消费者(Consumer),要使用super
通配符。
还是以Collections
的copy()
方法为例:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i); // src是producer
dest.add(t); // dest是consumer
}
}
}
需要返回T
的src
是生产者,因此声明为List<? extends T>
,需要写入T
的dest
是消费者,因此声明为List<? super T>
。
1.9.8 无限定通配符(Unbounded Wildcards)
还可以使用无限定通配符,例如Pair<?>。初看起来,这好像与原始的Pair类型一样。实际上,这两种类型有很大的不同。类型Pair<?>有以下方法:
public ? getFirstMember() {}
public void setFirstMember(? firstMember) {}
在上述代码中,引用getFirstMember()方法返回值的变量的类型只能是Object类型,setFirstMember()方法不能被调用。
为什么要使用这样一个脆弱的类型?因为它对于部分简单的非空判断非常有用,但是无限定通配符也就只能做为非空判断方法形参的类型修饰符了。例如,下面这个泛型方法可用来测试一个Pair对象是否包含一个null引用,它不需要实际的类型:
public static <T> boolean hasNulls(Pair<T> pair) {
return pair.getFirstMember() == null || pair.getSecondMember() == null;
}
但是,《Java核心技术》认为使用无限定通配符类型作为形参的hasNulls()方法的代码可读性更好:
public static boolean hasNulls(Pair<?> pair) {
return pair.getFirstMember() == null || pair.getSecondMember() == null;
}
1.9.9 无限定通配符的使用规则
无限定通配符主要也有三种用法:修饰变量的类型、方法的形参类型和返回值类型,但是它们都有一定的限制。
无限定通配符在修饰变量的类型时,变量引用的参数化类型对象的实际类型参数没有限制。例如:
Pair<?> unboundedWildcards1 = new Pair<Employee>(new SeniorManager(), new Employee());
Pair<?> unboundedWildcards2 = new Pair<Object>(new SeniorManager(), new Employee());
Pair<?> unboundedWildcards3 = new Pair<Object>(new SeniorManager(), new String());
无限定通配符在修饰方法的形参时,该方法的形参不能引用任何类型的对象(但可以引用null),即该方法无法正常使用。例如:
Pair<?> unboundedWildcards = new Pair<Employee>(new SeniorManager(), new Employee());
unboundedWildcards.setFirstMember(null);
//编译报错:java: incompatible types: java.lang.Object cannot be converted to capture#1 of ?。因为"unboundedWildcards.getFirstMember()"的运行时类型为Object,所以它被当做Object类型,而不是"?"类型
unboundedWildcards.setFirstMember(unboundedWildcards.getFirstMember());
//编译报错:java: incompatible types: java.lang.Object cannot be converted to capture#1 of ?
unboundedWildcards.setFirstMember(new Object());
//编译报错:java: incompatible types: Employee cannot be converted to capture#1 of ?
unboundedWildcards.setFirstMember(new Employee());
//编译报错:java: incompatible types: Manager cannot be converted to capture#1 of ?
unboundedWildcards.setFirstMember(new Manager());
无限定通配符在修饰方法的返回值时,引用该方法返回值的变量的类型只能是Object类型。例如:
Pair<?> unboundedWildcards = new Pair<Employee>(new SeniorManager(), new Employee());
Object object = unboundedWildcards.getFirstMember();
//编译报错:java: incompatible types: capture#1 of ? cannot be converted to Employee
Employee employee = unboundedWildcards.getFirstMember();
//编译报错:java: incompatible types: capture#1 of ? cannot be converted to Manager
Manager manager = unboundedWildcards.getFirstMember();
//编译报错:java: incompatible types: capture#1 of ? cannot be converted to SeniorManager
SeniorManager seniorManager = unboundedWildcards.getFirstMember();
1.9.10 不同类型的引用变量可引用的对象的类型
-
List类型的变量能引用ArrayList<任意类型>的对象:
List rawList = new ArrayList(); rawList = new ArrayList<Object>(); rawList = new ArrayList<Number>();
-
List<?>类型的变量能引用ArrayList<任意类型>的对象:
List<?> unboundedWildcardList = new ArrayList(); unboundedWildcardList = new ArrayList<Object>(); unboundedWildcardList = new ArrayList<Number>();
-
List<? extends Number>类型的变量能引用ArrayList<Number及其子类型>的对象:
List<? extends Number> extendsWildcardList = new ArrayList(); //编译报错:java: incompatible types: java.util.ArrayList<java.lang.Object> cannot be converted to java.util.List<? extends java.lang.Number> extendsWildcardList = new ArrayList<Object>(); extendsWildcardList = new ArrayList<Number>(); extendsWildcardList = new ArrayList<Integer>(); //编译报错:java: incompatible types: java.util.ArrayList<com.sun.org.apache.xpath.internal.operations.String> cannot be converted to java.util.List<? extends java.lang.Number> extendsWildcardList = new ArrayList<String>();
-
List<? super Number>类型的变量能引用ArrayList<Number及其父类型>的对象:
List<? super Number> superWildcardList = new ArrayList(); superWildcardList = new ArrayList<Object>(); superWildcardList = new ArrayList<Number>(); //编译报错:java: incompatible types: java.util.ArrayList<java.lang.Integer> cannot be converted to java.util.List<? super java.lang.Number> superWildcardList = new ArrayList<Integer>(); //编译报错:java: incompatible types: java.util.ArrayList<com.sun.org.apache.xpath.internal.operations.String> cannot be converted to java.util.List<? super java.lang.Number> superWildcardList = new ArrayList<String>();
-
List<Number>类型的变量能引用ArrayList<Number,不能是Number父或子类型>的对象:
List<Number> numberList = new ArrayList(); //编译报错:java: incompatible types: java.util.ArrayList<java.lang.Object> cannot be converted to java.util.List<java.lang.Number> numberList = new ArrayList<Object>(); numberList = new ArrayList<Number>(); //编译报错:java: incompatible types: java.util.ArrayList<java.lang.Integer> cannot be converted to java.util.List<java.lang.Number> numberList = new ArrayList<Integer>(); //编译报错:java: incompatible types: java.util.ArrayList<com.sun.org.apache.xpath.internal.operations.String> cannot be converted to java.util.List<java.lang.Number> numberList = new ArrayList<String>();
1.9.11通配符捕获(Wildcard Capture)
下面编写一个方法来交换Pair对象里的两个元素:
public static void swapPairElement(Pair<?> p) {
? t = p.getFirstMember();
p.setFirstMember(p.getFirstMember());
p.setSecondMember(t);
}
这是一个问题,因为在交换的时候必须临时保存第一个元素。幸运的是,这个问题有一个有趣的解决方案。我们可以写一个辅助方法swapPairElementHelper(),如下所示:
public static <T> void swapPairElementHelper(Pair<T> p) {
T t = p.getFirst();
p.setFirstMember(p.getSecondMember())
p.setSecond(t);
}
注意,swapPairElementHelper()是一个泛型方法,它有一个固定的Pair<?>类型的参数而swapPairElement()不是一个泛型方法。现在可以由swapPairElement()方法调用swapPairElementHelper()方法:
public static void swapPairElement(Pair<?> p) {
swapPairElementHelper(p);
}
在这种情况下,swapPairElementHelper()方法的参数T捕获通配符。它不知道通配符指示哪种类型,但是这是一个明确的类型,并且从public static <T> void swapPairElementHelper(Pair<T> p)
的定义可以看到T指示那个类型。当然在这个案例中并不一定要用通配符,我们也能直接把public static <T> void swapPairElementHelper(Pair<T> p)
实现为一个没有通配符的泛型方法。不过考虑下面这个例子,这里通配符类型很自然地出现在计算中间:
public static void maxminBonus(Manager[] a, Pair<? super Manager> result) {
minmaxBonus(a, result);
swapPairElementHelper(result);
}
在这里,通配符捕获机制是不可避免的。
通配符捕获只有在非常限定的情况下才是合法的。Java编译器必须能够保证通配符表示单个确定的类型。例如,ArrayList<Pair<T>>中的T永远不能捕获ArrrayList<Pair<?>>中的通配符。数组列表可以保存两个Pair<?>,其中的?分别有不同的类型。
1.10 反射和泛型
反射允许你在运行时分析任意对象。如果对象是泛型类的实例,关于泛型类型参数你将得不到太多信息,因为它们已经被擦除了。在下面的小节中,我们将学习利用反射可以获得泛型类的哪些信息。
1.10.1 泛型Class类
现在,Class类是泛型的。例如,String.class实际上是一个Class<String>类的对象(事实上是唯一的对象)。
类型参数十分有用,这是因为它允许Class方法的返回类型更加具有特定性。Class的以下方法就使用了类型参数:
private void viewDetail(){ Map map1 = new HashMap(); Map<String,Object> map2 = new HashMap<String,Object>(); Map<Object,Object> map3 = new HashMap<Object,Object>(); Map<String,String> map4 = new HashMap<String,String>(); test1(map1); test1(map2); test1(map3); //编译错误 test1(map4); //编译错误}private void test1(Map<String,Object> map){}
该章节内容较为深奥和复杂,待后续完善。
本文参考文献:
通用泛型知识点:
java泛型的使用(二)在实例化对象时不指定泛型,则自动识别为object
Java 泛型中的通配符 T,E,K,V,?,你确定都了解吗?
Java中的泛型会被类型擦除,那为什么在运行期仍然可以使用反射获取到具体的泛型类型?
Java泛型详解:和Class的使用。泛型类,泛型方法的详细使用实例
暂未分类的Java泛型文章:
泛型的缺陷,泛型的桥方法:
Java泛型引起的堆污染
一场由Java堆污染(Heap Pollution)引发的思考
java7新特性 当使用可变并且非具体类型形式化参数的方法时候,改进警告与报错的提示
Java字节码,Java虚拟机:
java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题
Java泛型数组:
Java泛型的协变、逆变和不变: