8

请参见以下示例:

interface I {}

class A implements I {}

class B implements I {}

class Foo{
    void f(A a) {}
    void f(B b) {}
    static public void main(String[]args ) {
        I[] elements = new I[] {new A(), new B(), new B(), new A()};
        Foo o = new Foo();
        for (I element:elements)
            o.f(element);//won't compile
    }
}

为什么重载方法不支持向上转换?

如果在运行时实现重载,它将提供更大的灵活性。例如,访问者模式会更简单。是否有任何技术原因阻止 Java 这样做?

4

7 回答 7

6

重载解决涉及一些重要的规则来确定最适合的重载,并且很难在运行时有效地执行这些操作。相比之下,覆盖解决方案更容易——在困难的情况下,您只需查找foo对象类的函数,而在简单的情况下(例如,当只有一个实现,或在此代码路径中只有一个实现时),您可以将虚拟方法转换为静态编译的、非虚拟的、非动态调度的调用(如果你是根据代码路径来做的,你必须做一个快速检查以验证对象实际上是你期望)。

事实证明,Java 1.4 及更低版本没有运行时覆盖解决方案是一件好事,因为这会使泛型更难改造。泛型在覆盖解析中发挥作用,但由于擦除,此信息在运行时不可用。

于 2012-05-23T04:33:32.083 回答
4

没有理论上的理由不能做到这一点。Common Lisp 对象系统支持这种类型的构造——称为多重分派——尽管它在一个稍微不同的范式中这样做(方法,而不是附加到对象,是泛型(或泛型函数)的实例,它可以在多个参数值的运行时)。我相信 Java 也有扩展来启用它(我想到了 Multi-Java,尽管这可能是多重继承而不是多重分派)。

但是,除了语言设计者认为不应该这样做之外,可能还有 Java 语言无法完成的原因,我会让其他人去推理。不过,它确实给继承带来了复杂性。考虑:

interface A {}
interface B {}
class C implements A {}

class Foo {
    public void invoke(A a) {}
    public void invoke(B b) {}
}

class Bar extends Foo {
    public void invoke(C c) {}
}

class Baz extends Bar {
    public void invoke(A a) {}
}

Baz obj = new Baz();
obj.invoke(new C);

哪个invoke被调用?Baz? Bar? 是什么super.invoke?提出确定性语义是可能的,但至少在某些情况下,它们可能会涉及混淆和意外。鉴于 Java 旨在成为一种简单的语言,我认为引入这种混淆的特性可能不会被视为符合其目标。

于 2012-05-24T15:14:29.093 回答
3

是否有任何技术原因阻止 Java 这样做?

代码正确性:您当前的示例提供了 I 的两个实现和两个相应的方法 f。然而,没有什么能阻止其他实现 I 的类的存在 - 将分辨率移动到运行时也会将编译错误替换为可能隐藏的运行时错误。

性能:正如其他人所提到的,方法重载涉及相当复杂的规则,在编译时这样做肯定比在运行时为每个方法调用都这样做要快。

向后兼容性:当前重载的方法是使用传递参数的编译时类型而不是它们的运行时类型来解决的,改变行为以使用运行时信息会破坏许多现有的应用程序。

如何解决它

使用访问者模式,我不明白有人会认为它很难。

interface I{
      void accept(IVisitor v);
}
interface IVisitor{
      void f(A a);
      void f(B b);
}

class A implements I{
    void accept(IVisitor v){v.f(this);}
}
class B implements I{
    void accept(IVisitor v){v.f(this);}
}
class Foo implements IVisitor{
 void f(A a) {}
 void f(B b) {}
 static public void main(String[]args ) {
    I[] elements = new I[] {new A(), new B(), new B(), new A()};
    Foo o = new Foo();
    for (I element:elements)
        element.accept(o);
 }


}
于 2012-05-26T21:32:46.783 回答
3

我认为除了语言的设计者之外没有人可以回答这个问题。我不是该主题的专家,但我将仅提供我的意见。

通过阅读JLS 15.12关于方法调用表达式,很明显选择正确的方法来执行是一个已经很复杂的编译时过程;首先是在引入泛型之后。

