当前位置:   article > 正文

深入分析Java反射(三)-泛型_java 反射 泛型

java 反射 泛型

前提#

Java反射的API在JavaSE1.7的时候已经基本完善,但是本文编写的时候使用的是Oracle JDK11,因为JDK11对于sun包下的源码也上传了,可以直接通过IDE查看对应的源码和进行Debug。

本文主要介绍反射中一个比较难的问题-泛型

泛型的简介#

泛型是在2004年JavaSE 5.0(JDK1.5)版本中添加到Java编程语言中的泛型编程工具。泛型的设计是为了应用在Java的类型系统,提供"用类型或者方法操作各种类型的对象从而提供编译期的类型安全功能(原文:a type or method to operate on objects of various types while providing compile-time type safety)"。但是在2016年的一些研究表明,泛型并不是在所有的情况下都能保证编译期的类型安全,例如切面(Aspect)编程的编译期类型安全并没有完全实现。

泛型的一个最大的优点就是:提供编译期的类型安全。举个很简单的例子,在引入泛型之前,ArrayList内部只维护了一个Object数组引用,这种做法有两个问题:

  • 从数组列表获取一个元素的时候必须进行类型的强转。
  • 向数组列表中可以添加任何类型的对象,导致无法得知数组列表中存放了什么类型的元素。

引入泛型之后,我们可以通过类型参数明确定义ArrayList

  1. ArrayList<String> list = new ArrayList<String>();
  2. // JavaSE 7以后的版本中构造函数可以省略类型,编译器可以推导出实际类型
  3. ArrayList<String> list = new ArrayList<>();

下面先列举出Java中泛型的一些事实:

  • Java虚拟机中不存在泛型,只有普通的类和方法,但是字节码中存放着泛型相关的信息
  • 所有的类型参数都使用它们的限定类型替换。
  • 桥方法(Bridge Method)由编译器合成,用于保持多态(Java虚拟机利用方法的参数类型、方法名称和方法返回值类型确定一个方法)。
  • 为了保持类型的安全性,必要时需要进行类型的强制转换。

理解类型擦除#

类型擦除是什么#

类型擦除(或者更多时候喜欢称为"泛型擦除")的具体表现是:无论何时定义一个泛型类型,都自动提供一个相应的原始类型(Raw Type,这里的原始类型并不是指int、boolean等基本数据类型),原始类型的类名称就是带有泛型参数的类删去泛型参数后的类型名称,而原始类型会擦除(Erased)类型变量,并且把它们替换为限定类型(如果没有指定限定类型,则擦除为Object类型),举个例子Pair<T>带有泛型参数的类型如下:

  1. public class Pair<T>{
  2. private T first;
  3. private T second;
  4. public Pair(T first,T second){
  5. this.first = first;
  6. this.second = second;
  7. }
  8. public T getFirst(){
  9. return first;
  10. }
  11. public T getSecond(){
  12. return second;
  13. }
  14. }

擦除类型后的Pair<T>的原始类型为:

  1. public class Pair{
  2. private Object first;
  3. private Object second;
  4. public Pair(Object first,Object second){
  5. this.first = first;
  6. this.second = second;
  7. }
  8. public Object getFirst(){
  9. return first;
  10. }
  11. public Object getSecond(){
  12. return second;
  13. }
  14. }

举个更复杂的例子,如果泛型参数类型是有上限的,变量会擦除为上限的类型:

  1. public class Interval<T extends Comparable & Serializable> implements Serializable {
  2. private T lower;
  3. private T upper;
  4. public Interval(T lower, T upper) {
  5. this.lower = lower;
  6. this.upper = upper;
  7. }
  8. //省略其他方法
  9. }

类型擦除后的Interval<T extends Comparable & Serializable>原始类型:

  1. public class Interval implements Serializable {
  2. private Comparable lower;
  3. private Comparable upper;
  4. public Interval(Comparable lower, Comparable upper) {
  5. this.lower = lower;
  6. this.upper = upper;
  7. }
  8. //省略其他方法
  9. }

像上面这种多个泛型上限的类型,应该尽量把标识接口上限类型放在边界列表的尾部,这样做可以提高效率。

为什么需要擦除类型#

在JDK1.5之前,也就是在泛型出现之前,所有的类型包括基本数据类型(int、byte等)、包装类型、其他自定义的类型等等都可以使用类文件(.class)字节码对应的java.lang.Class描述,也就是java.lang.Class类的一个具体实例对象就可以代表任意一个指定类型的原始类型。这里把泛型出现之前的所有类型暂时称为"历史原始类型"。

在JDK1.5之后,数据类型得到了扩充,出历史原始类型扩充了四种泛型类型:参数化类型(ParameterizedType)、类型变量类型(TypeVariable)、限定符类型(WildcardType)、泛型数组类型(GenericArrayType)。历史原始类型和新扩充的泛型类型都应该统一成各自的字节码文件类型对象,也就应该把泛型类型归并进去java.lang.Class中。但是由于JDK已经迭代了很多版本,泛型并不属于当前Java中的基本成分,如果JVM中引入真正的泛型类型,那么必须涉及到JVM指令集和字节码文件的修改(这个修改肯定不是小的修改,因为JDK当时已经迭代了很多年,而类型是编程语言的十分基础的特性,引入泛型从项目功能迭代角度看可能需要整个JVM项目做回归测试),这个功能的代价十分巨大,所以Java没有在Java虚拟机层面引入泛型。

Java为了使用泛型,于是使用了类型擦除的机制引入了"泛型的使用",并没有真正意义上引入和实现泛型。Java中的泛型实现的是编译期的类型安全,也就是泛型的类型安全检查是在编译期由编译器(常见的是javac)实现的,这样就能够确保数据基于类型上的安全性并且避免了强制类型转换的麻烦(实际上,强制类型转换是由编译器完成了,只是不需要人为去完成而已)。一旦编译完成,所有的泛型类型都会被擦除,如果没有指定上限,就会擦除为Object类型,否则擦除为上限类型。

既然Java虚拟机中不存在泛型,那么为什么可以从JDK中的一些类库获取泛型信息?这是因为类文件(.class)或者说字节码文件本身存储了泛型的信息,相关类库(可以是JDK的类库,也可以是第三方的类库)读取泛型信息的时候可以从字节码文件中提取,例如比较常用的字节码操作类库ASM就可以读取字节码中的信息甚至改造字节码动态生成类。例如前面提到的Interval<T extends Comparable & Serializable>类,使用javap -c -v命令查看其反编译得到的字节码信息,可以看到其签名如下:

Signature: #22                          // <T::Ljava/lang/Comparable;:Ljava/io/Serializable;>Ljava/lang/Object;Ljava/io/Serializable;

JAVA 复制 全屏

这里的签名信息实际上是保存在常量池中的,关于字节码文件的解析将来会出一个系列文章详细展开。

Type体系#

前文提到了在JDK1.5中引入了四种新的泛型类型java.lang.reflect.ParameterizedTypejava.lang.reflect.TypeVariablejava.lang.reflect.WildcardTypejava

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/523374
推荐阅读
相关标签
  

闽ICP备14008679号