通俗易懂的泛型原理及其相关知识点

1,671 阅读11分钟

前言

环境: JDK 1.8 Intellij IDEA   / Android  Studio /  ASM ByteViewer /byte viewcoder

最近和同事讨论到泛型擦除和泛型重载、如何获取泛型,本文就泛型以及泛型使用过程中会涉及到的注解和反射获取一些泛型类型进行分析。下篇文章将分析在泛型擦除机制下,Retrofit 使用动态代理和注解、反射,如何能正确处理数据的。

解决的问题

什么是泛型?

泛型存在的意义?

泛型的原理和泛型擦除

如何获取泛型?

泛型擦除了,反射怎么还能获取?

你了解泛型重载吗? 

反序列化时  TypeToken 为何需要使用匿名内部类方式  ?

枚举类耗内存具体是为什么?

阅读完本文大概需要10分钟 +  耐心

目录

一、泛型

1、什么是泛型?

泛型是JDK 1.5 中引入的新特性,本质是参数化类型,在编译时会检测是否为非法类型。

JDK 7以下可以类似这样写

Fruit<Apple> apples = new  Fruit<Apple>();

JDK 7 及以上可以这样写

Fruit<Apple> apples  =  new Fruit<>();

当然也可以按 JDK 7 以前的写法 <Apple>() ,但是编译器会提示换成 <>()JVM完全可以推断出来类型,不需要在后面表明了。

注意,泛型变量是不能声名成静态的,例如 public static T t ,实例化是在定义泛型类型对象时指定的,对象没有创建,不能确定这个泛型是什么。还有泛型实例也不能创建,例如 new T(),这些IDE 都会有提示。

数组是协变的,泛型擦除后无法满足数组协变的原则,例如如果AppleFruit的子类型,那么数组类型Apple[]就是Fruit[]的子类型,所以这就使得此向上转型是成立的。但是你肯定没有遇到过 List<Apple>[]List<Fruit> 这样的写法,因为泛型擦除后,运行时不知道这两个到底是什么类型。

2、泛型存在的意义

这个就需要配合对泛型使用的理解,明白它的一些作用。

具体主要作用如下:

  • 增强编译时的错误检测,减少因类型问题引发的运行时异常 。如果按模块打包,可能会检测不出。

    例如 List data = new ArrayList() 是不是可以添加任意类型?你取出来时,该如何判断是什么类型呢?如果使用泛型,List<String> data = new ArrayList<>() ,那么取出来肯定是String类型

  • 避免类型转换

  • 增加代码的复用性,写基类时经常用到泛型,方便扩展和子类复用

3、泛型原理和泛型擦除

首先了解两个概念

可具体化类型 : 一个可以在整个运行时直到其类型信息的类型,包括基本类型、非泛型类型、原始类型等

不可具体化类型 :无法在整个运行时可知其类型信息的类型,其类型信息已在编译时擦除,如 List<String>List<Number>JVM 无法在运行时分辨这两者

我们看一下下面的代码,猜下结果是什么?

AppleBanana 都是继承自 Fruit

ArrayList<Apple> apples = new ArrayList<>();
ArrayList<Banana> banbans = new ArrayList<>();​
System.out.println(apples.getClass() == banbans.getClass());

结果打印 是 true 。为什么呢?肯定和泛型有关

使用 javap 或者 Bytecoder Viewer 等插件,查看一个使用了泛型的类( 注意:在查看字节码过程中,直接点击class 文件,查看到的信息和 java 文件没什么差别,这里看到的是签名,保留了定义的格式,这个信息会存在类的常量池里),如下图

我们可以看到这个类的 get set 方法所对应的 字节码,泛型信息 变成了 Object , 这就是泛型擦除

我们改写一下FruitManager 如下图

get set 方法中泛型信息变成了 Fruit ,也就是这里的泛型会擦除成自己的父类

然后我们再修改FruitManager 类,查看如下图

