Java泛型中的桥方法(Bridge Method)

前几天在看 Java 泛型的时候,发现了一个有趣的现象。就是在某些情况下,编译器在编译我们的类文件的时候会帮我们自动生成某些方法,称作桥方法。

我们知道 Java 中的泛型在编译为 class 的时候会把泛型擦除,也就是说你写的 <T> 到最后 class 文件中其实都是 Object,看下面代码示例:

public class A<T> {
    private T value;
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
}
// ------------编译后-------------
public class A {
    private Object value;
    public Object getValue() {
        return value;
    }
    public void setValue(Object value) {
        this.value = value;
    }
}

可以看出,Java 中的泛型在编译后都变成了 Object,也可以说 Java 中的泛型其实是编译器为我们做了优化,虚拟机中是没有泛型的。

那我们接着看下面这段代码:

public class B extends A<String> {
    @Override
    public void setValue(String value) {
        System.out.println("---B.setValue()---");
    }
}

我们写了一个 B 类,继承自 A 类,并重写了 setValue 方法。

我们来思考一个问题,按我们上面所说的 Java 泛型的擦除机制,实际 A 类中 setValue 方法应该是这样的:

// A 类中的 setValue 方法
public void setValue(Object value){
    this.value = value;
}

这个时候问题出来了,我们发现 B 类中的 setValue 方法参数与 A 类中的 setValue 方法参数不一样。按照 Java 重写方法的规则,B 类中的 setValue 方法实际上并没有重写父类中的方法,而是重载。

所以实际上 B 类中应该是有两个 setValue 方法,一个自己的,一个继承来的:

// 自己的
public void setValue(String value){...}
// 从父类继承的
public void setValue(Object value){...}

所以在某些场景,比如反射调用 B 类中的方法的时候,就有可能会调用到从父类继承的那个 setValue 方法。

这个时候就会出现与我们意愿不一致的结果了,违反了我们重写方法的意愿了。

当然,这种情况是不会出现的,因为 Java 编译器帮我们处理了这种情况。我们来查看 B.class 字节码文件:

// class version 52.0 (52)
// access flags 0x21
// signature LA<Ljava/lang/String;>;
// declaration: B extends A<java.lang.String>
public class B extends A  {
  // compiled from: B.java
  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 4 L0
    ALOAD 0
    INVOKESPECIAL A.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LB; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public setValue(Ljava/lang/String;)V
   L0
    LINENUMBER 8 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "---B.setValue()---"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 9 L1
    RETURN
   L2
    LOCALVARIABLE this LB; L0 L2 0
    LOCALVARIABLE value Ljava/lang/String; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1041
  public synthetic bridge setValue(Ljava/lang/Object;)V
   L0
    LINENUMBER 4 L0
    ALOAD 0
    ALOAD 1
    CHECKCAST java/lang/String
    INVOKEVIRTUAL B.setValue (Ljava/lang/String;)V
    RETURN
   L1
    LOCALVARIABLE this LB; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 2
}

我们看到 B 类中有两个 setValue 方法,一个参数为 String 类型,一个参数为 Object 类型,参数为 Object 类型的就是 Java 编译器帮我们生成的桥方法,实际代码如下:

public void setValue(String value){...}
public void setValue(Object value){
    setValue((String)value);
}

桥方法内部其实就是调用了我们自己的 setValue 方法,这样就避免了在重写的时候我们还能调用到父类的方法。

问题还没有完,我们接着看:

public class B extends A<String> {
    @Override
    public String getValue() {
        return super.getValue();
    }
}

B 类重写了 A 类中的 getValue 方法。按照泛型的擦除,父类中的 getValue 方法返回值其实是 Object。

所以其实编译器也帮我们生成了桥方法,这里就不贴字节码文件了,大家可以自己查看。编译后的 B 类其实是这样:

public class B extends A {
    // 自己定义的方法
    public String getValue(){...}
    // 编译器生成的桥方法
    public Object getValue(){
        return getValue();
    }
}

这个时候我们发现 B 类有点颠覆我们的常识了,难道一个类中允许出现方法签名相同的多个方法?

  • 方法签名确实是方法名+参数列表
  • 我们也不能在同一个类中写两个方法签名相同的方法
  • JVM 会用方法名、参数类型和返回类型来确定一个方法,所以针对方法签名相同的两个方法,返回值类型不相同的时候,JVM是能分辨的
发布了8 篇原创文章 · 获赞 7 · 访问量 713

猜你喜欢

转载自blog.csdn.net/hao_yan_bing/article/details/89447792