写在最前:本笔记全程参考《Java核心技术卷I》,添加了一些个人的思考和整理
目录
泛型的特性、限制和局限性
1. 不能用基本数据类型实例化参数
例如,没有ArrayList<int>
,或者Pair<double>
,而只有ArrayList<Integer>
以及Pair<Double>
因为在类型擦除之后,所有的泛型都将转换为限定类型或者Object,但是基本数据类型不能转换为Object
2. 运行时类型查询只适用于原始类型
例如,对于代码if (a intanceof Pair<String>)
,它只会检查变量a
是否为任意类型的Pair
,而不会检查是否为Pair<String>
,存在风险,编译器会报错
当强制类型转换时同样会存在该风险:Pair<String> p = (Pair<String>) a;
,会出现警告
同样的道理,对于带泛型的数据类型,getClass
方法只会返回原始类型:
Pair<String> stringPair = new Pair<>();
Pair<Double> doublePair = new Pair<>();
stringPair.getClass() == doublePair.getClass() // 结果为true,都是Pair.class
3. 不能创建参数化类型的数组
例如:Pair<String>[] table = new Pair<>[10];
会报错
原因:
一般来说,数组会记住它的元素类型,如果试图储存其他类型的元素就会出现ArrayStoreException
异常。
int[] a = new int[10];
a[0] = "String"; // 报错,出现ArrayStoreException异常
但是,对于泛型类型,擦除会使这种机制无效。类型擦除后,table
的类型是Pair[]
,它会转换为Object[]
。Object[]
可以储存任意类型的元素,但是最后会出现类型错误。参考下面这个例子,Pair<String>
数组里可以进去一个类型为Pair<Double>
的叛徒。
Pair<String>[] table = new Pair<>[10];
// table在运行时变成了Object[]
table[0] = new Pair<Double>; // 正常来讲会报错,但是因为类型擦除,所以运行时仍然可以赋值成功
// 在这之后,table数组中就出现了一个叛徒,有极大的可能在将来引起异常,是一个潜在的bug
因此,不能创建参数化类型的数组。
需要说明的是,只是不能创建参数化类型的数组,但是还是可以声明类型为Pair<String>[]
,只是不能用new Pair<String>[10]
初始化这个变量。
另外,有个不安全的技巧:可以声明通配类型的泛型数组,然后进行强转:
var table = (Pair<String>[]) new Pair<?>[10];
但是这样子仍然是不安全的,上面的问题依然存在,你仍然可以在Pair<String>
数组里存入Pair<Double>
,并最终引起冲突、产生异常。
4. Varargs警告
对于可变长度参数:
public static <T> void addAll(Collection<T> coll, T... ts) { ... }
可变长度参数本质上也是一个数组,包含提供的所有实参。
如果这么调用呢?
Collection<Pair<String> table = new Collection<>();
Pair<String> pair1 = new Pair<>();
Pair<String> pair2 = new Pair<>();
addAll(table, pair1, pair2); // Pair<String>类型的可变长度参数
这样子不就创建了一个Pair<String>[]
数组了嘛?
好在java对这种情况,规则有所放松,你只会得到一个警告,而不是错误。
忽略警告
-
前面提到的
@SuppressWarnings("unchecked")
注解可以忽略该警告 -
java7中,在方法上添加
@SafeVarargs
注解,也可以忽略警告注意,该注解只能够用于声明为
static
或者final
、或者在java9后的private
方法和构造方法,其他方法都可能被覆盖而使注解失效。
5. 不能实例化类型变量
不能使用类似new T()
或者new T(...)
的表达式。类型擦除后,T
会变成Object
,就没什么意义了
public Pair() {
// 下面会出错
first = new T();
second = new T();
}
解决方法1:借助方法引用
java8之后,最好的解决办法就是让调用者提供一个构造器表达式(借助方法引用),例如:
Pair<String> p = Pair.makePair(String::new);
makePair
方法接收一个自己设计的泛型接口Supplier<T>
,这是一个函数式接口,表示一个无参数且返回类型为T
的函数:
public static <T> Pair<T> makePair(Supplier<T> constr) {
return new Pair<>(constr.get(), constr.get());
}
解决方法2:借助反射
比较传统的解决办法是通过反射调用构造器.newInstance()
方法来构造泛型对象,但是不能直接使用方法:T.class.getConstrutor().newInstance();
,因为T.class
是不合法的,它会被擦除成为Object.class
正确做法是:直接提供一个Class
对象,借助反射来完成new
的操作:
public static <T> T getObj(Class cl) {
return cl.getConstructor().newInstance();
}
值得一提的是,Class
类型也是一个泛型,String.class
实际上是一个Class<String>
的实例(而且是唯一的实例)
6. 不能构造泛型数组
类似于不能实例化泛型字段一样,由于泛型擦除,所以也不能实例化数组。
需要注意的是,任然可以声明泛型数组,代码T[] t;
是合法的,但是不允许写成:new T[n];
,这样就试图实例化泛型数组了,编译器会报错。
例如,对于下面的代码,由于类型擦除,总是会构造Comparable[2]
的数组:
public static <T extends Comparable> T[] minmax(T... a) {
// 返回值为T[]是可以的
T[] mm = new T[2]; // 错误,类型擦除会使其总是构造Comparable[2]的数组
}
ArrayList的实现 类型擦除+强转
如果数组仅仅作为一个类的私有字段,那么可以将这个数组的元素类型声明为擦除的类型,并使用强制类型转换。如ArrayList
可以有如下实现方式:
public class ArrayList<E> {
private Object[] elements;
@SuppressWarnings
public E get(int n) {
return (E) elements[n]; // 在内部强制类型转换,直接返回所需的数据类型
}
public void set(int n, E e) {
elements[n] = e; // 不必强制类型转换
}
}
源码截图:
实际上,还有另一种思路:
public class MyArrayList<E> {
private E[] elements;
public MyArrayList() {
elements = (E[]) new Object[10]; // 强制类型转换,实际上是一个假象
}
public E get(int n) {
return elements[n];
}
public void set(int n, E e) {
elements[n] = e;
}
public static void main(String[] args) {
MyArrayList<Double> myArrayList = new MyArrayList<>();
myArrayList.set(1,122.1);
Double aDouble = myArrayList.get(1);
System.out.println(aDouble); // 122.1
}
}
这里的强制类型转换是一个假象,而类型擦除会使其无法察觉。因为E[]
最后会擦除为Object[]
解释一下:
(E[]) new Object[10];,乍一看会出现ClassCastException异常(因为E[]的类型要么比Object[]小,要么相同,而java中不能把父类强转为子类),但是因为类型擦除,E[]最后会变成Object[],所以就等价于Object[] elements = (Object[]) new Object[10]不会出现强转异常。
注:《Java核心技术卷I 第11版》p433中,书上的意思好像是,上方第二种代码才是
ArrayList
源码的实现方式,但是查看Java源代码,发现是第一种思路,源码截图在上面。附上原书截图:
实际运用存在的问题
我们企图运用上面这个思路,让minmax
方法最终能够返回泛型数组(但是并不实例化泛型数组,而是实例化一个父类数组,再强转为指定类型)。尝试一下代码,发现有错误:
public static <T extends Comparable> T[] minmax(T.. a) {
Comparable result = new Comparable[2];
// ...
return (T[]) result; // 出现警告:未检查的强制类型转换(Unchecked cast)
}
当使用下面的方式调用方法时,编译没有问题,但是运行时会出现ClassCastException异常
String[] names = ArrayAlg.minmax("Tom", "Dick", "Harry");
方法返回时Comparable[]
强转为String[]
时出现异常。这里其实是子类型引用指向父类型对象,违反了多态。(再回顾一下上面的ArrayList
,人家是巧妙地借助了类型擦除,而使得强转成功的)
解决方案
1. 通过方法引用
提供一个数组构造器表达式:
String names = ArrayAlg.minmax(String[]::new, "Tom", "Dick", "Harry");
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a) {
// 通过构造器的apply方法实例化指定类型的数组,赋值给泛型
// 此处传入的是String[]::new,下面代码相当于T[] result = new String[2];
T[] result = constr.apply(2);
// ...
return (T[]) result;
}
回顾一下方法引用,
String[]::new
相当于lambda表达式中的n -> new String[n]
2. 通过反射
public static <T extends Comparable> T[] minmax(T... a) {
var result = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);
// ...
return result;
}
7. ArrayListt的toArray方法
ArrayList的toArray
方法的实现方式:有两种
-
直接返回
Object[]
。源码:public Object[] toArray() { return Arrays.copyOf(elementData, size); }
-
返回指定的泛型类型数组
T[]
,但是没有类型(因为默认实现是一个Object[]
,需要传入一个泛型数组。如果传入的数组足够大,那么会使用传入的数组返回。源码:@SuppressWarnings("unchecked") public <T> T[] toArray(T[] a) { if (a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }
使用时,如果不清楚具体的数组长度,可以传入一个长度为0的数组,不占用额外空间。
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Object[] objects = list.toArray();
String[] strings = list.toArray(new String[0]);
8. 泛型类的静态上下文不能使用泛型变量
翻译成人话:不能在静态字段或方法中使用泛型变量。如下:
public class Singleton<T> {
private static T singleIntance; // 报错
public static T getSingleIntance() {
if (singleIntance == null) {
// 初始化singleIntance
}
return singleIntance;
}
}
原因:
如果觉得上面这样可行,那程序员就可以声明一个Singleton<Random>
共享一个随机数生成器对象,同时再另外声明一个Singleton<BufferReader>
共享一个读取器对象。
但是类型擦除之后,只剩下Singleton
类而没有泛型变量,它的字段也只有一个数据类型,不能即是BufferReader
又是Random
,但又是静态的(多个实例会共享同一个静态资源),这就冲突了。
所以,我们无法做到借助泛型同时实现多个数据类型的静态单例,应该禁止使用带有类型变量的静态字段和方法。
9. 不能抛出或捕获泛型类的实例
实际上,泛型类扩展Throwable
甚至是不合法的:
public class Probleam<T> extends Exception {} // 错误
错误提示为:Generic class may not extend ‘java.lang.Throwable’。
may not意为不允许。同样的用法在英语四级中也出现过。英语四级中题干有一处为:you may not use any of the words in the blank more than once. “请用任意词填空,每词只能用一次”
但是允许使用Throwable
或者其他异常类来限定泛型:
public static <T extends Throwable> void doWork(Class<T> t) {
try {
System.out.println(t);
} catch (Exception e) {
e.printStackTrace();
}
}
catch
子句中不能使用类型变量,例如下面的代码将不能通过编译:
public static <T extends Throwable> void doWork(Class<T> t) {
try {
System.out.println(t);
} catch (T e) { // 错误。Cannot catch type parameters
e.printStackTrace();
}
}
可以取消对检查型异常的检查
java异常处理机制的一个基本原则是,必须为所有检查型异常提供一个处理器。不过利用泛型可以取消(避免)这个机制:
@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T {
throw (T) t;
}
假设这个方法包含在Task
接口中,如果有一个检查型异常e
,可以调用按一下方式将异常转换为非检查型异常并抛出:
try {
...
} catch (Throwable t) {
Task.<RuntimeException>throwAs(t); // 转换成非检查型异常
}
这有什么意义呢?举例来说,在正常情况下,必须捕获一个Runnable
的run
方法中的所有检查型异常,如果想要向上抛出,就必须先把这些异常“包装”到非检查型异常中。我们这里明没有这种“包装”,只是抛出异常,“哄骗”编译器,让它相信这不是一个检查型异常。
一个简单的测试:
public class Problem implements Runnable{
@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T {
// java.io.FileNotFoundException: D:\read.txt (系统找不到指定的文件。)
System.out.println(t);
throw (T) t;
}
@Override
public void run() {
try {
// 提供一个不存在的文件路径
BufferedReader reader = new BufferedReader(new FileReader("D://read.txt"));
} catch (Throwable t) {
// 转换成非检查型异常
Problem.<RuntimeException>throwAs(t);
}
}
public static void main(String[] args) {
// 不用捕获异常
Thread thread = new Thread(new Problem());
thread.start();
}
}
10. 注意擦除后的冲突
当泛型类型被擦除后,不能够存在引发冲突的代码。
比如,泛型类Pair<T>
添加一个equals
方法:public boolean equals(T obj) { ... }
,
看起来,它与Object的equals
方法:public boolean equals(Object obj) { ... }
是两个不同的方法。
但是实际上,前者在泛型擦除之后,T
会被擦除为Object
,两个方法就变得一模一样,起冲突了。如果有代码调用了Pair<T>
的equals
方法,程序不知道应该执行哪一个方法。
程序会给出错误:‘equals(T)’ in ‘Pair’ clashes with ‘equals(Object)’ in ‘java.lang.Object’; both methods have same erasure, yet neither overrides the other.
一个补救的办法自然就是重新命名引发冲突的方法。
桥方法咋不管用了?
复习一下桥方法
为什么这里没有桥方法呢?我分析了一下发现:
- 桥方法中,子类的参数类型是确定的,而父类是类型变量。运行时父类的泛型被擦除,参数类型变为Object,因此如果没有桥方法,则程序总是调用父类的方法,会破坏多态性。因此编译器会在父类中添加一个桥方法,其参数为Object,在这个桥方法中再调用子类的方法。
- 而此处,反了过来:子类的参数类型一个类型变量,是不确定的,反倒是父类的参数是确定的,而且是一个Object!好家伙,我桥方法本来就要添加一个参数为Object的方法,你这已经有了,而且子类也会变成Object!运行时类型擦除后,会出现两个方法签名完全相同的方法,要是再添加一个桥方法,那就是三个了,桥方法还帮倒忙!
所以这一次桥方法帮不了你了,只能禁止这种情况的发生。
不能有同个接口的不同参数化
泛型规范说明还提出了另一个规则:为了支持擦除转换,倘若两个接口类型是同一接口的不同参数化,就不能让一个类或类型变量继承或实现于这两个接口类型。
例如,下面的代码是不合法的:
class Employee implements Comparable<Employee> { ... }
class Manager extends Employee implements Comparable<Manager> { ... }
Manager
会实现Comparable<Employee>
和Comparable<Manager>
,这是同个接口的不同参数化。
不允许的原因在于,如果同个接口有不同参数化,会导致合成的桥方法起冲突。实现了Comparale<X>
的类会获得一个桥方法:
public int compareTo(Object other) { return compareTo((X) other); }
不能对不同的类型X
合成两个这种桥方法,会起冲突