背景和问题
我们需要通过方法的参数类型,创建参数的实例。本地开发测试正常,部署测试环境提示反射异常。
为便于理解,改为学生与学校的关系表示。
代码:
1interface IStudent { 2 void learn(); 3} 4 5interface ISchool<R extends IStudent> { 6 void add(R r); 7} 8 9class MiddleStudent implements IStudent{ 10 [@Override](https://my.oschina.net/u/1162528) 11 public void learn() { 12 //TODO 13 } 14} 15 16class MiddleSchool implements ISchool<MiddleStudent> { 17 [@Override](https://my.oschina.net/u/1162528) 18 public void add(MiddleStudent middleStudent) { 19 /TODO 20 } 21} 22public class Test { 23 public static void main(String[] args) throws IllegalAccessException, InstantiationException { 24 //所有方法 25 Method[] methods = MiddleSchool.class.getMethods(); 26 for (Method method : methods) { 27 System.out.println(method.getName()); 28 //找add方法 29 if("add".equals(method.getName())){ 30 //取第一个参数的class 31 Class clazz = method.getParameterTypes()[0]; 32 //创建实例 33 clazz.newInstance(); 34 } 35 } 36 } 37}
异常
1Exception in thread "main" java.lang.InstantiationException: IStudent 2 at java.lang.Class.newInstance(Class.java:427) 3 at Test.main(Test.java:44) 4 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 5 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 6 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 7 at java.lang.reflect.Method.invoke(Method.java:497) 8 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144) 9Caused by: java.lang.NoSuchMethodException: IStudent.<init>() 10 at java.lang.Class.getConstructor0(Class.java:3082) 11 at java.lang.Class.newInstance(Class.java:412) 12 ... 6 more
通过debug,发现add同名方法有两个,参数分别是MiddleStudent和IStudent,而IStudent是接口,不能实例化。解决方式:
1//判断class类型是接口,返回 2if(clazz.isInterface()){ 3 continue; 4}
为什么会有两个add方法呢?
javap -verbose MiddleSchool.class
1public void add(MiddleStudent); 2 descriptor: (LMiddleStudent;)V 3 flags: ACC_PUBLIC 4 Code: 5 stack=0, locals=2, args_size=2 6 0: return 7 LineNumberTable: 8 line 29: 0 9 LocalVariableTable: 10 Start Length Slot Name Signature 11 0 1 0 this LMiddleSchool; 12 0 1 1 middleStudent LMiddleStudent; 13 14 public void add(IStudent); 15 descriptor: (LIStudent;)V 16 flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC 17 Code: 18 stack=2, locals=2, args_size=2 19 0: aload_0 20 1: aload_1 21 2: checkcast #2 // class MiddleStudent 22 5: invokevirtual #3 // Method add:(LMiddleStudent;)V 23 8: return 24 LineNumberTable: 25 line 24: 0 26 LocalVariableTable: 27 Start Length Slot Name Signature 28 0 9 0 this LMiddleSchool;
编译后已经是两个add方法了,其中add(IStudent)的flags值ACC_BRIDGE,表示桥接方法(桥接方法实际是调用了实际的泛型方法)
为什么要生成桥接方法呢?
对于明确或不明确的泛型类,保证兼容性。如List类:
1 public static void main(String[] args) { 2 //明确泛型 3 List<String> listString = new ArrayList<String>(); 4 listString.add("abc"); 5 //不明确泛型 6 List list = new ArrayList<>(); 7 list.add(new Object()); 8 //结果为true,可见class是同一个 9 System.out.println(listString.getClass() == list.getClass()); 10 }
泛型在编译时编译器会检查往集合中添加的对象的类型是否匹配泛型类型,如果不正确会在编译时就会发现错误,而不必等到运行时才发现错误。因为泛型是在1.5引入的,为了向前兼容,所以会在编译时去掉泛型(泛型擦除),但是我们还是可以通过反射API来获取泛型的信息,在编译时可以通过泛型来保证类型的正确性,而不必等到运行时才发现类型不正确。由于java泛型的擦除特性,如果不生成桥接方法,那么与1.5之前的字节码就不兼容了。
知识点:
- 泛型类型擦除 type erasure
- 桥接方法
感谢前人栽树