这里对应的字节码相比于上面的都类似,唯一不同的时上图右边的 synthetic bridge set 方法(对应于父类的set方法),这个方法称为桥方法,桥接的意思,内部会检查类型,强转,调用 第一个 set 方法(对应于子类的set方法)。

不妨可以看下AbstractFruitManager 类生成的字节码

既然 FruitManager 类继承了 AbstractFruitManager类,那么从字节码角度来说,子类FruitManager 中是不是需要实现 上图 右侧的 set 字节码方法?需要。那么子类是如何实现的呢?就是通过桥方法

将字节码抽象成如下

public class  AbstractFruitManager{
    void set(Object o);
}
public class FruitManager{
    public void set(Furit fruit){}
    
    @Override
    public synthetic  bridge  set(Object o){
        set((Fruit)o);
    }
    .....
}

总结

泛型原理: 泛型是JDK 5 新引入的新特性,为了向下兼容,虚拟机其实是不支持泛型,所以Java 实现的是一种伪泛型机制,即Java在编译器擦除了所有的泛型信息,这样Java就不需要产生新的类型到字节码,所有泛型类型最终都是同一种原始类型,在Java运行时不存在泛型信息。

泛型擦除:会检查泛型 T 是否有限制,无限制转换成 Object 类,有限制则转换成限制类。如果继承类或者接口,然后子类实现了或者重写了它们的泛型方法,编译器会为 父类或者父接口的方法生成桥方法(保持多态性),在桥方法里会再调用子类的方法

从上面对泛型擦除的描述,其实还能发现,泛型是存不了基本类型的,例如List<int> int 转换不了ObjectObject也不能存放int类型,同理泛型 使用 instanceOf 也需要注意泛型擦除,例如 List<String> list , list instanceOf List<String> 这肯定是不能判断的,泛型String丢失了,除非是 List<?>

也就是因为泛型擦除的缘故,List<String> list,反射调用add方法,放入一个Integer类型数据,是成功的。

并且泛型参数化也不考虑继承关系

例如List<String> list = new ArrayList<Object>() ,这个是会编译错误的,这个可以等价于

List<Object> list = new  ArrayList<>();
list.add(new  Object());
List<String> list2 =list;

假设不是错误的,我们就可以通过 list添加各种各样的对象,然后获取时转换成 String 类型对象,显然不行。

还有一个问题,例如

FruitManager<? super Apple> manager2 =  new  FruitManager<Fruit>();
manager2.set(new Apple());  //这里是没问题的
Apple apple = manager2.get(); // 这个编译错误,是有问题的,Apple的任意父类Object object = manager2.get();

因为泛型擦除,这里限定了下界Apple,会擦除成Object并不知道具体的类型,所以符合编译器报错,适合存放数据

换种写法

FruitManager<? extend Fruit> manager  = new FruitManager<>();
manager.set(new Apple());//这个是会报错的 Fruit的任意子类
Fruit fruit = manager.get();//这个是不会错的

这个限定了上界,泛型擦除为 Fruit ,适合读数据

从这引申出泛型的PESC原则,即 Producer extends Consumer super? super T 负责生产 set? extend T 负责消费 get 。例如 Collections<>.copy(List<? super T> dest, List<? extends T> src) 方法。

4、获取泛型类参数信息

ParameterizedTypeJDK 大于等于 1.7 提供的,参数化类型,指的是声明时带有泛型的类型,它是一个接口,继承自Type类,而TypeJava编程语言中所有类型的通用超级接口,这些类型包括原始类型、参数化类型、数组类型等。

TypeVariable 也是继承自Type类,代表的是泛型尖括号里的东西,例如List<T> 里面的 T ,或者是 T tT

一般可以通过以下类型代码获取泛型

