朱凯-Java泛型和 Kotlin 泛型
    Java 泛型,你了解类型擦除吗?

    类型擦除
    泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。
    这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
    通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时的那段代码。

    1. List<String> l1 = new ArrayList<String>();
    2. List<Integer> l2 = new ArrayList<Integer>();
    3. System.out.println(l1.getClass() == l2.getClass());

    打印的结果为 true 是因为 List<String>List<Integer>在 jvm 中的 Class 都是 List.class。
    泛型信息被擦除了。
    可能同学会问,那么类型 String 和 Integer 怎么办?
    答案是泛型转译。

    1. public class Erasure <T>{
    2. T object;
    3. public Erasure(T object) {
    4. this.object = object;
    5. }
    6. }

    Erasure 是一个泛型类,我们查看它在运行时的状态信息可以通过反射。

    1. Erasure<String> erasure = new Erasure<String>("hello");
    2. Class eclz = erasure.getClass();
    3. System.out.println("erasure class is:"+eclz.getName());

    打印的结果是

    1. erasure class is:com.frank.test.Erasure

    Class 的类型仍然是 Erasure ,并不是 Erasure<T>这种形式,那我们再看看泛型类中 T 的类型在 jvm 中是什么具体类型。

    1. Field[] fs = eclz.getDeclaredFields();
    2. for ( Field f:fs) {
    3. System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
    4. }

    打印结果是

    1. Field name object type:java.lang.Object

    那我们可不可以说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?
    这种说法,不完全正确。
    我们更改一下代码。

    1. public class Erasure <T extends String>{
    2. // public class Erasure <T>{
    3. T object;
    4. public Erasure(T object) {
    5. this.object = object;
    6. }
    7. }

    现在再看测试结果:

    1. Field name object type:java.lang.String

    我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>则类型参数就被替换成类型上限。
    所以,在反射中。

    1. public class Erasure <T>{
    2. T object;
    3. public Erasure(T object) {
    4. this.object = object;
    5. }
    6. public void add(T object){
    7. }
    8. }

    add() 这个方法对应的 Method 的签名应该是 Object.class。

    1. Erasure<String> erasure = new Erasure<String>("hello");
    2. Class eclz = erasure.getClass();
    3. System.out.println("erasure class is:"+eclz.getName());
    4. Method[] methods = eclz.getDeclaredMethods();
    5. for ( Method m:methods ){
    6. System.out.println(" method:"+m.toString());
    7. }

    打印结果是

    1. method:public void com.frank.test.Erasure.add(java.lang.Object)

    也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod(“add”,Object.class)否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。

    类型擦除带来的局限性
    类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。

    理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如
    image.png
    正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配,但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制。

    1. public interface List<E> extends Collection<E>{
    2. boolean add(E e);
    3. }

    上面是 List 和其中的 add() 方法的源码定义。
    因为 E 代表任意的类型,所以类型擦除时,add 方法其实等同于

    1. boolean add(Object obj);

    那么,利用反射,我们绕过编译器去调用 add 方法。

    1. public class ToolTest {
    2. public static void main(String[] args) {
    3. List<Integer> ls = new ArrayList<>();
    4. ls.add(23);
    5. // ls.add("text");
    6. try {
    7. Method method = ls.getClass().getDeclaredMethod("add",Object.class);
    8. method.invoke(ls,"test");
    9. method.invoke(ls,42.9f);
    10. } catch (NoSuchMethodException e) {
    11. // TODO Auto-generated catch block
    12. e.printStackTrace();
    13. } catch (SecurityException e) {
    14. // TODO Auto-generated catch block
    15. e.printStackTrace();
    16. } catch (IllegalAccessException e) {
    17. // TODO Auto-generated catch block
    18. e.printStackTrace();
    19. } catch (IllegalArgumentException e) {
    20. // TODO Auto-generated catch block
    21. e.printStackTrace();
    22. } catch (InvocationTargetException e) {
    23. // TODO Auto-generated catch block
    24. e.printStackTrace();
    25. }
    26. for ( Object o: ls){
    27. System.out.println(o);
    28. }
    29. }
    30. }

    打印结果是:

    1. 23
    2. test
    3. 42.9

    可以看到,利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制。