反射、注解和泛型相关的坑
反射调用方法
根据反射来获取方法,很多人觉得获取方法传入参数就可以了,但是遇到方法的重载的时候,怎么能够知道此次执行走的是哪个方法呢?
但使用反射时的误区是,认为反射调用方法还是根据入参确定方法重载
但是事实证明,反射调用方法,是以反射获取方法时传入的方法名称和参数类型(方法的签名)来确定调用方法的。
泛型经过类型擦除后多出桥接方法的坑
类型擦除:
类型擦除就是把泛型在编译器统一改成 Object 类型
public class Parent<T> { private AtomicInteger count = new AtomicInteger(0); private T val; public void setVal(T val) { System.out.println("parent is called"); System.out.println(count.incrementAndGet()); this.val = val; } }
public class Child1 extends Parent{ public void setVal(String str) { System.out.println("child is called"); super.setVal(str); } public static void main(String[] args){ Child1 child1 = new Child1(); Arrays.stream(child1.getClass().getMethods()).filter(m -> Objects.equals(m.getName(), "setVal")).forEach( ele -> { try { ele.invoke(child1, "val"); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } ); } }
child is called parent is called 1 parent is called 2
从输出结果来看,由于子类继承父类时没有指定类型,所以在获取方法时 getMethods() 获取到了两个方法,一个是子类的,一个是父类的。
由于没有指定类型,所以子类的setVal() 入参是 string. 父类的 入参是 Object 所以不是重写方法,在获取时就会获取到两个
这个问题说明了
- 一是,子类没有指定 String 泛型参数,父类的泛型方法 setVal(T value) 在泛型擦除后是 setVal(Object value),子类中入参是 String 的 setVal 方法被当作了新方法;
- 二是,子类的 setValue 方法没有增加 @Override 注解,因此编译器没能检测到重写失败的问题。这就说明,重写子类方法时,标记 @Override 是一个好习惯。
方法的桥接:
针对于上面的问题,我们想到,既然是getMethods()方法可以获取到父类和子类的方法,那我使用 getDeclaredMethods() 岂不是就可以了,然后子类再加上指定类型,使用注解 @Override 是不是就可以了
public class Child1 extends Parent<String>{ @Override public void setVal(String str) { System.out.println("child is called"); super.setVal(str); } public static void main(String[] args){ Child1 child1 = new Child1(); Arrays.stream(child1.getClass().getDeclaredMethods()).filter(m -> Objects.equals(m.getName(), "setVal")).forEach( ele -> { try { ele.invoke(child1, "val"); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } ); } }
子类改成这样后,发现运行结果更奇怪了:
child is called parent is called 1 child is called parent is called 2
调用了两次的子类方法,我们知道,子类中就一个 setVal() 的方法,为什么会调用两次呢?
这就是在反射的过程中的桥接方法导致的
反编译代码得到:
public class com.starzyn.others.Child1 extends com.starzyn.others.Parent<java.lang.String> { public com.starzyn.others.Child1(); Code: 0: aload_0 1: invokespecial #1 // Method com/starzyn/others/Parent."<init>":()V 4: return public void setVal(java.lang.String); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String child is called 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: aload_0 9: aload_1 10: invokespecial #5 // Method com/starzyn/others/Parent.setVal:(Ljava/lang/Object;)V 13: return public static void main(java.lang.String[]); Code: 0: new #6 // class com/starzyn/others/Child1 3: dup 4: invokespecial #7 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #8 // Method java/lang/Object.getClass:()Ljava/lang/Class; 12: invokevirtual #9 // Method java/lang/Class.getDeclaredMethods:()[Ljava/lang/reflect/Method; 15: invokestatic #10 // Method java/util/Arrays.stream:([Ljava/lang/Object;)Ljava/util/stream/Stream; 18: invokedynamic #11, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate; 23: invokeinterface #12, 2 // InterfaceMethod java/util/stream/Stream.filter:(Ljava/util/function/Predicate;)Ljava/util/stream/Stream; 28: aload_1 29: invokedynamic #13, 0 // InvokeDynamic #1:accept:(Lcom/starzyn/others/Child1;)Ljava/util/function/Consumer; 34: invokeinterface #14, 2 // InterfaceMethod java/util/stream/Stream.forEach:(Ljava/util/function/Consumer;)V 39: return public void setVal(java.lang.Object); Code: 0: aload_0 1: aload_1 2: checkcast #15 // class java/lang/String 5: invokevirtual #16 // Method setVal:(Ljava/lang/String;)V 8: return }
我们可以看到,在编译后,子类文件确实多出了一个入参为 Object 的setVal() 方法,这个方法就是桥接方法
解决的时候只需要再加上 methid.isBridge() 的条件判断就好了
最后小结下,使用反射查询类方法清单时,我们要注意两点:
- getMethods 和 getDeclaredMethods 是有区别的,前者可以查询到父类方法,后者只能查询到当前类。
- 反射进行方法调用要注意过滤桥接方法。
注解能够被继承么
Show me code:
@MyAnnotation(value = "parent") public class Parent { }
public class Child1 extends Parent{ public static void main(String[] args){ Child1 child1 = new Child1(); System.out.println(child1.getClass().getAnnotation(MyAnnotation.class).value()); } }
这样测试时发现是没办法继承到的
如果我们在注解上面加了 @inherted 后呢?发现就可以获取到了
总结:
- 在加了 @Inherted 注解的注解在类级别上是可以进行继承的
- 注解无法在方法级别上进行继承