Java、静态方法绑定和泛型都包含一些方法重载

Posted

技术标签:

【中文标题】Java、静态方法绑定和泛型都包含一些方法重载【英文标题】:Java, Static Method Binding and Generics all rolled up with some Method Overloading 【发布时间】:2011-10-02 09:16:53 【问题描述】:

所以正如标题所暗示的那样,我的问题有点奇怪和复杂。我知道我将要做的事情打破了“良好”编程实践的所有规则,但是嘿,如果我们不活得一点点,生活会怎样?

所以我所做的是创建以下程序。 (请注意,这是真正尝试理解泛型的大型实验的一部分,因此某些函数名称可能有点乱序)

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 不是覆盖,而是按预期相互重载(它们具有不同的类型,因为它们的参数的泛型类型不同)。这可以通过使用@Override 注释来验证 将 class Cage 中的 void printList(List&lt;?&gt;) 方法更改为非静态会产生相应的编译时错误 将 class BigCage 中的方法 void &lt;U extends Dog&gt; printList(List&lt;U&gt;) 更改为 void &lt;U&gt; printList(List&lt;U&gt;) 会产生相应的错误。 在 ma​​in() 中通过 class BigCage 调用 printList()(即 BigCage.printList(...))生成相同的运行时错误 在 ma​​in() 中,通过 class Cage 调用 printList()(即 Cage.printList(...))按预期工作只调用Cage中的printList版本 如果我将 printList(List&lt;?&gt;) 的定义从 class Cage 复制到 class BigCage,这将隐藏 class Cage 中的定义,我得到相应的编译器错误

现在,如果我不得不在黑暗中试一试这里发生了什么,我会说编译器搞砸了,因为它在多个阶段工作:类型检查重载方法解析。在类型检查阶段,我们通过了违规行,因为 class BigCageclass Cage 继承了 void printList(List&lt;?&gt;),这将匹配我们扔给它的任何旧 List,因此请确保我们有一个可以工作的方法。但是,一旦需要使用实际调用的方法来解决问题,我们就会因为类型擦除而出现问题,这会导致 BigCage.printListCage.printList 具有完全相同的签名。这意味着当编译器正在寻找 animalCage.printList(animalCage); 的匹配项时,它将选择它匹配的第一个方法(如果我们假设它从 BigCage 的底部开始并将其原因直到 Object),它会首先找到 void &lt;U extends Dog&gt; printList(List&lt;U&gt;)正确匹配的void printList(List&lt;?&gt;)

现在是我真正的问题:我离真相有多近?这是一个已知的错误?这是一个错误吗?我知道如何解决这个问题,这更像是一个学术问题。

**编辑**

正如下面很少有人发布的那样,此代码将在 Eclipse 中运行。 我的具体问题涉及 javac 版本 1.6.0_26。还有,我不是 确定在这种情况下我是否完全同意 Eclipse,即使它 有效,因为将 printList(List&lt;?&gt;) 添加到 BigCage 将 导致 Eclipse 中出现编译时错误,我看不出原因 当手动继承相同的方法时,它应该可以工作 添加(参见上面的注 6)。

【问题讨论】:

IANALL,但上面的注释 3 似乎是一把确凿的证据 - 它看起来像一个错误。另一方面,这些东西总是很乱——C++模板也有同样的问题——只是更多。 我在 Eclipse 中复制了相同的代码并运行了它。它工作得很好。没有例外。我已经复制了同一个文件中的所有类(我知道这是一种不好的做法,但我只是在检查你的问题)。 @Logan 我已经编辑了原始问题,以更好地反映您对 Eclipse 与 javac 的观点 【参考方案1】:

考虑这个小问题:

class A

    static void foo() 

class B extends A

    static void foo() 

void test()

    A.foo();
    B.foo();

假设我们从B 中删除foo 方法,并且我们只重新编译B 本身,当我们运行test() 时会发生什么?是否应该因为找不到B.foo() 而引发链接错误?

根据 JLS3 #13.4.12,删除 B.foo 不会破坏二进制兼容性,因为仍然定义了 A.foo。这意味着,当 B.foo() 被执行时,A.foo() 被调用。请记住,test() 没有重新编译,所以这个转发必须由 JVM 处理。

相反,让我们从B 中删除foo 方法,然后重新编译。即使编译器静态地知道B.foo() 实际上意味着A.foo(),它仍然会在字节码中生成B.foo()。目前,JVM 会将B.foo() 转发到A.foo()。但是如果将来B 获得一个新的foo 方法,新方法将在运行时被调用,即使test() 没有被重新编译。

