17 Stimmen

Gibt es einen Grund dafür, dass Java late/static binding für überladene Methoden in derselben Klasse verwendet?

Gibt es einen bestimmten Grund, warum Java eine frühe Bindung für überladene Methoden verwendet? Wäre es nicht möglich, dafür late binding zu verwenden?

Exemple :

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

Drucke: OBJEKT OBJEKT

Ich würde eher erwarten: INTEGER OBJEKT

15voto

aioobe Punkte 397211

Korgen, ich stimme Ihnen zu, dass eine solche Funktion möglicherweise sehr nützlich sein könnte, weshalb ich diese Frage sehr interessant fand.

Um das klarzustellen: Beim Ausführen dieses Codeschnipsels:

Object i = new Integer(2);
SomeClass sc = new SomeClass();
sc.doSomething(i);

die JVM sollte sehen, dass i ist vom Laufzeittyp Integer und führen Sie den Code aus als ob sie folgendes ausgeführt hätte :

Integer i = new Integer(2);         // Notice the different static type.
SomeClass sc = new SomeClass();
sc.doSomething(i);

(Ich gehe nicht darauf ein, wie dies intern geschehen sollte, sondern lege nur die hypothetische Semantik fest, die in dieser speziellen Situation gewünscht wird).

Hintergrund: (Korgen, Sie können dies überspringen) Die Auswahl der aufzurufenden Funktion wird manchmal als "dispatch" bezeichnet. Wenn man einen einzigen Typ berücksichtigt (den Typ von o en o.m() ) bei der Entscheidung, welche Funktion aufgerufen werden soll, heißt es einzelner Versand . Was wir hier suchen, heißt Mehrfachversand da wir die aufzurufende Funktion anhand der mehrere Typen (d.h. sowohl den Laufzeittyp des Aufrufenden als auch den Laufzeittyp der Argumente).

Möglicher Grund für die fehlende Unterstützung für Mehrfachversand in Java:

  1. Effizienz. Der Einzelversand lässt sich wesentlich effizienter mit Hilfe eines virtuelle Methodentabelle . Um den Wikipedia-Artikel über den doppelten Versand zu zitieren:

    In einer Sprache, die Double Dispatch unterstützt, ist dies etwas kostspieliger, da der Compiler Code generieren muss, um den Offset der Methode in der Methodentabelle zur Laufzeit zu berechnen, wodurch sich die Gesamtlänge des Befehlspfads erhöht.

  2. Vermächtnis. Dies ist das Verhalten von Sprachen, die Java inspiriert haben, wie C++, Smalltalk und Eiffel. Zumindest folgt Single Dispatch dem Prinzip des geringsten Erstaunens.

  3. Komplexität. Die Spezifikation über wie man feststellt, welche Methode aufgerufen werden soll ist eine recht interessante Lektüre. Es ist Erstaunlicherweise komplex, und es ist keineswegs klar, wie die Komplexität vom Compiler auf die JVM übertragen werden kann. Betrachten Sie zum Beispiel das folgende Snippet:

    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       |
    }                                  '--------------'

    nun, welche Methode hier aufgerufen werden soll:

    A c = new C();
    Object i = new Integer(0);
    c.foo(i);

    Entsprechend dem Laufzeittyp des Aufrufers C.foo je nach Laufzeittyp des Arguments aufgerufen werden sollte B.foo aufgerufen werden sollte.

    Eine Möglichkeit wäre, dies auf die gleiche Weise zu lösen wie einen Anruf staticMethod(c, i) würde in Anwesenheit von staticMethod(A, Object) , staticMethod(B, Integer) y staticMethod(C, Number) . (Es ist jedoch zu beachten, dass in diesem Fall nicht mehr B.foo noch C.foo aufgerufen werden würde, wie oben vorgeschlagen).

    Eine andere Möglichkeit wäre, die Methode hauptsächlich basierend auf dem Typ des Anrufers und sekundär basierend auf den Typen der Argumente, in diesem Fall C.foo angerufen werden würde.

    Ich sage nicht, dass es unmöglich ist, eine genau definierte Semantik festzulegen, aber es würde die Regeln wohl noch komplexer und in einigen Aspekten möglicherweise sogar kontra-intuitiv machen. Im Falle der frühen Bindung können zumindest der Compiler und/oder die IDE dem Entwickler helfen, indem sie Garantien dafür geben, was zur Laufzeit tatsächlich passiert.

8voto

Jay Punkte 26044

Der naheliegendste Grund scheint mir zu sein, dass der Compiler damit garantieren kann, dass es tatsächlich eine Funktion gibt, die aufgerufen wird.

Angenommen, Java wählt die Funktion auf der Grundlage des Laufzeittyps aus, und Sie schreiben dies:

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

