泛型的反射问题分析

2018-12-18  本文已影响922人  耗子2015

背景和问题

我们需要通过方法的参数类型,创建参数的实例。本地开发测试正常,部署测试环境提示反射异常。

为便于理解,改为学生与学校的关系表示。

代码:

interface IStudent {
    void learn();
}

interface ISchool<R extends IStudent> {
    void add(R r);
}

class MiddleStudent implements IStudent{
    @Override
    public void learn() {
    //TODO
    }
}

class MiddleSchool implements ISchool<MiddleStudent> {
    @Override
    public void add(MiddleStudent middleStudent) {
    /TODO
    }
}
public class Test {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        //所有方法
        Method[] methods = MiddleSchool.class.getMethods();
        for (Method method : methods) {
            System.out.println(method.getName());
            //找add方法
            if("add".equals(method.getName())){
                //取第一个参数的class
                Class clazz = method.getParameterTypes()[0];
                //创建实例
                clazz.newInstance();
            }
        }
    }
}

异常

Exception in thread "main" java.lang.InstantiationException: IStudent
    at java.lang.Class.newInstance(Class.java:427)
    at Test.main(Test.java:44)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: java.lang.NoSuchMethodException: IStudent.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.newInstance(Class.java:412)
    ... 6 more  

通过debug,发现add同名方法有两个,参数分别是MiddleStudent和IStudent,而IStudent是接口,不能实例化。解决方式:

//判断class类型是接口,返回
if(clazz.isInterface()){
    continue;
}

为什么会有两个add方法呢?

javap -verbose MiddleSchool.class

public void add(MiddleStudent);
    descriptor: (LMiddleStudent;)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 29: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   LMiddleSchool;
            0       1     1 middleStudent   LMiddleStudent;

  public void add(IStudent);
    descriptor: (LIStudent;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #2                  // class MiddleStudent
         5: invokevirtual #3                  // Method add:(LMiddleStudent;)V
         8: return
      LineNumberTable:
        line 24: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   LMiddleSchool;

编译后已经是两个add方法了,其中add(IStudent)的flags值ACC_BRIDGE,表示桥接方法(桥接方法实际是调用了实际的泛型方法)

为什么要生成桥接方法呢?

对于明确或不明确的泛型类,保证兼容性。如List类:

 public static void main(String[] args) {
        //明确泛型
        List<String> listString = new ArrayList<String>();
        listString.add("abc");
        //不明确泛型
        List list = new ArrayList<>();
        list.add(new Object());
        //结果为true,可见class是同一个
        System.out.println(listString.getClass() == list.getClass());
    }

泛型在编译时编译器会检查往集合中添加的对象的类型是否匹配泛型类型,如果不正确会在编译时就会发现错误,而不必等到运行时才发现错误。因为泛型是在1.5引入的,为了向前兼容,所以会在编译时去掉泛型(泛型擦除),但是我们还是可以通过反射API来获取泛型的信息,在编译时可以通过泛型来保证类型的正确性,而不必等到运行时才发现类型不正确。由于java泛型的擦除特性,如果不生成桥接方法,那么与1.5之前的字节码就不兼容了。

知识点:

  1. 泛型类型擦除 type erasure
  2. 桥接方法

感谢前人栽树

  1. https://www.ibm.com/developerworks/cn/java/j-jtp01255.html
  2. https://rednaxelafx.iteye.com/blog/586212
  3. https://blog.csdn.net/mhmyqn/article/details/47342577
上一篇 下一篇

猜你喜欢

热点阅读