今天写代码的时候遇到了一个有意思的问题, 感觉可以归于泛型重载冲突, 网上找了几个例子, 但是都不是我遇到的这种.
泛型擦除导致的问题
下面的代码编译会报错:
class Test1 {
public void sum(List<Integer> list) { }
public void sum(List<String> list) { }
}
此时编译器会报错, 因为 Java 的泛型是伪泛型 (c# 是真泛型), 为什么说是伪泛型呢? 因为 Java 泛型只存在于源代码中, 编译后, 泛型信息已经被 "擦除" 了. 编译后类似:
class Test1 {
public void sum(List<E> list) { }
public void sum(List<E> list) { }
}
两个函数具有相同的签名, 当然编译器会拒绝为我们编译这样的代码.
class Test2 {
public Integer sum(List<Integer> list) { }
public String sum(List<String> list) { }
}
这段代码能正常编译吗? 答案是, 这段代码能正常编译 (重载成功了), 而且还能正常执行! JVM 规定函数的返回类型并不参与 "函数特征签名" 的生成, 那为什么能编译成功呢? 是因为在 class 文件中, 只要描述符不完全一致的两个方法就能共存于一个 class 文件中. 字节码中, 特征签名还包括了方法的返回值以及受查异常表, 这就是为什么在 class 文件中, 仅仅返回值不同的两个方法能共存的原因.
协变式覆盖 (override)
在 JDK 1.4 及以前, 子类方法如果要覆盖超类的某个方法, 必须具有完全相同的方法签名, 包括返回值也必须完全一样.
JDK 5 开始, 只要子类方法与超类方法具有相同的方法签名, 或者子类方法的返回值是超类方法的子类型 (增加了对协变返回值的支持), 就可以覆盖. 这样有什么好处呢? 以 Object 类的 clone 方法为例:
class Object {
public Object clone() { }
}
在 5.0 以前, 如果子类需要重载 clone 方法, 必须像下面这样写代码:
class Point {
public int x;
public int y;
public Point(int x, int y) { this.x=x; this.y=y; }
public Object clone() { return new Point(x,y); }
}
虽然在我们的 Point
类里, clone
方法总是返回一个 Point
类型的对象, 但却必须把返回类型写成 Object, 在外部使用 clone 方法时也必须使用恼人的强制类型转换.
在 Java5.0 以后, 我们就可以利用新的覆盖规则, 像下面这样编写代码:
class Point {
public int x;
public int y;
public Point(int x, int y) { this.x=x; this.y=y; }
public Point clone() { return new Point(x,y); }
}
这样, 我们就可以直接使用 Point p2 = p1.clone()
而不用强制类型转换了.
泛型重载 (overload)
Java 的方法重载一般指在同一个类中的两个同名方法, 规则是: 两个方法必须具有不同的方法签名. 因此形式参数必须不相同, 使得编译器能够区分开这两个重载的方法. 由于编译器不能仅仅通过方法的返回值类型来区分重载方法, 所以如果两个方法只有返回类型不同, 其它完全一样, 编译是不能通过的. 泛型, 重载是 java 语言级别的, 但 "擦除" 技术是关于实现的, 它关系到合法 class 文件的生成, 而合法的 class 文件才能被 JVM 接受, JVM 本来就支持签名相同, 但返回类型不同的方法存在.
在 Java 语言角度的添加这种限制也是自然的. 比如两个方法:
void test(int i);
int test(int i);
编译器不能确定到底应该调用哪个方法, 所以这种情况在 Java 中不允许存在.
但是, 对于这两个方法 test(ArrayList<String> list)
和 test(ArrayList<Integer> list)
, 在 Java 语言的级别, 即编译时, 也可以是合法的重载!
因为编译器可以通过参数类型信息来确定调用哪个版本. 再加上返回类型不同, 经过编译和类型擦除得到的两个方法是可以在 class 文件中共存的. 这样问题就解决了.
在泛型方法的重载时, 这个规则变化如下:
class Overloaded {
public static int sum(List<Integer> ints) {
int sum = 0;
for (int i : ints) sum += i;
return sum;
}
public static String sum(List<String> strings) {
StringBuffer sum = new StringBuffer();
for (String s : strings) sum.append(s);
return sum.toString();
}
}
上面是两个泛型方法的重载例子, 由于 Java 的泛型采用擦除法实现, List<Integer>
和 List<String>
在运行时是完全一样的, 都是 List 类型. 也就是, 擦除后的方法签名如下:
int sum(List)
String sum(List)
JVM 允许这两个方法进行重载 (overload), 虽然它们的方法签名相同 (形参), 只有返回值类型不同. 这在两个普通方法的重载中是不允许的.
当然了, 如果两个泛型方法的参数在擦除后相同, 而且返回值类型也完全一样, 那编译肯定是不能通过的.
类似地, 一个类不能同时实现两个具有相同擦除的接口. 如 Class A implements Comparable<Integer>
, Comparable<Long>
.
后续
如果两个泛型方法在擦除泛型信息后, 如果只是具有相同的参数类型, 而返回值不一样, 就可以进行重载.
此类泛型重载在 JDK 1.7 及以上编译时已不允许. JDK7, 8 是不可以编译的, 需要用 JDK6 才可以 (答案中的均使用 oracle jdk 提供的编译器).
首先, 按道理这个本来就应该报错, 从 Java 语言层面来说, 方法重载依赖于相同的方法名, 不同的参数个数, 类型, 顺序, 而 List<Integer>
和 List<String>
类型擦除后都为 List<E>
, 从而不符合方法重载的要求. 但是, 为什么会说这种依赖返回值可以通过甚至正常运行, 原因在于, 编译后的俩个方法在 class 中的 signature 分别为
(Ljava/util/List<Ljava/lang/Integer;>;)I
(Ljava/util/List<Ljava/lang/String;>;)Ljava/lang/String;
它们可以合法的共存在一个 class 文件中.
从 jdk7 开始呢, 编译期做了 check, 保证了 behavior 一致, 所以报错.
JDK-6182950 : methods clash algorithm should not depend on return type
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6182950
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Following code should not compile:class x { int f(List<String> l) {return 0;} double f(List<Integer> l) {return 0;} }
since both method have the same erasure.
JLS3 draft 8.4.8.3 says, it is error if there are two methods m1 and m2 in a class such that:
- m1 and m2 have the same name
- m2 is accessible
- the signature of m1 is not subsignature if m2
- both methods have same erasure
文章评论