今日在看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类型了
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。
好了,关于桥接办法和组成办法就记录到这儿吧,欢迎我们批评指正,♀️♀️♀️
btw: 桥接办法一定是组成办法,但组成办法不一定是桥接办法。