java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java isBridge() isSynthetic()

Java实现桥接方法isBridge()和合成方法isSynthetic()

作者:秋官

本文主要介绍了Java实现桥接方法isBridge()和合成方法isSynthetic(),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

今天在看spring的时候看到这样一段代码

public abstract class ReflectionUtils {
    public static final MethodFilter USER_DECLARED_METHODS =
          (method -> !method.isBridge() && !method.isSynthetic());
}    

其中 isBridge() 和 isSynthetic() 分别用来判断方法是否为桥接方法和合成方法,那么接下来我们就看下他俩到底有什么作用?

1.桥接方法

桥接方法是在jdk5引入泛型后,为了使泛型方法生成的字节码和之前的版本相兼容,而由编译器自动生成的方法。

编译器是在什么时候会生成桥接方法呢?这个在官方的JLS中也有说明,可以具体看下。

当子类在继承(或实现)一个带有泛型的父类(或接口)时,在子类中明确指定了泛型,此时编译器在编译时就会自动生成桥接方法。

1.1 从字节码看桥接方法

我们通过一段代码来看下:

//接口
public interface Action<T> {
    T play(T action);
}
//实现类
public class Children implements Action<String> {
    @Override
    public String play(String action) {
        return "play basketball.....";
    }
}

我们将实现类Children编译看下字节码:

