反射、注解和泛型相关的坑

简介: 根据反射来获取方法,很多人觉得获取方法传入参数就可以了,但是遇到方法的重载的时候,怎么能够知道此次执行走的是哪个方法呢?

反射、注解和泛型相关的坑

反射调用方法

根据反射来获取方法,很多人觉得获取方法传入参数就可以了,但是遇到方法的重载的时候,怎么能够知道此次执行走的是哪个方法呢?

但使用反射时的误区是,认为反射调用方法还是根据入参确定方法重载

但是事实证明,反射调用方法,是以反射获取方法时传入的方法名称和参数类型(方法的签名)来确定调用方法的。

泛型经过类型擦除后多出桥接方法的坑

类型擦除:

类型擦除就是把泛型在编译器统一改成 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 注解的注解在类级别上是可以进行继承的
  • 注解无法在方法级别上进行继承
相关文章
|
7月前
|
Java
【反射】Java反射机制 -- 常用构造器与方法
【反射】Java反射机制 -- 常用构造器与方法
67 0
|
4月前
|
Java 程序员 API
注解和反射
这篇文章详细介绍了Java注解的概念、作用、内置注解、元注解、自定义注解,
注解和反射
|
5月前
|
Java 编译器
注解和反射(二)
注解和反射
30 0
|
5月前
|
Java 程序员 编译器
注解和反射(一)
注解和反射
34 0
|
Java
66.【注解与反射】(三)
66.【注解与反射】
46 0
|
编译器
反射、注解和泛型相关的坑
泛型 类型擦除 桥接方法
|
Oracle Java 关系型数据库
反射与自定义注解
反射与自定义注解
反射与自定义注解