Java是否有任何理由对同一类中的重载方法使用后期/静态绑定?

时间:2010-10-07 15:47:29

标签: java polymorphism

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

9 个答案:

答案 0 :(得分:15)

科尔根,我同意你的观点,这样的功能可能非常有用,这就是我发现这个问题非常有趣的原因。

要清楚;执行此代码段时:

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

(请注意,我不会讨论如何在内部完成此操作,我只是在这种特定情况下确定所需的假设语义。)

背景:(Korgen,您可以跳过此选项)选择要调用的函数有时也称为“dispatch”。在决定调用哪个函数时考虑单一类型(oo.m()的类型)时,称为单一调度。我们之后所说的是multiple dispatch,因为我们根据多个类型(即被调用者的运行时类型和参数的运行时类型)来决定调用哪个函数。

在Java中不包括对多个调度的支持的可能原因:

  1. 效率。通过virtual method table可以更有效地实施单一调度。引用维基百科有关Double dispatch的文章:

      

    在支持双调度的语言中,这稍微昂贵一些,因为编译器必须生成代码以在运行时计算方法表中方法的偏移量,从而增加整个指令路径长度。 < / p>

  2. 旧版。这是启发Java的语言中的行为,例如C ++,Smalltalk和Eiffel。至少,单一调度遵循最不惊讶的原则。

  3. 复杂性。 how to determine which method to call上的规范是一个非常有趣的读物。它是令人惊讶的复杂,如何将复杂性从编译器推到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.fooC.foo。)

    另一个选择是根据被调用者的类型选择主要的方法,然后根据参数的类型选择 ,在这种情况下{{1}将被召唤。

    我并不是说要定义一个定义明确的语义是不可能的,但我可以说这些规则更加复杂,甚至可能在某些方面反直觉。在早期绑定的情况下,至少编译器和/或IDE可以通过保证在运行时实际发生的事情来帮助开发人员。

答案 1 :(得分:8)

在我看来,最明显的原因是它允许编译器保证实际上会有一个函数被调用。

假设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”,但第二次调用无处可去。我想它可能会产生运行时错误。这类似于严格类型与松散类型的论点。如果它在运行时选择了该功能,它在某种意义上可能更灵活。但是通过在编译时选择函数,我们在编译时得到错误消息,而不是等到运行时间,并确保我们已经使用每个相关的数据组合运行了所有可能的路径。

答案 2 :(得分:7)

很简单。使用的方法由编译器而不是运行时系统选择。这是允许编译器中的类型检查首先工作的原因。

所以,如果你在一个Object中填充一个Integer,你必须告诉编译器你知道它包含一个Integer,所以可以选择合适的方法。

您想要完成的任务通常是使用对象上的方法完成的,因此“this.doSomething()”可以完成您希望它执行的操作。

答案 3 :(得分:4)

它实际上是晚期绑定,而不是早期绑定。早期绑定仅适用于不可重写的方法。

鉴于此代码:

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 "<init>":()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."<init>":()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(wikipedia link here),运行时根据变量中的值决定调用什么方法,而不是基于什么变量声明为。

编译器的工作方式是说:

  • 我正在调用SomeClass.doSomething 声明为Object的变量。
  • SomeClass是否有一个名为的方法 doSomething采取对象?
  • 如果是,则输出invokevirtual 打电话给那个方法。

您想要的是在运行时发生的额外步骤(在编译时不可能发生):

  • 变量指向一个 整数。
  • 我打电话给 SomeClass.doSomething方法。
  • 拨打最佳匹配 SomeClass.doSomething方法 采用整数,数字,对象 (先调用先找到的那个)。

Java在运行时不会这样做,而是简单地调用编译器决定调用的方法。

You can simulate multiple dispatch in Java like so

答案 4 :(得分:3)

  

Java是否为重载方法使用早期绑定有什么特定原因?是不是可以使用后期绑定?

重载方法的动态绑定的一个问题是,如果不同的重载具有不同的返回类型,它将不起作用。运行时行为将更难理解,并且应用程序可能必须处理由动态重载解析失败导致的新类型的运行时异常。

第二个问题是选择基于实际参数类型动态使用的方法将是昂贵的。你不能通过简单的vtable方法调度来实现它。

此外,这是不必要的,因为您可以通过使用方法覆盖/多态来获得方法的动态绑定。

答案 5 :(得分:2)

这是可能的。甚至更多,标准库中有这样的代码(类 - TreeSet,作者(sic!) Josh Bloch)。

在他的一篇演讲中,他说这是错误的。

来自Joshua Bloch How to Design a Good API & Why it Matters

  

小心过载

     
      
  • 避免模棱两可的过载   
        
    • 适用于同一实际的多重载荷
    •   
    • 保守:没有两个具有相同数量的args
    •   
  •   
  • 只是因为你的意思并不代表你应该这样做   
        
    • 通常更好地使用不同的名称
    •   
  •   
  • 如果您必须提供不明确的重载,请确保相同的行为   相同的论点

         

    public TreeSet(Collection c); //忽略订单

         

    public TreeSet(SortedSet s); //尊重订单

  •   

答案 6 :(得分:1)

void doSomething(Comparable c) {..}
void doSomething(Iterable i) {..}

class Foo implements Comparable, Iterable { ..}

doSomething(new Foo()); // which one??

答案 7 :(得分:1)

您看到OBJECT OBJECT而不是INTEGER OBJECT,因为您已宣布 iObject,而不是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

http://ideone.com/sEZrP


正如Thorbjørn's answer所解释的那样,这是因为方法调用在编译时消除歧义,而不是在运行时消除歧义。

答案 8 :(得分:1)

其他人已经比我更好地解释了“为什么”。

但我要说的是,如果你想要这种行为,你会想看看Double Dispatch,尤其是Visitor Pattern