带泛型返回值的方法添参后无法覆写的JLS合规性疑问
问题拆解与JLS层面的解答
咱们一步步对应Java语言规范的条款来解释这几个现象,把核心规则和案例结合起来看:
1. 无参泛型方法覆写合法的原因
先看第一个合法的案例:
public abstract class A { public abstract <X> Supplier<X> getSupplier(); public static class B extends A { @Override public Supplier<String> getSupplier() { return String::new; } } }
根据JLS §8.4.8.1(方法覆写的判定规则),当父类方法是泛型方法时,子类非泛型方法要覆写它,必须满足两个关键条件:
- 子类方法的签名是父类方法签名的擦除结果(§8.4.8.1 条件5c);
- 子类方法的签名是父类方法签名的子签名(§8.4.8.1 条件4)。
这里要注意:方法签名仅包含方法名+参数类型列表,返回类型、泛型类型参数都不算在签名里。父类方法getSupplier()的签名(擦除后)还是getSupplier()(参数列表为空,擦除不影响),子类方法的签名完全匹配这个擦除结果。
同时,子类的返回类型Supplier<String>满足返回类型可替代性(§8.4.8.3):它是父类泛型返回类型Supplier<X>的具体化,编译器会自动生成一个桥接方法来适配泛型类型:
// 编译器自动生成的桥接方法 <X> Supplier<X> getSupplier() { return (Supplier<X>) getSupplier(); }
这个桥接方法才是真正覆写父类泛型方法的逻辑,子类自己的Supplier<String> getSupplier()是被桥接方法调用的具体实现。
2. 带参数泛型方法覆写不合法的原因
再看第二个编译报错的案例:
public abstract class A { public abstract <X> Supplier<X> getSuppliers(Collection<String> strings); public static class B extends A { @Override // 编译报错:方法未覆写父类方法 public Supplier<String> getSuppliers(Collection<String> strings) { return String::new; } } }
问题出在父类方法签名的擦除结果上:根据JLS §4.6(类型擦除规则),父类方法的参数Collection<String>擦除后是Collection,因此父类方法签名的擦除结果是getSuppliers(Collection)。
而子类方法的签名是getSuppliers(Collection<String>),和父类签名的擦除结果不匹配,违反了§8.4.8.1的条件5c(子类非泛型方法的签名必须等于父类泛型方法签名的擦除),因此编译器判定这个方法没有覆写父类方法,@Override注解直接报错。
3. 合法覆写触发ClassCastException的原因
当你执行这段代码时:
A b = new B(); URL url = b.<URL>getSupplier().get(); // 抛出ClassCastException
本质是泛型类型擦除带来的运行时类型不匹配:
- 编译时,编译器根据父类的泛型声明,认为
b.<URL>getSupplier()返回的是Supplier<URL>,因此允许赋值给URL变量; - 运行时,泛型类型被完全擦除,实际调用的是子类的
Supplier<String> getSupplier(),返回的Supplier实例生成的是String对象; - 当把
String对象强制转换为URL时,就会触发ClassCastException——这是因为编译时的泛型检查无法感知子类的具体返回类型,而运行时的类型信息又因擦除丢失,导致类型转换失败。
内容的提问来源于stack exchange,提问作者lexicore