从这个意义上说,静态方法之间存在一种压倒一切的关系。当compile看到B.foo()时,不管B今天是否有foo(),它都必须将其编译为字节码中的B.foo()

在您的示例中,当编译器看到BigCage.printList(animalCage) 时,它会正确推断出它实际上是在调用Cage.printList(List&lt;?&gt;)。所以它需要将调用编译成字节码为BigCage.printList(List&lt;?&gt;)——这里的目标类必须是BigCage而不是Cage

哎呀!字节码格式尚未升级以处理这样的方法签名。泛型信息作为辅助信息保存在字节码中,但对于方法调用,这是旧方法。

擦除发生。该调用实际上被编译成BigCage.printList(List)。太糟糕了BigCage 在擦除后还有一个printList(List)。在运行时,调用该方法!

这个问题是由于 Java 规范和 JVM 规范不匹配造成的。

Java 7 收紧了一点;实现字节码和JVM无法处理这种情况,它不再编译你的代码:

错误:名称冲突: BigCage 中的 printList(List) 和 Cage 中的 printList(List) 有 同样的擦除,但两者都没有隐藏 其他

另一个有趣的事实:如果这两个方法有不同的返回类型,你的程序就可以正常工作。这是因为在字节码中,方法签名包括返回类型。所以Dog printList(List)Object printList(List) 之间没有混淆。另见Type Erasure and Overloading in Java: Why does this work? 这个技巧只在 Java 6 中被允许。Java 7 禁止它,可能是出于技术以外的原因。

【讨论】:

因此,如果我遵循正确,编译器会将 BigCage.printList(List&lt;?&gt;) 添加到 class BigCage 因为 class Cage 定义了 printList(List&lt;?&gt;) 并且 BigCage 扩展了 Cage。我想我唯一的后续问题是为什么我不能手动将printList(List&lt;?&gt;) 添加到 class BigCage 而没有编译时错误(注 6)?编译器是否不可能看到 BigCage 中的静态方法之一具有与基类之一相同的类型擦除并生成错误? 2 个类中的两个分离方法。不能把它们归为一类。请参阅***.com/questions/5527235/… javac6 无法在一个类中填充两个具有相同字节码签名的方法 如果我仍然遗漏了一些明显的东西,我深表歉意。如果我将 BigCage 中的方法 void &lt;U extends Dog&gt; printList(List&lt;U&gt;) 更改为 void &lt;U&gt; printList(List&lt;U&gt;)(注 3),我仍然不明白为什么会出现编译器错误。 void &lt;U extends Dog&gt; printList(List&lt;U&gt;)void &lt;U extends Dog&gt; printList(List&lt;U&gt;) 不会导致相同的擦除吗?当没有扩展时,为什么 U 扩展任何东西会导致错误消失?我知道 JVM 无法正确选择要选择的重载方法,但编译器不能检查这种情况并生成错误吗? 该错误是由于方法不明确引起的,与字节码无关。如果 U 以Dog 为界,则print(List&lt;U&gt;)print(List&lt;?&gt;) 更具体。如果有人调用print(list) 并且两种方法都匹配签名,则选择更具体的方法。如果U 是无限的,那么这两种方法都不比另一种更具体。如果调用匹配这两种方法,编译器会因为歧义而放弃。 这个讨论在java6下是正确的。这是即将过时的 Java 6 的一些有趣特性。在 java 7 中,甚至不允许同时声明这两个方法,无论 U 是否有界。你的程序不会在 java7 下编译。讨论一些只在 java 6 下才观察到的有趣的东西真的没有多大价值。【参考方案2】:

这不是错误。该方法是静态的。您不能覆盖静态方法,只能隐藏它们。

当您在 bigCage 上调用“printList”时,您实际上是在 BigCage 类而不是对象上调用 printList,它将始终调用您在 BigCage 类中声明的静态方法。

【讨论】:

