Java、静态方法绑定和泛型都与一些方法重载结合在一起
正如标题所暗示的,我的问题有点奇怪和复杂。我知道我要做的事情违反了“良好”编程实践的所有规则,但是嘿,如果我们不活一点,生活会怎样呢?
所以我所做的是创建以下程序。 (请注意,这是一个更大的实验的一部分,旨在真正尝试和理解泛型,因此某些函数名称可能有点乱)
import java.util.*;
public class GenericTestsClean
{
public static void test2()
{
BigCage<Animal> animalCage=new BigCage<Animal>();
BigCage<Dog> dogCage=new BigCage<Dog>();
dogCage.add(new Dog());
animalCage.add(new Cat());
animalCage.add(new Dog());
animalCage.printList(dogCage);
animalCage.printList(animalCage);
}
public static void main(String [] args)
{
//What will this print
System.out.println("\nTest 2");
test2();
}
}
class BigCage<T> extends Cage<T>
{
public static <U extends Dog> void printList(List<U> list)
{
System.out.println("*************"+list.getClass().toString());
for(Object obj : list)
System.out.println("BigCage: "+obj.getClass().toString());
}
}
class Cage<T> extends ArrayList<T>
{
public static void printList(List<?> list)
{
System.out.println("*************"+list.getClass().toString());
for(Object obj : list)
System.out.println("Cage: "+obj.getClass().toString());
}
}
class Animal
{
}
class Dog extends Animal
{
}
class Cat extends Animal
{
}
现在让我困惑的是,它可以与 javac 1.6.0_26 一起编译良好但是当我运行它时,我得到以下类强制转换异常:
Test 2
*************class BigCage
BigCage: class Dog
*************class BigCage
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Dog
at BigCage.printList(GenericTestsClean.java:31)
at GenericTestsClean.test2(GenericTestsClean.java:13)
at GenericTestsClean.main(GenericTestsClean.java:21)
这里需要注意的一些事情:
- 两个 printList NOT 重写,但按预期互相重载(它们具有不同的类型,因为通用类型他们的论点是 不同的)。这可以通过使用 @Override 注释来验证。
- 将 class Cage 中的
void printList(List)
方法更改为非静态会生成适当的编译时错误 - 更改方法
void class BigCage 中的 printList(List)
为void printList(List)
生成适当的错误。 - 通过 类 BigCage 调用 printList() (即 BigCage.printList(...))会生成相同的运行时错误
- 在 main()中 main() 通过class Cage调用printList()(即Cage.printList(...))按预期工作,仅调用版本printList 中Cage
- 如果我将
printList(List)
的定义从 class Cage 复制到 class BigCage,这将隐藏 class Cage 中的定义,我得到相应的编译器错误
现在,如果我不得不在黑暗中尝试一下这里发生的事情,我会说编译器搞砸了因为它分多个阶段工作:类型检查和重载方法解析。在类型检查阶段,我们解决了有问题的行,因为 class BigCage 从 class Cage
继承了 void printList(List)
,这将匹配我们扔给它的任何旧列表,所以确保我们有一个可以工作的方法。然而,一旦需要解决实际调用的方法,我们就会遇到一个问题,因为类型擦除会导致 BigCage.printList
和 Cage.printList
具有完全相同的签名。这意味着当编译器寻找 animalCage.printList(animalCage);
的匹配时,它将选择它匹配的第一个方法(如果我们假设它从 BigCage 的底部开始并工作,那么它的原因是Object) 它会发现 void ; printList(List)
首先而不是正确的匹配 void printList(List)
现在我真正的问题:与真相是我在这里吗?这是一个已知的错误吗?这是一个错误吗?我知道如何解决这个问题,这更多的是一个学术问题。
**编辑**
正如下面很少有人发布的那样,这段代码可以在 Eclipse 中运行。 我的具体问题涉及 javac 版本 1.6.0_26。还有,我不是 确定在这种情况下我是否完全同意 Eclipse,即使它 有效,因为将
printList(List)
添加到 BigCage 将 导致 Eclipse 中出现编译时错误,我不明白原因 当相同的方法被继承而不是手动时它应该起作用 添加(请参阅上面的注释 6)。
So as the title implies my question is a bit odd and complicated. I know what I'm about to do breaks all the rules of "good" programming practices but hey, what's life if we don't live a little?
So what I did was create the following program. (Note this was part of a larger experiment to really try and understand generics so some of the function names maybe a bit out of order)
import java.util.*;
public class GenericTestsClean
{
public static void test2()
{
BigCage<Animal> animalCage=new BigCage<Animal>();
BigCage<Dog> dogCage=new BigCage<Dog>();
dogCage.add(new Dog());
animalCage.add(new Cat());
animalCage.add(new Dog());
animalCage.printList(dogCage);
animalCage.printList(animalCage);
}
public static void main(String [] args)
{
//What will this print
System.out.println("\nTest 2");
test2();
}
}
class BigCage<T> extends Cage<T>
{
public static <U extends Dog> void printList(List<U> list)
{
System.out.println("*************"+list.getClass().toString());
for(Object obj : list)
System.out.println("BigCage: "+obj.getClass().toString());
}
}
class Cage<T> extends ArrayList<T>
{
public static void printList(List<?> list)
{
System.out.println("*************"+list.getClass().toString());
for(Object obj : list)
System.out.println("Cage: "+obj.getClass().toString());
}
}
class Animal
{
}
class Dog extends Animal
{
}
class Cat extends Animal
{
}
Now what is confusing me is that this compiles fine with javac 1.6.0_26 but when I run it I get the following class cast exception:
Test 2
*************class BigCage
BigCage: class Dog
*************class BigCage
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Dog
at BigCage.printList(GenericTestsClean.java:31)
at GenericTestsClean.test2(GenericTestsClean.java:13)
at GenericTestsClean.main(GenericTestsClean.java:21)
A number of things to note here:
- The two printList are NOT overriding but overloading each other as expected(They have different types because the generic types of their arguments are different). This can be verified by using the @Override annotation
- Changing the
void printList(List<?>)
method in class Cage to be non-static generates an appropriate compile time error - Changing the method
void <U extends Dog> printList(List<U>)
in class BigCage tovoid <U> printList(List<U>)
generates an appropriate error. - In main() calling printList() through the class BigCage (ie BigCage.printList(...)) generates the same runtime error
- In main() calling printList() through the class Cage (ie Cage.printList(...)) works as expected only calling the version of printList in Cage
- If I copy the definition for
printList(List<?>)
to class BigCage from class Cage, which will hide the definition in class Cage, I get the appropriate compiler error
Now if I had to take a shot in the dark as to what is going on here, I'd say the compiler is screwing up because it's working in multiple phases: Type Checking and Overloaded Method Resolution. During the type checking phase we get through the offending line because class BigCage inherited void printList(List<?>)
from class Cage
which will match any old List we throw at it, so sure we have a method that will work. However once it comes time to resolve with method to actually call we have a problem due to Type Erasure which causes both BigCage.printList
and Cage.printList
to have the exact same signature. This means when compiler is looking for a match for animalCage.printList(animalCage);
it will choose the first method it matches (and if we assume it starts at the bottom with BigCage and works its why up to Object) it'll find void <U extends Dog> printList(List<U>)
first instead of the correct match void printList(List<?>)
Now for my real question: How close to the truth am I here? Is this a known bug? Is this a bug at all? I know how to get around this problem, this is more of an academic question.
**EDIT**
As few people have posted below, this code will work in Eclipse.
My specific question deals with javac version 1.6.0_26. Also, I'm not
sure if I completely agree with Eclipse in this case, even though it
works, because adding aprintList(List<?>)
to BigCage will
result in a compile time error in Eclipse and I can't see reason why
it should work when the same method is inherited verses manually
added (See Note 6 above).
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
考虑这个小问题:
假设我们从
B
中删除foo
方法,并且只重新编译B
本身,那么当我们运行时会发生什么>测试()
?它是否应该因为找不到B.foo()
而抛出链接错误?根据 JLS3 #13.4.12,删除
B.foo
不会破坏二进制兼容性,因为A.foo
仍然被定义。这意味着,当执行B.foo()
时,会调用A.foo()
。请记住,没有重新编译test()
,因此此转发必须由 JVM 处理。相反,让我们从
B
中删除foo
方法,然后重新编译所有方法。即使编译器静态地知道B.foo()
实际上意味着A.foo()
,它仍然会在字节码。目前,JVM 会将B.foo()
转发到A.foo()
。但是,如果将来B
获得新的foo
方法,即使test()
没有重新编译,该新方法也会在运行时被调用。从这个意义上说,静态方法之间存在着压倒一切的关系。当compile看到
B.foo()
时,它必须将其编译为字节码形式的B.foo()
,无论B
是否有今天>foo()
。在您的示例中,当编译器看到
BigCage.printList(animalCage)
时,它会正确推断它实际上正在调用Cage.printList(List)
。因此它需要将调用编译为字节码BigCage.printList(List)
- 目标类必须是BigCage
这里而不是Cage.
哎呀!字节码格式尚未升级以处理此类方法签名。泛型信息作为辅助信息保留在字节码中,但对于方法调用,这是旧的方式。
擦除发生。该调用实际上被编译到 BigCage.printList(List) 中。太糟糕了,
BigCage
在擦除后还有一个printList(List)
。在运行时,该方法被调用!这个问题是由于Java规范和JVM规范不匹配造成的。
Java 7 稍微收紧了;意识到字节码和 JVM 无法处理这种情况,它不再编译你的代码:
另一个有趣的事实:如果这两个方法具有不同的返回类型,您的程序将正常工作。这是因为在字节码中,方法签名包括返回类型。因此
Dog printList(List)
和Object printList(List)
之间不会产生混淆。另请参阅 Java 中的类型擦除和重载:为什么这有效? 这个技巧只在 Java 6 中允许。Java 7 禁止它,可能是出于技术原因以外的原因。Consider this trivial problem:
Suppose we remove the
foo
method fromB
, and we only recompileB
itself, what could happen when we runtest()
? Should it throw linkage error becauseB.foo()
is no found?According to JLS3 #13.4.12, removing
B.foo
doesn't break binary compatibility, becauseA.foo
is still defined. This means, whenB.foo()
is executed,A.foo()
is invoked. Remember, there's no recompilation oftest()
, so this forwarding must be handled by JVM.Conversely, let's remove
foo
method fromB
, and recompile all. Even though compiler knows statically thatB.foo()
actually meansA.foo()
, it still generateB.foo()
in the bytecode. For now, JVM will forwardB.foo()
toA.foo()
. But if in futureB
gains a newfoo
method, the new method will be invoked at runtime, even iftest()
isn't recompiled.In this sense, there is a overriding relation among static methods. When compile sees
B.foo()
, it must compile it toB.foo()
in bytecode, regardless whetherB
has afoo()
today.In your example, when compiler sees
BigCage.printList(animalCage)
, it correctly infer that it's actually callingCage.printList(List<?>)
. So it needs to compile the call into bytecode asBigCage.printList(List<?>)
- the target class must beBigCage
here instead ofCage
.Oops! Bytecode format hasn't been upgrade to handle method signature like that. Generics information are preserved in bytecode as auxilary information, but for method invocation, it's the old way.
Erasure happens. The call is actually compiled into
BigCage.printList(List)
. Too badBigCage
also has aprintList(List)
after erasure. At runtime, that method is invoked!This problem is due to the mismatch between Java spec and JVM spec.
Java 7 tightens up a little; realizing bytecode and JVM can't handle such situations, it no longer compiles your code:
Another fun fact: if the two methods have different return types, your program will work correctly. This is because in byte code, method signature includes return type. So there is no confusion between
Dog printList(List)
andObject printList(List)
. See also Type Erasure and Overloading in Java: Why does this work? This trick is only allowed in Java 6. Java 7 forbids it, probably for reasons other than technical ones.这不是一个错误。该方法是静态的。您不能覆盖静态方法,只能隐藏它们。
当您在 bigCage 上调用“
printList
”时,您实际上是在 BigCage 类上调用printList
,而不是对象,该对象将始终调用 BigCage 类中声明的静态方法。This is not a bug. The method is static. You cannot override static methods, you only hide them.
When you call "
printList
" on bigCage, you really are callingprintList
on BigCage class and not the object, which will always call your static method declared in BigCage class.这是此代码的最简单版本,具有相同的问题:
我认为编译器应该返回错误:(
或关于名称与相同错误发生冲突的内容),但事实并非如此。
反汇编后(javap -c GenericTestsClean)我们得到:
Calling
java GenericTestsClean
:javac 1.6.0_10 version
Eclipse compiller version
恕我直言,这个结果都是不正确的。
This is simplest version of this code with the same problem:
I think that compiller should return error:
(or sth about name clash with the same errasure) but it doesn't.
After dissasembling (javap -c GenericTestsClean) we got:
Calling
java GenericTestsClean
:javac 1.6.0_10 version
Eclipse compiller version
IMHO this results are both incorrect.
恕我直言,此代码可能不正确。 类 BigCage 中的方法 printList 应该会导致名称冲突,因为 Cage 中的 printList 具有相同的擦除,但两者都不会覆盖另一个。编译器编译它很奇怪:)
生成的字节码(javac 1.6.0_10)相当于this:
循环中的强制转换导致异常。
Eclipse 内置编译器生成这样的代码(无例外地工作):
或者源代码没问题,但编译器创建了错误的字节码?
事实上,我们调用方法
void printList(List list)
带有参数BigCage; AnimalCage
和 Animal 不扩展 Dog。IMHO this code may be incorrect. Method printList in class BigCage should cause name clash coz printList in Cage have the same erasure, yet neither overrides the other. Strange that compiller compiles it :)
The resulting bytecode (javac 1.6.0_10) is equivalent to this:
The cast in loop causes exception.
Eclipse built-in compiller generates such code (wich works without exception):
Or maybe source is OK, but compiler is creating bad bytecode?
The fact is that we call method
<U extends Dog> void printList(List<U> list)
with parameterBigCage<Animal> animalCage
and Animal does not extend Dog.