Type type = getClass().getGenericSuperclass();//返回此Class所表示的实体(类、接口、基本类型等)的直接超类的Type
if( type instanceof ParameterizedType ){
     ParameterizedType parametrizedType = (ParameterizedType)type;
     Type clazz = parametrizedType.getActualTypeArguments()[0];//返回此类型实际类型参数的Type对象的数组
     if( clazz instanceof Class ){
          this.clazz = (Class<T>) claz;
      }
}

ParameterizedType 还有其它方法,例如 getRawType() 获取泛型尖括号前面类型,List<String> 获取的是List , getOwnerType() 获取的就是声明类型。

5、泛型擦除了,反射怎么还能获取?

为什么泛型擦除后,反射能得到泛型类型?其实泛型信息还是保留在了类的常量池里面的。

我们先看下面这段代码

public class Test {
    public static void main(String[] args){
        ArrayList<String> list =new ArrayList<>();
        list.add("1");
        String data = list.get(0);
    }
}

ArrayList get 源码如下

  public E get(int index) {
     rangeCheck(index);
     return elementData(index);
 }
 E elementData(int index) {
     return (E) elementData[index];
 }

elementDataObject 数组,get 方法里 它会强转成 E ,即 String。但是你看到的是真的吗?泛型擦除后,这里E 不就变成Object了吗?怎么能获取到呢?

Test 代码对应的字节码如下

可以看到ArrayList get 方法返回的是 Object , 然后通过 CHECKCAST 强制转换成了 String,所以 String 类型不是在 get 方法里 的时候强转的,而是在你调用的地方强转的。

6、泛型重载

这上面报错的编译前检查失败,是因为编译之后类型擦除List<String>List<Apple> 是一样的。

对应字节码如下

从上面你会发现List<String>List<Date> 其实都会被擦除List,当然如果返回值不一样,也不行,假设其中一个eat的返回值 为 int它们两个方法伪代码类似,相当于两个普通方法,形参一样,返回值不一样

void eat(List list)int eat(List list)

那么,你有可能会问,方法名一样,返回值不一样呢?如下

上述两个 String 类型参数的 eat 方法,是不能共同存在的,会报错。虽然字节码有区别,但是JVM不允许类似的普通方法重载。看了几篇文章,都说JVM允许类似的两个方法重载??因为他们是基于JDK 1.6分析的,JDK 1.7 已经不允许了,编译器就做了检查操作,保证行为一致

JVM规定函数的返回值不参与"函数签名"的生成,不参与重载选择,函数的特征签名仅仅包括方法名称、参数类型以及参数顺序,字节码中特征签名还包括了方法的返回值以及受检查异常表。

既然有重载,肯定会有多态,多态体现就在协变和逆变。

协变 List<? extend Number> list = new ArrayList<Integer>()

逆变 List<? super Number> list = new ArrayList<Object>()

为了将泛型融入多态。将泛型存在的场景尽可能的满足里氏替换原则

二、Type获取真实泛型类型

TypeJava 编程语言中所有类型的公共高级接口,是对Java编程语言类型的一个抽象

Type体系包含原始类型 Class (实现类)、参数化类型ParameterizedType、数组类型GenericArrayType、类型变量TypeVariable、通配符泛型 WildcardType

1、Type类型介绍

TypeVariable

Type[] getBounds()

返回此类型参数的上界列表,如果没有上界则返回Object. 例如 V extends @Custom Number & Serializable 这个类型参数,有两个上界,NumberSerializable

D getGenericDeclaration()

类型参数声明时的载体,例如 class TypeTest<T, V extends @Custom Number & Serializable> ,那么V 的载体就是TypeTest

String getName()

AnnotatedType[] getAnnotatedBounds()

Java 1.8加入 AnnotatedType: 如果这个这个泛型参数类型的上界用注解标记了,我们可以通过它拿到相应的注解

package demo;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;

/**
 * TypeVariable 泛型变量 获得泛型上下限等信息
 *
 * @param <T>
 * @param <V>
 */
