对于泛型的使用我想大家都非常熟悉, 但是对于类型擦除, 边界拓展等细节问题, 可能不是很清楚, 所以本文会重点讲解一下; 另外对泛型的了解其实可以看出, 一个语言特性的产生逻辑, 这对我们平时的开发也是非常有帮助的;
一, 为什么会出现泛型
首先泛型并不是 Java 的语言特性, 是直到 JDK1.5 才支持的特性(具体区别后面会讲到); 那么在泛型出现之前是怎么做的呢?
- List list = new ArrayList();
- list.add("123");
- String s = (String) list.get(0);
如上面代码所示, 在集合里面需要我们自己记住放进去的是什么, 取出来的时候再强转; 也就将这种类型转换的错误推迟到了运行时, 即麻烦还不安全, 所以才出现了泛型;
使用场景: 泛型类, 泛型接口, 泛型方法;
- public class Test<T>
- public interface Test<T>
- public <T> void test(T t)
二, 泛型会带来什么样的问题
正如上面所讲泛型并不是 Java 一开始就具有的特性, 所以在后来想要增加泛型的时候, 就必须要兼容以前的版本, Sun 他们想到的折中解决方案就是类型擦除; 意思就是泛型的信息只存在于编译期, 在运行时期所有的泛型信息都被擦除了, 就想没有一样;
- List<String> list1 = new ArrayList<>();
- List<Integer> list2 = new ArrayList<>();
- System.out.println(list1.getClass());
- System.out.println(list2.getClass() == list1.getClass());
- // 打印:
- class java.util.ArrayList
- true
可以看到 List<String > 和 List<String > 在运行时其实都是一样的, 都是 class java.util.ArrayList; 所以在使用泛型的时候需要牢记, 在运行时期没有泛型信息, 也无法获取任何有关参数类型的信息; 所以凡是需要获取运行时类型的操作, 泛型都不支持!
1. 不能用基本类型实例化类型参数
- new ArrayList<int>(); // error
- new ArrayList<Integer>(); // correct
因为类型擦除, 会擦除到他的上界也就是 Object; 而 Java 的 8 个基本类型的直接父类是 Number, 所以基本类型不不能用基本类型实例化类型参数, 而必须使用基本类型的包装类;
2. 不能用于运行时类型检查
- t instanceof T // error
- t instanceof List<T> // error
- t instanceof List<String> // error
- t instanceof List // correct
但是可以使用 clazz.isInstance(); 进行补偿;
3. 不能创建类型实例
T t = new T(); // error
同样可以使用 clazz.newInstance(); 进行补偿;
4. 不能静态化
- private static T t; // error
- private T t; // correct
- private static List<T> list; // error
- private static List<?> list; // correct
- private static List<String> list; // correct
- // e.g.
- class Test<T> {
- private T t;
- public void set(T arg) { t = arg; }
- public T get() { return t; }
- }
因为静态变量在类中共享, 而泛型类型是不确定的, 所以泛型不能静态化; 但是非静态的时候, 编译期可以根据上下文推断出 T 是什么, 例如:
- Test l = new Test();
- System.out.println(l.get());
- l.set("123");
- System.out.println(l.get());
- // javap -v 反编译
- 12: invokevirtual #15 // Method JDK/Test14_genericity$Test.get:()Ljava/lang/Object;
- 15: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
- 18: aload_1
- 19: ldc #17 // String 123
- 21: invokevirtual #18 // Method JDK/Test14_genericity$Test.set:(Ljava/lang/Object;)V
- 24: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
- // ---------------------------
- Test l = new Test();
- System.out.println(l.get());
- l.set("123");
- System.out.println(l.get());
- // javap -v 反编译
- 12: invokevirtual #15 // Method JDK/Test14_genericity$Test.get:()Ljava/lang/Object;
- 15: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
- 18: aload_1
- 19: bipush 123
- 21: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
根据上面的代码, 可以很清楚的看到, 编译器对非静态类型的推导;
另外 List<?>和 List<String > 之所以是正确的, 仍然是因为编译器可以在编译期间就能确定类型转换的正确性;
5. 不能抛出或捕获泛型类的实例
- catch (T t) // error
- class Test<T> extends Throwable // error
因为在捕捉异常时候需要运行时类信息, 并且判断异常的继承关系, 所以不能抛出或捕获泛型类的实例;
6. 不允许作为参数进行重载
- void test(List<Integer> list)
- void test(List<String> list)
因为在运行时期泛型信息被擦除, 重载的两个方法签名就完全一样了;
7. 不能创建泛型数组
对于一点我觉得是最重要的, 关于数组的介绍可以参考, Array 相关 ;
- List<String>[] lists = new ArrayList<String>[10]; // error
- List<String>[] lists1 = (List<String>[]) new ArrayList[10]; // correct
之所以不能创建泛型数组的主要原因:
数组是协变的, 而泛型的不变的;
数组的 Class 信息是在运行时动态创建的, 而运行时不能获取泛型的类信息;
根据上面的讲解可以看出所谓的擦除补偿或者擦除后的修正, 其大体思路都是用额外的方法告知运行时的类型信息, 可以是记录到局部变量, 也可以是指定参数的确切类型(Array.newInstance(Class<?> componentType, int length));
三, 边界拓展
基于安全的考虑 Java 泛型是不变的(避免取出数据时的类型转换错误);
List<Object> list = new ArrayList<String>(); // error
所以在使用集合类的时候, 每个集合都需要强制指定确切类型就有点不方便, 比如我想指定一个集合存放 A 以及 A 的子类; 在这种情况下就引入了 extends,super,? 来拓展和管理泛型的边界;
1. 无界通配符 <?>
通配符主要用于泛型的使用场景(泛型一般有 "声明" 和 "使用" 两种场景);
通常情况下 <?> 和原生类型大致相同, 就像 List 和 List<?>的表现大部分都是一样的; 但是要注意他们其实是有本质去别的,<?> 代表了某一特定的类型, 但是编译器不知道这种类型是什么; 而原生的表示可以是任何 Object, 其中并没有类型限制;
- List<?> list = new ArrayList<String>(); // correct
- list.add("34"); // error
- String s = list.get(0); // error
- Object o = list.get(0); // correct
- boolean add(E e);
上面的代码很明确的反应了这一点(<?> 代表了某一特定的类型, 但是编译器不知道这种类型是什么),
因为编译器不知道这种类型是什么, 所以在添加元素的时候, 当然也就不能确认添加的这个类型是否正确; 当使用 <?> 的时候, 代码中的 add(E e)方法, 此时的 E 会被替换为 <?>, 实际上编译器为了安全起见, 会直接拒绝参数列表中涉及通配符的方法调用; 就算这个方法没有向集合中添加元素, 也会被直接拒绝;
当 List<?>取出元素的时候, 同样因为不知道这个特定的类型是什么, 所以只能将取出的元素放在 Object 中; 或者在取出后强转;
2. 上界 <extends>
extends, 主要用于确定泛型的上界;
- <T extends Test>
- // 泛型声明
- <T extends Test & interface1 & interface2>
- // 声明泛型是可以确定多个上界
- <? extends T>
- // 泛型使用时
界定的范围如图所示:
应当注意的是当 extends 用于参数类型限定时:
- List<? extends List> list = new ArrayList<ArrayList>(); // correct
- list.add(new ArrayList()); // error
- List l = list.get(0); // correct
- ArrayList l = list.get(0); // error
上面的分析同无界通配符类似, 只是 List l = list.get(0); 是正确的, 是因为 <? extends List> 界定了放入的元素一定是 List 或者 list 的子类, 所以取出的元素能放入 List 中, 但是不能放入 ArrayList 中;
3. 下界 <super>
super, 主要用于确定泛型的下界; 如图所示:
- List<? super HashMap> list = new ArrayList<>(); // correct
- LinkedHashMap m = new LinkedHashMap(); // correct
- HashMap m1 = m; // correct
- Map m2 = m; // correct
- list.add(m); // correct
- list.add(m1); // correct
- list.add(m2); // error
- Map mm = list.get(0); // error
- LinkedHashMap mm1 = list.get(0); // error
根据图中的范围对照代码, 就能很快发现 Map 在 List<? super HashMap > 的范围之外; 而编辑器为了安全泛型下界集合取出的元素只能放在 Object 里面;
4. PECS 原则
PECS 原则是对上界和下界使用的归纳, 即 producer-extends, consumer-super; 结合上面的两幅图, 表示:
extends, 只能读, 相当于生产者, 向外产出;
super, 只能写, 相当于消费者, 只能接收消费;
同时边界不能同时规定上界和下界, 正如图所示, 他们的范围其实是一样的, 只是开口不一样;
5. 自限定类型
对于上面讲的泛型边界拓展, 有一个很特别的用法,
- class Test<T extends Test<T>> {
- }
- public <T extends Comparable<T>> T max(List<T> list) {
- }
自限定类型可以通俗的解释, 就是用自己限定自己, 即自和自身相同的类进行某操作; 如上面的 max 方法, 就表示可以和自身进行比较的类型;
那么如果想要表达只要是同一祖先就能相互比较呢?
- public <T extends Comparable<? super>> T max(List<? extends T> list) {
- }
- <T extends Comparable<? super>>: 表明只要是同一祖先就能相互比较,<? extends T > 表明集合中装的都是同一祖先的元素;(出至《Effective Java》第 28 条)
总结
对于泛型的时候首先要很清楚的知道, 在运行时没有任何泛型的信息, 全部都被擦除掉了;
需要知道 Java 泛型做不到的事情;
需要知道怎么拓展边界, 让泛型更加灵活;
来源: https://www.cnblogs.com/sanzao/p/10415035.html