java 的桥方法诞生的原因该如何理解?类型擦除后继承的额外方法调用为什么会报错?

java 里面,编译器不认识泛型,在 java 代码编译的时候,会进行类型擦除。

// 泛型类
public class A<T> {
    
    public void test(T val)
    {
        System.out.println("class A " + val);
    }
}

// 编译阶段,类型擦除后
public class A {
    public void test(Object val)
    {
        System.out.println("class A " + val);
    }
}

然后如果有一个类继承了上述泛型类:

public class B extends A<String> {
    public void test(String val)
    {
        System.out.println("class B" + val);
    }
}

// 类型擦除后
// 从以下代码可以看出
// B 实际还继承了另外一个方法
// test(Object val)
public class B extends A {
    public void test(String val)
    {
        System.out.println("class B" + val);
    }
}

然后疑问就发生在下面的代码中:

B b = new B();
A<String> a = b;

// a 变量引用的是 b实例
// 所以 调用 test 方法的时候
// 正常理解也应该调用 b 实例的 test 方法?
// 然而编译器在处理的时候却莫名其妙的
// 会在 B 类中产生一个桥方法(public void test(Object val) {this.test((String) val)})
// 然后实际调用的不是 B.test(String val)
// 而是上述桥方法
// 这是为什么呢?
a.test("one");

// 然后另外一个问题是
// 从上面可知 B 实际上继承了另外一个方法 A.test(Object val)
// 这是类型擦除后继承的
// 但是 下面的代码调用 却会报错!这是为什么?
a.test(20);

请耐心看上面代码注释中的问题描述 和 疑问,谢谢。核心问题有两个:

  • 桥方法诞生的原因是啥(请结合上述例子做解释,谢谢)
  • 类型擦除后继承的额外方法(test(Object val))为什么调用后会报错?(请结合上述例子做解释,谢谢)
阅读 4k
4 个回答

感谢 @huisexiaochou @Richard_Yi 两位回答,结合我参考的 Java中的类型擦除和桥方法 这篇文章,我终于理解了桥方法的诞生的原因 和 其他一些列泛型的疑问:

首先 java 中编译器是不认识泛型的,所以,泛型类 或 泛型方法在编译阶段会做类型擦除。非限定类型变量会用 Object 替换。限定类型变量用 第一个限定类型替换。

如下:

非限定类型变量类型擦除

class A<T> {
    public T test(T val)
    {
        return val;
    }
}

// 类型擦除后
class A {
    public Object test(Object val)
    {
        return val;
    }
}

限定类型变量类型擦除

class B<T extends Comparable> {
    public T test(T val)
    {
        return val;
    }
}

// 类型擦除后
class B {
    public Comparable test(Comparable val)
    {
        return val;
    }
}

有了类型擦除后,再来看看普通类的多态会发生什么现象。

class A {
    public void test()
    {
        System.out.println("A::test");
    }
    
    public void test(Object val)
    {
        System.out.println("A::test Object");
    }
}

class B extends A {
    public void test()
    {
        System.out.println("B::test");
    }
    
    public void test(String val)
    {
        System.out.println("B::test String");
    }
}

B b = new B();
A a = b;

// B::test
a.test();

// A::test Object
a.test("string");

从以上例子可以看出,多态中,当子类重写了超类方法时,则调用子类方法,否则都是调用超类方法!

现在我们再来看看加入了泛型类之后的结果是怎样的?

class A<T>
{
    public T test(T val)
    {
        return val;
    }
}

// B extends A<String>
// 这句实际上是告诉我们这边请将 A 泛型类的类型变量当成是 String 来看
// 注意,这只是对我们开发人员的一种提示
// 并非 java 语言的实际处理方式
class B extends A<String>
{

    // 然后这个地方假设将 A 中的类型变量 T 看成是 String 的话
    // 很显然这个方法就是对 A 类 test 方法的重写
    // (以上两句话,注意仅是假设!!)
    public String test(String val)
    {
        return val;
    }
}

// A 类型擦除后
class A {
    public Object test(Object val)
    {
        return val;
    }
}

// B 类型擦除后
class B extends A {
    public String test(String val)
    {
        return val;
    }
}

// 看类型擦除后,B 类实际上继承的是 
// test(Object) 方法
// 然后非常显然的是 B 类的 test(String)
// 方法没有如期望的那般重写 A 类的同名方法

B b = new B();
// 这边还需要一个 jvm 翻译泛型表达式的知识基础
// 不然可能会无法理解下面两句代码的差别 
// A<String> a = b; String res = a.test("running");
// A a = b; Object res = a.test("running");
// 我这边简单说下:
// 泛型表达式本质上是对编译器的一些命令
// 如这边既然用了 A<String> 也就是说
// 编译器会将 A 中的类型变量当成是 String 来看待并再次基础上进行类型检查和结果处理(注意仅仅是看待,并不是实质上的替换!)
// 实际表现就是在编译阶段就能够发现各种语法上的错误
A<String> a = b;

// 然后令人纠结的地方就在这儿了!
// 我们显然是期望调用的是 B::test
// 因为我们在声明 B 的时候继承的是 A<String>
// 所以我们普遍认为 B 类的 test(String) 方法已经对 
// A 类的 test(T) 方法进行了重写
// 但是由于 A 类比较特殊,是泛型类
// 泛型类存在类型擦除的情况
// 结果导致的就是,A 类实际上仅有 test(Object) 方法
// 所以 B 类的 test(String) 方法实际上并没有重写 A 类的 test(Object) 方法
// 故而按照多态的表现,这边仍然会调用 A 类的 test(Object) 方法
// 很显然这并不是我们希望调用的!!。
// 这就是 类型擦除 和 多态产生的冲突!!
// 这个冲突导致的现象是不合理的,所以我们要修复它。
// java` 语言的开发者们想到的办法是
// 通过在 B 类中合成一个 桥方法 来解决这个冲突
// 桥方法是对 A 类 test(Object) 方法的覆写
// 并且函数体调用的是 B 类的 test(String) 方法
// 具体形如:Object test(Object val) {return this.test((String) val);}
// 通过桥方法,我们看到 现在的调用实际就是我们期望看到的调用。
a.test("running");

// 另外一些奇葩的调用方式如下
// 首先这调用的是桥方法
// 其次桥方法中有这样一段代码 test(Object val) {this.test((String) val);}
// 其中 (String) val 这一段强制类型转换就会导致下面代码执行失败
// 因为 byte/short/int/long/float/double 等是无法转换成 String 类型的
a.test(20);
  1. https://blog.csdn.net/mhmyqn/...
  2. 因为是运行时类型擦除,强调的是运行时这三个字。a的类型是A<String>,编译器检查的时候会检查。你上述的情况20是int,而不是String,编译器当然会编译不通过。

1.产生桥方法的原因是因为需要动态分发

A<String> a = b;
a.test("one"); //invokevirtual test(Object)  意味着B中必须有一个test(Object)

2.A a = new B(); a.test(20);可以避开编译期检查 运行时还是会因为 (String)20 报错

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题