public class TypeVariableTest<T extends Comparable & Serializable, V> {
    T key;
    V value;

    public static void main(String[] args) {
        //获取字段类型
        try {
            Field fieldKey = TypeVariableTest.class.getDeclaredField("key");
            Field fieldValue = TypeVariableTest.class.getDeclaredField("value");

            TypeVariable keyType = (TypeVariable) fieldKey.getGenericType();
            TypeVariable valueType = (TypeVariable) fieldValue.getGenericType();

            //getName
            System.out.println(keyType.getName());  //T
            System.out.println(valueType.getName());  //V

            //getGenericDeclaration
            System.out.println(keyType.getGenericDeclaration());  //class demo.TypeVariableTest
            System.out.println(valueType.getGenericDeclaration());  //class demo.TypeVariableTest

            //getBounds
            System.out.println("Key的上界");
            for (Type type : keyType.getBounds()) {
                System.out.println(type);    //interface java.lang.Comparable
interface java.io.Serializable
            }
            System.out.println("Value的上界");
            for (Type type : valueType.getBounds()) {
                System.out.println(type);       //class java.lang.Object
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出信息

T
V
class demo.TypeVariableTest
class demo.TypeVariableTest
Key的上界
interface java.lang.Comparable
interface java.io.Serializable
Value的上界
class java.lang.Object

ParameterizedType

Type[] getActualTypeArguments()

获取参数类型<>里面的那些值,例如Map<K,V> 那么就得到 [K,V]的一个数组 

Type getRawType()

获取参数类型<>前面的值,例如例如Map<K,V> 那么就得到 Map

Type getOwnerType()

获取其父类的类型,例如Map 有一个内部类Entry, 那么在Map.Entry<K,V> 上调用这个方法就可以获得 Map

public class ParameterizedTypeTest {
    Map<String,String> map;
    Map.Entry<String,String> entryMap ;

    public static void main(String[] args){
        try {
            Field field = ParameterizedTypeTest.class.getDeclaredField("map");
            System.out.println(field.getGenericType());  //java.util.Map<java.lang.String, java.lang.String>

            ParameterizedType pType = (ParameterizedType) field.getGenericType();
            System.out.println(pType.getRawType());  //interface java.util.Map

            for (Type type :pType.getActualTypeArguments()){
                System.out.println(type);  //class java.lang.String  class java.lang.String
            }
            System.out.println(pType.getOwnerType());  //null

            Field field2 = ParameterizedTypeTest.class.getDeclaredField("entryMap");
            ParameterizedType pType2 = (ParameterizedType) field2.getGenericType();
            System.out.println(pType2.getOwnerType());  //interface java.util.Map
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

输出内容如下

java.util.Map<java.lang.String, java.lang.String>
interface java.util.Map
class java.lang.String
class java.lang.String
null
interface java.util.Map

GenericArrayType

Type getGenericComponentType()

获取泛型类型数组的声明类型,即获取数组方括号 [] 前面的部分

public class GenericArrayTypeTest<T> {
    List<String>[] lists;

    public static  void main(String[] args){
        try {
            Field f = GenericArrayTypeTest.class.getDeclaredField("lists");
            GenericArrayType genericArrayType = (GenericArrayType) f.getGenericType();
            System.out.println(genericArrayType.getGenericComponentType()); //java.util.List<java.lang.String>

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

输出内容如下

java.util.List<java.lang.String>

WildcardType

public class WildcardTypeTest {
    private List<? extends String> up;
    private List<? super Integer> down;

    public static void main(String[] args) {
        try {
            Field fieldA = WildcardTypeTest.class.getDeclaredField("up");
            Field fieldB = WildcardTypeTest.class.getDeclaredField("down");

            //拿到泛型类型
            ParameterizedType pTypeA = (ParameterizedType) fieldA.getGenericType();
            ParameterizedType pTypeB = (ParameterizedType) fieldB.getGenericType();

            //从泛型里拿到通配符类型
            WildcardType wTypeA = (WildcardType) pTypeA.getActualTypeArguments()[0];
            WildcardType wTypeB = (WildcardType) pTypeB.getActualTypeArguments()[0];

            System.out.println(wTypeA.getUpperBounds()[0]); //class java.lang.String
            System.out.println(wTypeB.getLowerBounds()[0]); //class java.lang.Integer

            System.out.println(wTypeA);  //? extends java.lang.String
            System.out.println(wTypeB);   //? super java.lang.Integer

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

输出内容如下

class java.lang.String
class java.lang.Integer
? extends java.lang.String
? super java.lang.Integer

2、TypeToken反序列化问题

Type的几种类型介绍完了,下面看一下比较经典的问题,反序列化

网络请求数据 Gson反序列化解析

public class BaseResponse<T> {
    T data;
    int code;
    String message;

    public BaseResponse(T data, int code, String message) {
        this.data = data;
        this.code = code;
        this.message = message;
    }

    @Override
    public String toString() {
        return "BaseResponse{" +
                "data=" + data +
                ", code=" + code +
                ", message='" + message + '\'' +
                '}';
    }
}
public class Data {
    String result;

    public Data(String result) {
        this.result = result;
    }

    @Override
    public String toString() {
        return "Data{" +
                "result='" + result + '\'' +
                '}';
    }
}

运行

BaseResponse<Data> baseResponse = new BaseResponse<>(new Data("数据"),200,"success");
Gson gson = new Gson();
String json = gson.toJson(baseResponse);
BaseResponse<Data> response = gson.fromJson(json,new TypeToken<BaseResponse<Data>>(){}.getType());
System.out.println(response.data.getClass()); //class com.demo.hehe.Data
System.out.println(response.data.result);  //数据
        
输出结果
class com.demo.hehe.Data
数据

我们看一下上面调用的TypeToken的内部实现

  protected TypeToken() {
    this.type = getSuperclassTypeParameter(getClass());
    this.rawType = (Class<? super T>) $Gson$Types.getRawType(type);
    this.hashCode = type.hashCode();
  }

我们发现加了protected 修饰符,不同的类在不同的包名下,必须通过new TypeToken(){} 来使用,否则会报错提示。

你可能会问,我不加protected,然后能不能达成一样的效果?

public class TypeReference<T> {
    Type type;
    public TypeReference(){
        this.type = getClass().getGenericSuperclass();
        ParameterizedType parameterizedType = (ParameterizedType) type;
        
        Type[] actualType = parameterizedType.getActualTypeArguments();
        type = actualType[0];
    }

    public Type getType() {
        return type;
    }
}

将之前的TypeToken换成TypeReference,效果是一致的,但是如果实现方式换成

BaseResponse<Data> response = gson.fromJson(json,new TypeReference<BaseResponse<Data>>().getType());

{} 去掉,即不是匿名内部类的方式,然后运行报错

java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType

	at com.demo.hehe.TypeReference.<init>(TypeReference.java:10)
	at com.demo.hehe.ExampleUnitTest.addition_isCorrect(ExampleUnitTest.java:22)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
......

为什么加了 {} 就可以,不加就不可以?

由于泛型擦除new TypeReference<BaseResponse<Data>> 被擦除成 new TypeReference<Object> 类似这样了。

通过加 {} 生成一个匿名内部类,相当于一个新类 ,指定了泛型类型的子类

class ChildTypeRefreence{
    BaseResponse<Data> t;
}

为什么?

我们先查看一下 new TypeReference<BaseResponse<Data>>().getType() 方式,显然是泛型被擦除了

再查看一下通过 new TypeReference<BaseResponse<Data>>(){}.getType() 方式

那这个Test$1类文件在哪呢?

一般我们编译后的 class 都在 build/intermediates/javac/debug/包名 下(针对 Android Studio项目),但是没有找到 Test$1 类文件,然后全局搜索项目文件,发现就在该目录,IDE 没有显示而已,我们打开该文件

class Test$1 extends TypeReference<BaseResponse<Data>> {
    Test$1(Test this$0) {
        this.this$0 = this$0;
    }
}

加了protected 修饰符,那么不同包下,想要使用就必须通过匿名内部类形式,就需要加{} 来使用,这样相当于创建了新的实体类,确定了泛型类型,编译才能将泛型签名信息记录到Class元数据中。

三、注解

对于注解的介绍可以看这篇文章 Java注解 ,你需要了解 注解、元注解、@Target注解类型、@Retention注解保留级别

声明注解 使用 @interface

元注解

在定义注解时,注解类也能够使用其他的注解声明,对注解类型进行注解的注解类。

@Target

  • ElementType.ANNOTATION_TYPE 可以应用于注解类型

  • ElementType.CONSTRUCTOR 可以应用于构造函数

  • ElementType.FIELD 可以应用于字段或属性

  • ElementType.LOCAL_VARIABLE 可以应用于局部变量

  • ElementType.METHOD 可以应用于方法级注解

  • ElementType.PACKAGE 可以应用于包声明

  • ElementType.PARAMETER 可以应用于方法的参数

  • ElementType.TYPE 可以应用于类的任何元素

@Retention

一般使用时需要注意@Retention,它有三种保留级别

RetentionPolicy.SOURCE - 标记的注解仅保留在源级别中,并被编译器忽略。
RetentionPolicy.CLASS - 标记的注解在编译时由编译器保留,但 Java 虚拟机(JVM)会忽略。 
RetentionPolicy.RUNTIME - 标记的注解由 JVM 保留,因此运行时环境可以使用它。

我们可以看下三种不同的级别对应的字节码

TestActivity的代码如下

@Route(path = "/test/activity")public class TestActivity {
}

这三种保留级别在对应的字节码中已经体现出来了,SOURCE级别的在字节码中找不到注解,CLASSRUNTIME级别的可以找到,但是它们区别于CLASS级别的字节码注解后面有一个invisible标签。

那它们可以应用于哪些场景呢?

级别

技术

说明

源码

APT / 语法检查

在编译器能够获取注解与注解声明的类,包括类中所有成员信息,一般用于自动生成额外的类文件或者在枚举类优化中,使用注解可以检查入参

字节码

ASM / AspectJ / Roubust

在编译出Class后,通过修改Class数据以实现修改代码的逻辑。对于是否需要修改的区分或者修改为不同逻辑的判断可以使用注解

运行时

反射

在程序运行期间,通过反射技术动态获取注解及其元素,从而完成不同的逻辑判断

对于APT技术,可以看之前 文件生成-组件化跳转分析

对于语法检查,我们可以看平时自己写的一些枚举类,例如下图

当使用了@IntDef注解限定值只能为 TYPE_ONETYPE_TWO 时,传入其它的编译器会提示,但实际编译还是能通过的。

说到这个,这里提一下,为什么枚举不建议使用,当然也得看具体业务场景。看下图

从字节码里可以看到会生成四个对象,每个对象占用内存为 对象头 + 对齐填充引用类型对象,开启指针压缩为4,不开启为8普通对象开启为12,不开启为8数据对象头开启为16,不开启为24。简单可以理解为每个对象为 12 个字节 + 对齐填充。图中枚举内存占用比一个对象多了3倍,因此网上大多建议优化此类枚举。

对于字节码增强ASMAspectJRoubust 可以学习一下,可以实现一些登录拦截、ARouter整个项目路径校验等,后续抽空学习并分享。

对于反射,可以看 EventBus 如何注册、解注册和发送事件的,如果开启了Index,其实就属于APT技术了。默认不开启Index,内部是通过反射收集 @Subscribe 注解方法的。