我同意。没有任何东西被覆盖(如上所述)。然而,这些方法被过度使用。我看到的问题是调用 animalCage.printList(animalCage) 应该是编译器错误,因为在 BigCage.printList(...) 中要求传递的 List 是 List&lt;U extends Dog&gt;which animalCage 不是。 animalCage 的类型为List&lt;Animal&gt;。这就是我们以 RunTime 异常结束的原因。 BigCage.printList 只需要 Dogs(或子类型),而传递的 animalCage 列表上有一只猫 你是对的。编译器假定(正确)当您调用 animalCage.printList(List) 时,您正在从超类调用 printList。 那为什么它实际上不调用 Cage.printList(...) 方法而不是调用 BigCage.printList(...) 中产生 RunTime 异常的版本呢? 您的代码正确编译并运行,没有任何类转换异常......它的价值。 您是使用 javac(1.6.0_26 版)还是 Eclipse 构建的?它们是两种不同的编译器。我在 Eclipse 中测试了相同的代码,发现它可以在那里工作。我想另一个问题是哪个编译器具有正确的行为?在这种情况下,我会选择 javac,因为如果我将 printList(List>) 的定义添加到 BigCage,Eclipse 会报告错误。见注 6。【参考方案3】:

这是此代码的最简单版本,但存在相同问题:

import java.util.*;

public class GenericTestsClean 
    public static void main(String[] args) 
        List<Animal> animalCage = new ArrayList<Animal>();
        animalCage.add(new Cat());
        animalCage.add(new Dog());
        BigCage.printList(animalCage);
    


class Animal 
class Dog extends Animal 
class Cat extends Animal 

class BigCage extends Cage 
    public static <U extends Dog> void printList(List<U> list) 
        System.out.println("BigCage#printList");
        for (Object obj : list) 
            System.out.println("BigCage: " + obj.getClass().toString());
        
    


class Cage 
    public static void printList(List list) 
        System.out.println("Cage#printList");
        for (Object obj : list) 
            System.out.println("Cage: " + obj.getClass().toString());
        
    

我认为编译器应该返回错误:

    GenericTestsClean.java:8: <U extends Dog>printList(java.util.List<U>) in BigCage cannot be applied to (java.util.List<Animal>)
        BigCage.printList(animalCage);
               ^
1 error

(或关于名称与相同错误的冲突)但事实并非如此。 拆解后(javap -c GenericTestsClean)我们得到:

invokestatic    #9; //Method BigCage.printList:(Ljava/util/List;)V

致电java GenericTestsClean:

javac 1.6.0_10 版本

BigCage#printList
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Dog
        at BigCage.printList(GenericTestsClean.java:19)
        at GenericTestsClean.main(GenericTestsClean.java:8)

Eclipse 编译器版本

BigCage#printList
BigCage: class Cat
BigCage: class Dog

恕我直言,这两个结果都不正确。

【讨论】:

【参考方案4】:

恕我直言,此代码可能不正确。 BigCage 类中的方法 printList 应该会导致名称冲突,因为 Cage 中的 printList 具有相同的擦除,但都不会覆盖另一个。奇怪的是编译器编译它:)

生成的字节码(javac 1.6.0_10)等价于:

class BigCage extends Cage 

    public static void printList(List list)
        System.out.println((new StringBuilder()).append("*************").append(list.getClass().toString()).toString());
        Dog dog;
        for(Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println((new StringBuilder()).append("BigCage: ").append(dog.getClass().toString()).toString()))
            dog = (Dog)iterator.next();
    

强制转换循环导致异常。 Eclipse 内置编译器生成这样的代码(无一例外):

class BigCage extends Cage

    public static void printList(List list)
        System.out.println((new StringBuilder("*************")).append(list.getClass().toString()).toString());
        Object obj;
        for(Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println((new StringBuilder("BigCage: ")).append(obj.getClass().toString()).toString()))
            obj = iterator.next();
    

或者也许源是好的,但是编译器正在创建错误的字节码? 事实上,我们使用参数BigCage&lt;Animal&gt; animalCage 调用方法&lt;U extends Dog&gt; void printList(List&lt;U&gt; list),而Animal 扩展Dog。

【讨论】:

我同意编译器应该标记这一点,因为这两种方法将导致相同的擦除类型。有趣的是,如果您删除 U 在 BigCage 中扩展 Dog 的要求,它就会出现。请参阅注 3。无论哪种方式,上面的代码都破坏了泛型的类型安全。 是的。我认为在这种情况下,使用 BigCage 参数调用 BigCage#printList 是不正确的。但是删除内部方法可以消除问题。

以上是关于Java、静态方法绑定和泛型都包含一些方法重载的主要内容,如果未能解决你的问题,请参考以下文章

Java协变式覆盖(Override)和泛型重载(Overload)

JAVA——泛型类和泛型方法(静态方法泛型)

Java-8 中的方法引用和泛型

C#方法重载和泛型接口

C#002 方法和接口

C#002 方法和接口