Java泛型
1、泛型定义
使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后在进行强制类型转换的代码具有更好的安全性和可读性。 –《Java核心技术》
泛型是在编译时期作用的;
泛型变量使用大写形式,在Java库中,一般使用变量E表示集合的元素类型,K和V表示表的关键字与值的类型。
2、通配符
2.1 无边界通配符
无边界通配符又成为非限定通配符
1 | public static void main(String[] args) { |
2.2 上边界通配符
上边界通配符和下边界通配符都属于限定通配符
1 | public static void main(String[] args) { |
? extends Number
如果限定的类型有多个,之间使用 & 进行分割
2.3 下边界通配符
1 | public static void main(String[] args) { |
3、泛型的使用
泛型必须先声明,再使用,不然会有编译错误;
泛型的声明是用过一对<>来完成,约定使用一个大写的字母来表示;
通配符不能用作返回值;
1 | public <T> T testA(T t, Test1<T> test1) { |
- 要从泛型类取数据时,用extends;
- 要往泛型类写数据时,用super;
- 既要取又要写,就不用通配符(即extends与super都不用)。
3.1 泛型类
1 | public class Demo<K, V> { |
3.2 泛型方法
1 | public class DemoTest4<K, V> { |
3.3 泛型接口
首先看一下不使用泛型接口的Demo
1 | 先定义接口,声明两个方法 |
上面是没有使用泛型的接口设计,但是aa方法的操作类型相当于在接口中写死了,如果此时我们需要一个String类型的aa方法,那是不是还要在声明一个String类型的接口,然后再去实现呢,这样是不是显得代码很臃肿,代码重复;
所以我们可以看一下使用泛型之后是怎么样的。
1 | 定义泛型接口 |
4、泛型擦除
在虚拟机上没有泛型类型对象,所有的对象都属于普通类。Java在处理泛型类型的时候,会处理成一个相应的原始类型。 擦除类型变量,并替换为限定类型,如果没有限定类型,默认使用Object替代。如果有限定类型,并且是多个,会使用第一个限定的类型来替换。
1 | public interface IGenericInte<T> { |
像上面这个T是一个无限定的变量,泛型擦除之后会直接使用Object替换。
当然调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换
1 | Pair<Employee> buddies = .... |
擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换,也就是说,编译器调用方法是其实是执行了一下两个虚拟机指令:
- 对原始方法Pair.getFirst()方法的调用
- 将返回的Object类型强制转换为Employee类型
1 | public static <T extends Comparable> T foo(T [] args) |
在擦除类型之后变成:
1 | public static Comparable T foo(Comparable [] args) |
参数类型T已经被擦除,只留下限定类型Comparable;
总之有关Java泛型转换的事实:
- 虚拟机没有泛型,只有普通的类和方法
- 所有的类型参数都用它们的限定类型替换
- ==桥方法被合成来保证多态==
- 为了保持类型安全型,必要时插入强制类型转换
第一条应该很好理解,这也是为什么会有泛型擦除这个概念,是因为JVM不能操作泛型;
第二条就是解释泛型如何进行类型的擦除;
第三条是泛型方法可能与多态的理念矛盾,所以使用桥方法来过渡或兼容;
第四条上面也有提到,会出现强制类型转换的情况;
5、泛型的约束与局限性
当然泛型的设计在java中并没有那么完美,它确实可以解决代码结构重用等问题,但是也是有一些局限性,下面是我根据《Java核心技术》进行的总结:
5.1 不能使用基础数据类型实例化类型参数
原因是类型擦除之后,如果使用Object原始类型,Object是无法存储基本数据类型的值。所以只能通过其包装类型声明;
5.2 运行时查询类型只适用与原始类型
1 | public class DemoTest5<T> { |
demoTest4.getClass().equals(demoTest5.getClass())其实比较的是DemoTest5这个类类型,我们输出一下demoTest4.getClass()的结果看一下:
1 | class com.ibli.javaBase.generics.DemoTest5 |
所以这里有一道非常经典的面试题,如何判断一个泛型他的具体类型是什么,这里我们可以使用反射去拿到泛型的具体类型;
5.3 不能创造参数化类型的数组
对于参数化类型的数组,在类型擦除之后,会变成Object[]类型,如果此时试图存储一个String类型的元素,就会抛出一个Array-StoreException异常;
主要目的还是处于到数组安全的保护,可以参考几篇文章:
1、如果Java不支持参数化类型数组,那么Arrays.asList()如何处理它们?
2、java不能创建参数化类型的泛型数组
3、java.lang.ArrayStoreException
5.4 Varargs警告
向参数个数可变的方法传递一个泛型类型的实例的场景,编译器会发出警告!
抑制这种警告的方式有两种:
- 在调用方法上增加注解@SuppressWarnings(“unchecked”)
- 还可以使用@SafeVarargs注解直接标注方法
5.5 不能实例化类型变量
不能使用new T(..) 或则new T[…]和T.class这样的表达式的类型变量;因为类型擦除后,T变成Object,显然我们在这里并不是想要创建一个Object实例。解决办法是在调用者提供一个构造器表达式,下面是用Supplier函数实现:
1 | public class Pair<T> { |
Supplier是一个函数接口,返回一个无参数并且返回类型为T的函数:
1 |
|
1 | public class TestMakePair { |
5.6 不能构造泛型数组
就像不能实例化一个泛型实例一样,也不能实例化数组。数组本身也有类型,用来监控存储在JVM中的数组,这个类型会被擦除,例如:
1 | public static <T extends Comparable> T[] foo(T[] a){ |
类型擦除,会让这个方法永远构造Comparabel[2]数组;
5.7 泛型类的静态上下文中类型变量无效
这个应该是比较好理解的,上文也提到过了,泛型类型是作用在泛型类上的,一些静态的方法或这静态的属性不能够使用泛型类的变量类型,编译器会直接报错;
5.8 不能抛出或者捕获泛型类的实例
Java既不能抛出也不能捕获泛型类对象,实际上,甚至泛型类扩展Throwable都是不合法的。
1 | public static <T extends Throwable> void doWork(Class<T> t){ |
在异常规范中使用类型变量是允许的,如下:
1 | public static <T extends Throwable> void doWork(Class<T> t) throws T { |
5.9 可以消除对受查异常的检查
Java异常处理要求必须为所有的受查异常提供一个处理器,但是使用泛型,可以规避这一点;
1 |
|
调用上面的方法,编译器会认为t是一个非受查异常;
5.10 注意擦除后的冲突
比如一个泛型类的equals方法,擦除之后,和Object的equals冲突;解决办法是重新命名引发错误的方法;
6、泛型的继承关系
如果Manage extends Employee,那么Pair< Manage >是Pair< Employee >的子类吗? 不是的!
但是泛型类可以扩展或实现其他的泛型类,很典型的一个例子ArrayList:
1 | public class ArrayList<E> extends AbstractList<E> |
ArrayList[E]继承了AbstractList[E];
对于Java泛型的一些思考
编译器如何推断出具体的类型? 参考资料:深入理解 Java 泛型
------------------- 他日若遂凌云志 敢笑黄巢不丈夫 -------------------