为什么重载不能在执行时实现?

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实现这一点?


我试图想象这种方法的语义如何工作,但我的大脑只是“恶心”。我期望能够更改我正在使用的接口实现,并使其余代码完全相同。 - Louis Wasserman
因为静态地尝试确定对象的运行时类型就像简单地运行程序一样? - Perception
1
@Perception 他在问为什么重载分派不能推迟到运行时而不是在编译时解析。这听起来会给 JVM 和类加载逻辑增加巨大的复杂性。 - Jim Garrison
1
@JimGarrison - 是的,我知道他在问什么。但我拒绝跨越逻辑障碍,将重载函数组视为JVM在运行时分派的“功能等效项”的虚拟查找。是的,这会增加很多复杂性。 - Perception
可能是重复的,或者至少与Java中的重载和多重分派非常相似。 - Edwin Dalorzo
7个回答

6
重载决议涉及一些非平凡的规则来确定哪个重载最合适,而在运行时高效地执行这些规则将是困难的。相比之下,覆盖决议更容易 - 在复杂情况下,您只需查找对象类的foo函数,而在简单情况下(例如,当此代码路径中只有一个实现或仅有一个实现时),您可以将虚拟方法转换为静态编译的、非虚拟的、非动态调度的调用(如果您基于代码路径进行操作,则必须快速检查以验证对象实际上是否符合预期)。
事实证明,Java 1.4及以下版本没有运行时覆盖决议是件好事,因为这将使泛型的后期改装更加困难。泛型在覆盖决议中发挥了作用,但由于类型擦除,这些信息在运行时不可用。

4
没有理论上的障碍可以阻止它的实现。通用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旨在成为一种简单的语言,我认为引入这种混淆的特性不太可能符合其目标。


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);
 }


}

3
我认为只有语言的设计者才能回答这个问题。我并不是这方面的专家,但我会提供我的意见。通过阅读关于方法调用表达式的JLS 15.12,很明显选择正确的方法已经是一个复杂的编译时过程,特别是在引入泛型之后更是如此。现在想象一下将所有这些都移动到运行时,仅仅为了支持多方法这个小功能。对我来说,这听起来像是添加了太多复杂性的小功能,而且可能会带来一定的性能影响,因为现在所有这些决策都需要在运行时重复进行,而不是像现在这样只需要在编译时进行一次。
我们还可以加上一个事实,由于类型擦除的缘故,确定某些泛型类型的实际类型是不可能的。在我看来,放弃静态类型检查的安全性并不符合Java的最佳利益。
无论如何,有有效的替代方案来处理多分派问题,也许这些替代方案基本上可以证明为什么它没有被实现在语言中。因此,您可以使用传统的访问者模式或使用一定量的反射。

有一个过时的 MultiJava 项目,它在 Java 中实现了多重分派支持,还有一些其他使用反射支持 Java 多方法的项目:Java Multimethods, Java Multimethods Framework。也许还有更多。

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

另外,由于C#语言在其总体哲学上与Java非常相似,因此深入研究它如何支持多方法可能会很有趣,并思考在Java中提供类似功能的影响。如果您认为这是Java值得拥有的一个特性,甚至可以提交一个JEP,并且它可能会被考虑用于Java语言的未来版本。


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


2

看来你只能使用反射:

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);
    }       
}

输出:

from A
from B
from B
from A

1

你的方法说明它将接受AB,它们是I的派生类,它们可以包含比I更多的细节。

void f(A a) {}

当您尝试在案例接口中发送A的超类时,编译器需要确认您实际上正在发送A,因为在I中可用的详细信息可能不可用于I中,而且只有在运行时I才会实际引用A的实例,在编译时没有此类信息,因此您必须明确告诉编译器I实际上是AB,并进行转换以说明。

我对此无法理解太多,但如果意图是说 OP 想要的无法实现,那是可以的。 - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接