Compiled from "Children.java"
public class com.qiuguan.juc.bridge.Children extends java.lang.Object implements com.qiuguan.juc.bridge.Action<java.lang.String>
{
  public com.qiuguan.juc.bridge.Children();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/qiuguan/juc/bridge/Children;
  public java.lang.String play(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: ldc           #2                  // String play basketball.....
         2: areturn
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Lcom/qiuguan/juc/bridge/Children;
            0       3     1 action   Ljava/lang/String;
  //这个方法我们并没有定义,这个就是编译器自动生成的桥接方法
  public java.lang.Object play(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class java/lang/String
         5: invokevirtual #4                  // Method play:(Ljava/lang/String;)Ljava/lang/String;
         8: areturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/qiuguan/juc/bridge/Children;
}
Signature: #21                          // Ljava/lang/Object;Lcom/qiuguan/juc/bridge/Action<Ljava/lang/String;>;
SourceFile: "Children.java"

从字节码中可以看到,一共有3个方法,第一个是无参构造器,第二个是我们实现了接口的方法,而第三个就是编译器生成的桥接方法,单独看下这个桥接方法:

public java.lang.Object play(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
    //ACC_BRIDGE: 桥接方法的标识
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class java/lang/String
         5: invokevirtual #4                  // Method play:(Ljava/lang/String;)Ljava/lang/String;
         8: areturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/qiuguan/juc/bridge/Children;

可以看到它含有一个 ACC_BRIDGE 的标识,表明他是一个桥接方法,而且他的返回值类型和参数类型都是java.lang.Object,从字节码中的第9行可以看到,它会将Object转成String类型,然后再调用Children类中声明的方法。转换一下就是

public Object play(Object object) {
    return this.play((String)object);
}

所以说,桥接方法实际上调用了具体泛型的方法,看下下面的这段代码:

public class Test {
    public static void main(String[] args) {
        //接口不指定泛型
        Action children = new Children();
        System.out.println(children.play("basketball"));
        System.out.println(children.play(new Object()));
    }
}

父接口不指定泛型,那么在方法调用时就可以传任何参数,因为Action接口的方法参数实际上是Object类型,此时我传String或者Object都可以,都不会报错。在运行时参数类型不是Children声明的类型时,才会抛出类型转换异常,上面的代码输出就是这样:

play basketball.....
Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String
    at com.qiuguan.juc.bridge.Children.play(Children.java:7)
    at com.qiuguan.juc.bridge.Test.main(Test.java:21)

如果我们再声明 Action接口时指定泛型,比如:

Action<String> children = new Children();

当然这里只能是String类型,因为Children类的泛型类型就是String,如果指定其他类型,那么在编译时就会报错,这样就把类型检查从运行时提前到了编译时,这就是泛型的好处。

1.2 从反射看桥接方法

还是使用上面的例子,我们通过反射来看下:

public class Test {
    public static void main(String[] args) {
        Method[] declaredMethods = Children.class.getDeclaredMethods();
        for (Method m : declaredMethods) {
            System.out.printf("methodName = %s , paramType = %s, returnType = %s, isBridge() = %s\n", m.getName(), Arrays.toString(m.getParameterTypes()), m.getReturnType(), m.isBridge());
        }
    }
}

我们看下运行结果:

methodName = play , paramType = [class java.lang.String], returnType = class java.lang.String, isBridge() = false
methodName = play , paramType = [class java.lang.Object], returnType = class java.lang.Object, isBridge() = true

不难发现,它确实存在两个play方法,其中第二个就是编译器生成的桥接方法。

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

前面我们有说到 当子类在继承(或实现)一个带有泛型的父类(或接口)时,在子类中明确指定了泛型,此时编译器在编译时就会自动生成桥接方法,其实说白了就是和泛型有关。我们知道泛型是JDK5引入了,在JDK5之前,声明一个容器,我们一般会这样:

List list = new ArrayList<>();
list.add("abc");
list.add(123);
list.add(new Object());
list.add(0.3f);

往list容器中可以添加任何类型的对象,当从容器中取数据时,由于不确定类型,所以需要我们手动的去判断所需要的具体类型,在JDK5引入泛型后,我们就可以约定容器只能放什么类型的数据了:

List<String> list = new ArrayList();
list.add("abc");

这样就不用担心类型的问题了。但是泛型是在JDK5引入的,为了向下兼容,引入了泛型擦除的机制,在编译时将泛型去掉,变成Object类型。也正是由于泛型擦除的特性,如果不生成桥接方法,那么就与之前的字节码存在兼容性的问题了。

我们在回过头来看下前面的Aicton接口的字节码

Compiled from "Action.java"
public interface com.qiuguan.juc.bridge.Action<T extends java.lang.Object>
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
   #1 = Class              #10            // com/qiuguan/juc/bridge/Action
   #2 = Class              #11            // java/lang/Object
   #3 = Utf8               play
   #4 = Utf8               (Ljava/lang/Object;)Ljava/lang/Object;
   #5 = Utf8               Signature
   #6 = Utf8               (TT;)TT;
   #7 = Utf8               <T:Ljava/lang/Object;>Ljava/lang/Object;
   #8 = Utf8               SourceFile
   #9 = Utf8               Action.java
  #10 = Utf8               com/qiuguan/juc/bridge/Action
  #11 = Utf8               java/lang/Object
{
  public abstract T play(T);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_ABSTRACT
    Signature: #6                           // (TT;)TT;
}
Signature: #7                           // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "Action.java"

通过 “Signature: #6” 和 “Signature: #7”  可以看到,在编译完成后实际上就变成了Object类型了
java复制代码public abstract Object play(Object action) 

而Children实现了这个接口,如果不生成桥接方法,那么Children就没有实现接口中定义的这个方法,语义就不正确了,所以编译器才会自动生成桥接方法,来保证兼容性。

2.合成方法

我们还是通过例子来看什么是合成方法?,以及什么条件下会生成合成方法?

public class Animal {
    public static void main(String[] args) {
        Animal.Dog dog = new Animal.Dog();
        //外部类访问内部类的私有属性
        System.out.println(dog.name);
    }
    //内部类
    private static class Dog {
        private String name = "旺财";
    }
}

我们将上面的代码编译一下,可以看到有3个文件

Animal$1.class  // ?
Animal$Dog.class   //内部类
Animal.class  //外部类

其中第一个类是做什么的?我们并没有定义过,为什么会产生呢?先带着疑问往下看,我们先看下内部类的反编译结果:

可以使用在线反编译工具,或者用 javap -c Animal\$Dog.class 指令

import com.qiuguan.juc.bridge.Animal.1;
class Animal$Dog {
   private String name;
   private Animal$Dog() {
      this.name = "旺财";
   }
   //这是一个合成的构造器
   // $FF: synthetic method
   Animal$Dog(1 x0) {
      this();
   }
   //这里生成了一个 access$100的方法,这个是什么?
   // $FF: synthetic method
   static String access$100(Animal$Dog x0) {
      return x0.name;
   }
}

反编译后,我们看到它生成了 access$100的方法,这个方法是干什么的?我们并没有定义呀,为何会生成呢?我们还是继续往下看:
在我上面举的例子中,name是内部类Dog的私有属性,但是外部类却直接引用了这个属性,从语法结构上好像没有什么问题,但是从编译器的角度看,这就有点麻烦了,实际上外部类和内部类是平等的,就完全是两个独立的类,这种情况下,外部类直接引用内部类的私有属性,就有点为违背了封装原则。
于是,编译器就要做些什么,我们把外部类反编译也看下

javap -c Animal.class

Compiled from "Animal.java"
public class com.qiuguan.juc.bridge.Animal {
  public com.qiuguan.juc.bridge.Animal();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/qiuguan/juc/bridge/Animal$Dog
       3: dup
       4: aconst_null
       5: invokespecial #3                  // Method com/qiuguan/juc/bridge/Animal$Dog."<init>":(Lcom/qiuguan/juc/bridge/Animal$1;)V
       8: astore_1
       9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: aload_1
      //重点看这里。。。。。。
      13: invokestatic  #5                  // Method com/qiuguan/juc/bridge/Animal$Dog.access$100:(Lcom/qiuguan/juc/bridge/Animal$Dog;)Ljava/lang/String;
      16: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      19: return
}

重点看第19行的指令,这里在源码中就是输出内部类的name属性,但是从字节码中我们可以看到,它实际上调用了内部类的 access$100方法,这个方法是不是比较熟悉了,上面我们刚看到的,这个方法是一个静态方法,它返回的就是内部类的私有属性name。
现在知道外部类访问内部类的私有属性,编译器为我们做了什么了,接下来我们再继续回过头来看下,编译后生成的第三个类 Animal\$1.class

//看着就是一个普通的类,不过他是编译器生成的合成类。
// $FF: synthetic class
class Animal$1 {
}

这个类看起来就像是一个普通的类,只不过他是编译器生成的一个合成类。

说白了,synthetic 就是突破限制继而能够访问一些private的字段。尤其在这种内部类的情况。

再举一个在日常开发中也比较的枚举

public enum ColorEnum {
    RED,BLACK,GREEN,BLUE;
    public ColorEnum getColorEnum(String name){
        ColorEnum[] values = ColorEnum.values();
        for (ColorEnum value : values) {
            if (value.name().equals(name)) {
                return value;
            }
        }
        return ColorEnum.RED;
    }
}

借助在线工具反编译后看下:

public enum ColorEnum {
   RED,
   BLACK,
   GREEN,
   BLUE;
   // $FF: synthetic field
   private static final ColorEnum[] $VALUES = new ColorEnum[]{RED, BLACK, GREEN, BLUE};
   public ColorEnum getColorEnum(String name) {
      ColorEnum[] values = values();
      ColorEnum[] var3 = values;
      int var4 = values.length;
      for(int var5 = 0; var5 < var4; ++var5) {
         ColorEnum value = var3[var5];
         if(value.name().equals(name)) {
            return value;
         }
      }
      return RED;
   }
}

可以看到,它内部会生成一个合成属性 $VALUES。

到此这篇关于Java实现桥接方法isBridge()和合成方法isSynthetic()的文章就介绍到这了,更多相关Java isBridge() isSynthetic()内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文