Java是否有任何理由对同一类中的重载方法使用延迟/静态绑定?
Java是否为重载方法使用早期绑定有什么特定原因? 是不是可以使用后期绑定?
例:
public class SomeClass { public void doSomething(Integer i) { System.out.println("INTEGER"); } public void doSomething(Object o) { System.out.println("OBJECT"); } public static void main (String[] args) { Object i = new Integer(2); Object o = new Object(); SomeClass sc = new SomeClass(); sc.doSomething(i); sc.doSomething(o); } }
打印:对象对象
我宁愿期待:INTEGER OBJECT
在我看来,最明显的原因是它允许编译器保证实际上会有一个函数被调用。
假设Java选择了基于运行时类型的函数,你写了这个:
public class MyClass { public void foo(Integer i) { System.out.println("Integer"); } public void foo(String s) { System.out.println("String"); } public static void main(String[] args) { Object o1=new String("Hello world"); foo(o1); Object o2=new Double(42); foo(o2); } }
什么输出? 第一次调用foo可能会打印“String”,但第二次调用无处可去。 我想它可能会产生运行时错误。 这类似于严格类型与松散类型的论点。 如果它在运行时选择了该function,它在某种意义上可能更灵活。 但是通过在编译时选择函数,我们在编译时得到错误消息,而不是等到运行时间,并确保我们已经使用每个相关数据组合运行了每个可能的路径。
Korgen,我同意你的观点,这样的function可能非常有用,这就是我发现这个问题非常有趣的原因。
要清楚; 执行此代码段时:
Object i = new Integer(2); SomeClass sc = new SomeClass(); sc.doSomething(i);
JVM应该看到i
是运行时类型Integer
并执行代码,好像它执行了以下操作 :
Integer i = new Integer(2); // Notice the different static type. SomeClass sc = new SomeClass(); sc.doSomething(i);
(请注意,我不会讨论如何在内部完成此操作,我只是在这个特定情况下确定所需的假设语义。)
背景:(科尔根,你可以跳过这个)选择要调用的函数有时被称为“调度”。 当在决定调用哪个函数时考虑单个类型( om()
的o
的类型om()
,它被称为单个调度 。 我们之后所说的是多分派,因为我们将决定基于多种类型调用哪个函数(即被调用者的运行时类型和参数的运行时类型)。
不支持在Java中支持多个调度的可能原因:
-
效率。 通过虚拟方法表可以更有效地实现单个调度。 引用维基百科关于Double dispatch的文章:
在支持双调度的语言中,这稍微昂贵一些,因为编译器必须生成代码以在运行时计算方法表中方法的偏移量,从而增加整个指令路径长度。
-
遗产。 这是启发Java的语言中的行为,例如C ++,Smalltalk和Eiffel。 至少,单一调度遵循最不惊讶的原则。
-
复杂。 关于如何确定调用哪个方法的规范是一个非常有趣的读物。 它非常复杂,如何将复杂性从编译器推送到JVM并不明显。 例如,考虑以下代码段:
class A { .--------------. void foo(Object o) {} | A | } '--------------' | class B extends A { .--------------. void foo(Integer i) {} | B | } '--------------' | class C extends B { .--------------. void foo(Number n) {} | C | } '--------------'
现在应该在这里调用哪个方法:
A c = new C(); Object i = new Integer(0); c.foo(i);
根据被调用者的运行时类型,应该调用
C.foo
同时根据参数的运行时类型调用B.foo
。一种选择是以与调用
staticMethod(c, i)
在存在staticMethod(A, Object)
,staticMethod(B, Integer)
和staticMethod(C, Number)
的情况下解析相同的方式解决此问题。 (但请注意,在这种情况下,如上所述,将调用更好的B.foo
和C.foo
。)另一个选择是主要根据被调用者的类型选择方法, 其次是基于参数的类型,在这种情况下将调用
C.foo
。我并不是说不可能确定一个定义良好的语义,但我可以说这些规则更复杂,甚至可能在某些方面反直觉。 在早期绑定的情况下,至少编译器和/或IDE可以通过保证在运行时实际发生的事情来帮助开发人员。
这很简单。 使用的方法由编译器而不是运行时系统选择。 这是允许编译器中的类型检查首先工作的原因。
因此,如果在对象中填充Integer,则必须告诉编译器您知道它包含Integer,因此可以选择适当的方法。
您想要完成的任务通常是使用对象上的方法完成的,因此“this.doSomething()”可以完成您希望它执行的操作。
它实际上是晚期绑定,而不是早期绑定。 早期绑定仅适用于不可重写的方法。
鉴于此代码:
public class Test { void foo() { System.out.println("foo"); } final void bar() { System.out.println("bar"); } void car(String s) { System.out.println("car String"); } void car(Object o) { System.out.println("car Object"); } static void star() { System.out.println("star"); } public static void main(final String[] argv) { Test test; Object a; Object b; test = new Test(); a = "Hello"; b = new Object(); test.foo(); test.bar(); test.car(a); test.car(b); Test.star(); } }
我使用的javac为main生成了这个:
public static void main(java.lang.String[]); Code: 0: new #9; //class Test 3: dup 4: invokespecial #10; //Method "":()V 7: astore_1 8: ldc #11; //String Hello 10: astore_2 11: new #12; //class java/lang/Object 14: dup 15: invokespecial #1; //Method java/lang/Object." ":()V 18: astore_3 19: aload_1 20: invokevirtual #13; //Method foo:()V 23: aload_1 24: invokevirtual #14; //Method bar:()V 27: aload_1 28: aload_2 29: invokevirtual #15; //Method car:(Ljava/lang/Object;)V 32: aload_1 33: aload_3 34: invokevirtual #15; //Method car:(Ljava/lang/Object;)V 37: invokestatic #16; //Method star:()V 40: return }
invokevirtual意味着后期绑定,invokestatic和invokespecial意味着早期绑定。
这条线:
24:invokevirtual#14; //方法栏:()V
指的是一个不可重写的方法,所以从逻辑上讲它应该是invokespecial。 在加载类时,运行时显然可以自由地进行更改(我可能错了,我没有深入研究VM内部,但从我读到的内容似乎就是这种情况)。
所以你的问题就是为什么java没有所谓的Multiple Dispatch( 维基百科链接 ),运行时根据变量中的值决定调用什么方法,而不是基于声明变量的方法。
编译器的工作方式是说:
- 我在声明为Object的变量上调用SomeClass.doSomething。
- SomeClass有一个名为doSomething的方法,它接受一个Object吗?
- 如果是,则输出对该方法的invokevirtual调用。
您想要的是在运行时发生的额外步骤(在编译时不可能发生):
- 变量指向一个Integer。
- 我正在调用SomeClass.doSomething方法。
- 调用SomeClass.doSomething方法的最佳匹配,该方法采用整数,数字,对象(最先找到的调用)。
Java不是在运行时,而是简单地调用编译器决定调用的方法。
您可以像这样在Java中模拟多个调度 。
Java是否为重载方法使用早期绑定有什么特定原因? 是不是可以使用后期绑定?
动态绑定重载方法的一个问题是,如果不同的重载具有不同的返回类型,它将无法工作。 运行时行为将更难理解,并且应用程序可能必须处理由动态重载解析失败导致的新类型的运行时exception。
第二个问题是选择基于实际参数类型动态使用的方法将是昂贵的。 您无法通过简单的vtable方法调度来实现此function。
此外,这是不必要的,因为您可以通过使用方法覆盖/多态来获得方法的动态绑定。
这是可能的。 甚至更多,标准库中有这样的代码(类 – TreeSet,作者(sic!) Josh Bloch)。
在他的一个演讲中,他说这是错误的。
来自Joshua Bloch如何设计一个好的API及其重要性
小心超载
- 避免模糊的过载
- 适用于相同实际的多次过载
- 保守派:没有两个具有相同数量的args
- 只是因为你的意思并不代表你应该这样做
- 通常最好使用不同的名称
如果必须提供不明确的重载,请确保相同参数的行为相同
public TreeSet(Collection c); //忽略订单
public TreeSet(SortedSet s); //尊重订单
void doSomething(Comparable c) {..} void doSomething(Iterable i) {..} class Foo implements Comparable, Iterable { ..} doSomething(new Foo()); // which one??
你看到OBJECT OBJECT
而不是INTEGER OBJECT
因为你已经声明 i
是一个Object
而不是一个Integer
。 如果你这样做:
public class SomeClass { public void doSomething(Integer i) { System.out.println("INTEGER"); } public void doSomething(Object o) { System.out.println("OBJECT"); } public static void main (String[] args) { Integer i = new Integer(2); Object o = new Object(); SomeClass sc = new SomeClass(); sc.doSomething(i); sc.doSomething(o); } }
你会得到INTEGER OBJECT
。
正如Thorbjørn的回答所解释的那样,这是因为方法调用在编译时消除歧义,而不是在运行时消除歧义。
其他人比我更好地解释了“为什么”。
但是,我要说的是,如果你想要这种行为,你会想看看Double Dispatch ,特别是访客模式 。