现在想象一下将所有这些转移到运行时只是为了支持mutimemethods的单一特性。对我来说,这听起来像是一个给语言增加了太多复杂性的小特性,并且可能是一个具有一定性能影响的特性,因为所有这些决定都需要在运行时一遍又一遍地做出,而不仅仅是一次,就像今天一样,在编译时。

对于所有这些,我们可以添加一个事实,即由于类型擦除,不可能确定某些泛型类型的实际类型。在我看来,放弃静态类型检查的安全性并不符合 Java 的最佳利益。

无论如何,有有效的替代方案来处理多分派问题,也许这些替代方案几乎可以证明为什么它没有在该语言中实现。因此,您可以使用经典的访问者模式,也可以使用一定量的反射。

有一个过时的MultiJava 项目在 Java 中实现了多调度支持,还有几个其他项目使用反射来支持 Java 中的多方法:Java MultimethodsJava Multimethods Framework。也许还有更多。

您还可以考虑另一种支持多方法的基于 Java 的语言,例如ClojureGroovy

此外,由于 C# 是一种在其一般哲学上与 Java 非常相似的语言,因此更多地研究它如何支持多方法并思考在 Java 中提供类似功能的含义可能会很有趣。如果您认为这是 Java 中值得拥有的功能,您甚至可以提交JEP,它可能会在 Java 语言的未来版本中被考虑在内。

于 2012-05-23T06:44:01.103 回答
2

Guess you just have to settle with reflection:

import java.lang.reflect.*;

interface I {}    
class A implements I {}    
class B implements I {}

public class Foo {

    public void f(A a) { System.out.println("from A"); }
    public void f(B b) { System.out.println("from B"); }

    static public void main(String[]args ) throws InvocationTargetException
        , NoSuchMethodException, IllegalAccessException 
    {
        I[] elements = new I[] {new A(), new B(), new B(), new A()};
        Foo o = new Foo();
        for (I element : elements) {
            o.multiDispatch(element);    
        }
    }

    void multiDispatch(I x) throws NoSuchMethodException
        , InvocationTargetException, IllegalAccessException 
    {    
        Class cls = this.getClass();

        Class[] parameterTypes = { x.getClass() };
        Object[] arguments = { x };

        Method fMethod = cls.getMethod("f", parameterTypes);
        fMethod.invoke(this,arguments);
    }       
}

Output:

from A
from B
from B
from A
于 2012-05-26T00:04:08.420 回答
2

不是Java的答案。这个功能虽然存在于 C# 4 中:

using System;

public class MainClass {     
    public static void Main() {
        IAsset[] xx = { 
             new Asset(), new House(), new Asset(), new House(), new Car() 
        };     
        foreach(IAsset x in xx) {
            Foo((dynamic)x);
        }
    }


    public static void Foo(Asset a) {
        Console.WriteLine("Asset");
    }

    public static void Foo(House h) {
        Console.WriteLine("House");
    }

    public static void Foo(Car c) {
        Console.WriteLine("Car");
    }     
}

public interface IAsset { }      

public class Asset : IAsset { }

public class House : Asset { }

public class Car : Asset { }

输出:

Asset
House
Asset
House
Car

如果您使用的是 C# 3 及以下版本,则必须使用反射,我在我的博客Multiple Dispatch in C#上发表了一篇关于它的文章:http ://www.ienablemuch.com/2012/04/multiple-dispatch-in-c .html

如果你想在 Java 中进行多次调度,你可能会走反射路线。

这是 Java 的另一种解决方案:http ://blog.efftinge.de/2010/03/multiple-dispatch-and-poor-mens-patter.html

于 2012-05-23T04:47:02.670 回答
1

您的方法说它将接受AB哪些是 的派生类I,然后它们可以包含更多详细信息I

void f(A a) {}

当您尝试A在您的案例接口中发送超类时I,编译器需要确认您实际发送A的详细信息A可能不可用I,也只有在运行时I才会实际引用A编译时没有此类信息的实例。 , 所以你必须明确地告诉编译器I实际上是AorB并且你做一个演员来这么说。

于 2012-05-23T04:32:15.797 回答