Java的泛型和其它语言相比略显不足, 因为为了向前兼容导致了编译后使用了"擦除"机制, 所以一些比较细节的用法需要留意.
目录:
1. 类泛型, 接口泛型和方法泛型
2. 擦除
3. 边界
4. 通配符
5. Java泛型的限制及解决方案
6. 自限定
7. 动态类型安全
8. 异常
9. 混型
10. 潜在类型机制
参考: Thinking in Java, Core Java
进阶读物: 《Generics in the Java Programming Language》
1. 类泛型, 接口泛型, 方法泛型
Java中泛型三种典型用法, 泛型方法可以自适应, 不用显示指定泛型参数.
详细介绍略.
2. 擦除
由于泛型参数只用于编译期类型检查, 而在运行期会擦出到泛型参数的边界. 所以有以下影响:
(1) 副作用: 只能显示调用泛型参数边界的方法, 而不能显示调用泛型参数的其它方法.
(2) 编译器额外工作: 如果返回类型为泛型参数, 需要插入强制类型转换; 如果方法参数含有泛型参数, Override该方法时需要使用桥接保证多态(实现接口时普遍使用).
注: 运行时完全得不到泛型参数类型的具体信息, 包括使用Class.getTypeParameters(), 这个方法仅仅反映泛型参数的行参.
3. 边界
<T extends MyClass> 擦除到MyClass, 可调用MyClass中的方法.
4. 通配符
和边界是有区别的, 通俗地讲, 边界是型参而通配符是实参.
<? extends MyClass> 以此作为泛型参数的容器类可读不可写(可写null);
<? super MyClass> 以此作为泛型参数的容器类可写不可读.
此外还有无界通配符<?>, 类似原生类型或<? extends Object>, 但在编译器警告信息方面会有细致区别.
5. Java泛型的限制及解决方案
(1) 基本类型不能作为类型参数.
解决方法: 包装器类.
(2) 不能使用instanceof.
解决方法: 传入Class<T>, 使用Class<T>.isInstance().
(3) 不能使用new T()
解决办法: 传入一个工厂, Class<T>是一个内建工厂, 但是需要有无参构造器.
(4) 不能生成泛型参数数组
解决办法: Array.newInstance(Class<T> type, int size), 这样创建出来的数组确实是T类型的, 但是在Java类库中却大量使用了Object数组;
上面提到的解决办法可以突破擦除的限制, 但是在实际Coding中可能使用这种编程技巧的地方并不多.
另一方面, 有些问题时使用擦除后无法补偿的.
(1) 实现一个泛型参数接口的两种变体
(2) 带泛型参数的函数重载
6. 自限定类型
SelfBounded<T extends SelfBounded<T>> 使用自限定将获得某个方法的一个版本, 不使用的话基类的版本和派生类重载版本都将保留.
7. 动态类型检查
使用Collections中check开头的静态方法, 如Collections.checkedList(new ArrayList<MyClass>, MyClass.class), 第二个参数指定允许插入的类型.
8. 异常
不能够catch一个泛型类型异常, 但是可以throw一个泛型类型异常, 如throw <E extends Exception>.
9. 混型
由于Java的类不能直接继承自一个泛型类型参数, 所以不能像C++那样由泛型实现混型. 虽然Java里面可以使用接口混合, 装饰器模式和动态代理来实现混型, 但是明显比不上C++直接用泛型实现混型.
10. 潜在类型机制
Python和C++能够在编译和运行时获取实际类型信息以了解一个类型是否能够调用特性方法, 但是Java泛型在运行时使用了擦除.
(1) Java可以通过反射来获取一个类型中的域, 方法, 构造器. 进而实现潜在类型机制. 但是反射的缺陷是将所有的类型检查都移到了运行时.
(2) 另一种方法就是使用适配器模式. 潜在类型机制实际上是一种隐式接口, Java里面可以通过定义一个显示接口+适配器模式来实现相同的功能.