Was ist das Ergebnis? Der erste Aufruf von foo gibt vermutlich "String" aus, aber der zweite Aufruf kann nirgendwo hin. Ich nehme an, es könnte einen Laufzeitfehler erzeugen. Das ist ähnlich wie das Argument der strengen Typisierung gegenüber der losen Typisierung. Wenn die Funktion zur Laufzeit ausgewählt würde, könnte sie in gewisser Weise flexibler sein. Aber durch die Auswahl der Funktion zur Kompilierzeit erhalten wir die Fehlermeldungen zur Kompilierzeit und müssen nicht bis zur Laufzeit warten und sicher sein, dass wir jeden möglichen Pfad mit jeder relevanten Datenkombination ausprobiert haben.

7voto

Es ist ganz einfach. Die zu verwendende Methode wird vom Compiler und nicht vom Laufzeitsystem ausgewählt. Nur so kann die Typüberprüfung im Compiler überhaupt funktionieren.

Wenn Sie also eine Ganzzahl in ein Objekt packen, müssen Sie dem Compiler mitteilen, dass Sie wissen, dass es eine Ganzzahl enthält, damit die entsprechende Methode gewählt werden kann.

Was Sie erreichen wollen, wird normalerweise mit Methoden auf dem Objekt getan, so dass "this.doSomething()" das tut, was Sie tun wollen.

4voto

TofuBeer Punkte 59410

Es handelt sich um eine späte Bindung, nicht um eine frühe Bindung. Frühes Binden geschieht nur bei nicht überschreibbaren Methoden.

Angesichts dieses Codes:

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

Das von mir verwendete javac erzeugt dies für 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 bedeutet späte Bindung, invokestatic und invokespecial bedeutet frühe Bindung.

Die Linie:

24: invokevirtual #14; //Methode bar:()V

verweist auf eine nicht überschreibbare Methode, so dass sie logischerweise invokespecial sein sollte. Der Laufzeit steht es anscheinend frei, die Änderung vorzunehmen, wenn die Klasse geladen wird (ich könnte mich irren, ich habe mich nicht so tief in die VM-Interna eingearbeitet, aber nach dem, was ich gelesen habe, scheint das der Fall zu sein).

Ihre Frage lautet also, warum Java nicht über das sogenannte Multiple Dispatch ( wikipedia link hier ), bei dem die Laufzeitumgebung entscheidet, welche Methode auf der Grundlage des Variablenwerts aufgerufen werden soll, anstatt auf der Grundlage des Variablenwerts, der deklariert wurde.

Der Compiler arbeitet so, dass er etwas sagt wie:

  • Ich rufe SomeClass.doSomething auf einer Variablen, die als Objekt deklariert ist.
  • Verfügt SomeClass über eine Methode namens doSomething, die ein Objekt annimmt?
  • Wenn ja, dann geben Sie eine invokevirtual Aufruf zu dieser Methode aus.

Was Sie wollen, ist ein zusätzlicher Schritt, der zur Laufzeit passiert (er könnte nicht zur Kompilierzeit passieren), der sagt:

  • Die Variable verweist auf eine Ganzzahl.
  • Ich rufe die SomeClass.doSomething-Methode auf.
  • Rufen Sie die beste Übereinstimmung von SomeClass.doSomething-Methode auf, die eine Ganzzahl, eine Zahl oder ein Objekt annimmt aufruft (je nachdem, was zuerst gefunden wird).

Java macht das zur Laufzeit nicht, sondern ruft einfach die Methode auf, die der Compiler für den Aufruf vorgesehen hat.

In Java können Sie den Mehrfachversand wie folgt simulieren .

3voto

Stephen C Punkte 665668

Gibt es einen bestimmten Grund, warum Java eine frühe Bindung für überladene Methoden verwendet? Wäre es nicht möglich, dafür late binding zu verwenden?

Ein Problem bei der dynamischen Bindung von überladenen Methoden besteht darin, dass sie nicht funktionieren würde, wenn die verschiedenen Überladungen unterschiedliche Rückgabetypen hätten. Das Laufzeitverhalten wäre schwieriger zu verstehen, und die Anwendung müsste sich möglicherweise mit einer neuen Klasse von Laufzeitausnahmen auseinandersetzen, die durch eine fehlgeschlagene dynamische Überladungsauflösung verursacht werden.

Ein zweites Problem besteht darin, dass die Auswahl der zu verwendenden Methode dynamisch auf der Grundlage der tatsächlichen Argumenttypen teuer wäre. Sie können dies nicht durch einfaches Dispatching von vtable-Methoden implementieren.

Außerdem ist dies unnötig, da Sie die dynamische Bindung von Methoden durch Methodenüberschreibung/Polymorphismus erreichen können.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X