Überladene Methodenauswahl basierend auf dem realen Typ des Parameters

113

Ich experimentiere mit diesem Code:

interface Callee {
    public void foo(Object o);
    public void foo(String s);
    public void foo(Integer i);
}

class CalleeImpl implements Callee
    public void foo(Object o) {
        logger.debug("foo(Object o)");
    }

    public void foo(String s) {
        logger.debug("foo(\"" + s + "\")");
    }

    public void foo(Integer i) {
        logger.debug("foo(" + i + ")");
    }
}

Callee callee = new CalleeImpl();

Object i = new Integer(12);
Object s = "foobar";
Object o = new Object();

callee.foo(i);
callee.foo(s);
callee.foo(o);

Dies wird foo(Object o)dreimal gedruckt . Ich erwarte, dass bei der Methodenauswahl der reale (nicht der deklarierte) Parametertyp berücksichtigt wird. Vermisse ich etwas Gibt es eine Möglichkeit, diesen Code so zu ändern, dass er gedruckt wird foo(12), foo("foobar")und foo(Object o)?

Sergey Mikhanov
quelle

Antworten:

94

Ich erwarte, dass bei der Methodenauswahl der reale (nicht der deklarierte) Parametertyp berücksichtigt wird. Vermisse ich etwas

Ja. Ihre Erwartung ist falsch. In Java erfolgt der dynamische Methodenversand nur für das Objekt, für das die Methode aufgerufen wird, nicht für die Parametertypen überladener Methoden.

Zitieren der Java-Sprachspezifikation :

Wenn eine Methode aufgerufen wird (§15.12), werden die Anzahl der tatsächlichen Argumente (und alle expliziten Typargumente) und die Kompilierungszeittypen der Argumente zur Kompilierungszeit verwendet, um die Signatur der Methode zu bestimmen, die aufgerufen wird ( §15.12.2). Wenn es sich bei der aufzurufenden Methode um eine Instanzmethode handelt, wird die tatsächlich aufzurufende Methode zur Laufzeit mithilfe der dynamischen Methodensuche ermittelt (§15.12.4).

Michael Borgwardt
quelle
4
Können Sie bitte die von Ihnen angegebene Spezifikation erläutern? Die beiden Sätze scheinen sich zu widersprechen. Im obigen Beispiel werden Instanzmethoden verwendet, die aufgerufene Methode wird jedoch zur Laufzeit eindeutig nicht ermittelt.
Alex Worden
15
@Alex Worden: In diesem Fall wird der Kompilierungszeittyp der Methodenparameter verwendet, um die Signatur der aufzurufenden Methode zu bestimmen foo(Object). Zur Laufzeit bestimmt die Klasse des Objekts , für das die Methode aufgerufen wird , welche Implementierung dieser Methode aufgerufen wird, wobei berücksichtigt wird, dass es sich möglicherweise um eine Instanz einer Unterklasse des deklarierten Typs handelt, die die Methode überschreibt.
Michael Borgwardt
86

Wie bereits erwähnt, wird die Überladungsauflösung zur Kompilierungszeit durchgeführt.

Java Puzzlers hat dafür ein schönes Beispiel:

Puzzle 46: Der Fall des verwirrenden Konstruktors

Dieses Puzzle präsentiert Ihnen zwei verwirrende Konstruktoren. Die Hauptmethode ruft einen Konstruktor auf, aber welcher? Die Ausgabe des Programms hängt von der Antwort ab. Was druckt das Programm oder ist es überhaupt legal?

public class Confusing {

    private Confusing(Object o) {
        System.out.println("Object");
    }

    private Confusing(double[] dArray) {
        System.out.println("double array");
    }

    public static void main(String[] args) {
        new Confusing(null);
    }
}

Lösung 46: Fall des verwirrenden Konstruktors

... Javas Überlastungsauflösungsprozess läuft in zwei Phasen ab. In der ersten Phase werden alle Methoden oder Konstruktoren ausgewählt, auf die zugegriffen werden kann und die anwendbar sind. In der zweiten Phase werden die spezifischsten Methoden oder Konstruktoren ausgewählt, die in der ersten Phase ausgewählt wurden. Eine Methode oder ein Konstruktor ist weniger spezifisch als eine andere, wenn sie Parameter akzeptieren kann, die an die andere übergeben wurden [JLS 15.12.2.5].

In unserem Programm sind beide Konstruktoren zugänglich und anwendbar. Der Konstruktor Confusing (Object) akzeptiert alle an Confusing (double []) übergebenen Parameter , sodass Confusing (Object) weniger spezifisch ist. (Jedes Doppelarray ist ein Objekt , aber nicht jedes Objekt ist ein Doppelarray .) Der spezifischste Konstruktor ist daher Confusing (double []) , was die Ausgabe des Programms erklärt.

Dieses Verhalten ist sinnvoll, wenn Sie einen Wert vom Typ double [] übergeben . Es ist nicht intuitiv, wenn Sie null übergeben . Der Schlüssel zum Verständnis dieses Puzzles besteht darin, dass der Test, für den die Methode oder der Konstruktor am spezifischsten ist, nicht die tatsächlichen Parameter verwendet : die im Aufruf angezeigten Parameter. Sie werden nur verwendet, um zu bestimmen, welche Überladungen anwendbar sind. Sobald der Compiler bestimmt hat, welche Überladungen anwendbar und zugänglich sind, wählt er die spezifischste Überladung aus, wobei nur die formalen Parameter verwendet werden: die in der Deklaration angezeigten Parameter.

Um den Konstruktor Confusing (Object) mit einem Nullparameter aufzurufen , schreiben Sie new Confusing ((Object) null) . Dies stellt sicher, dass nur Verwirrend (Objekt) anwendbar ist. Um den Compiler zu zwingen, eine bestimmte Überladung auszuwählen, werden die tatsächlichen Parameter allgemeiner in die deklarierten Typen der formalen Parameter umgewandelt.

denis.zhdanov
quelle
4
Ich hoffe, es ist nicht zu spät zu sagen - "eine der besten Erklärungen für SOF". Danke :)
TheLostMind
5
Ich glaube, wenn wir auch den Konstruktor 'private Confusing (int [] iArray)' hinzufügen würden, würde er nicht kompiliert werden können, nicht wahr? Denn jetzt gibt es zwei Konstruktoren mit der gleichen Spezifität.
Risser
Wenn ich dynamische Rückgabetypen als Funktionseingabe verwende, verwendet es immer die weniger spezifische ... sagte die Methode, die für alle möglichen Rückgabewerte verwendet werden kann ...
Kaiser
16

Die Möglichkeit, einen Aufruf an eine Methode zu senden, die auf Argumenttypen basiert, wird als Mehrfachversand bezeichnet . In Java erfolgt dies mit dem Besuchermuster .

Da es sich jedoch um Integers und Strings handelt, können Sie dieses Muster nicht einfach einbinden (Sie können diese Klassen einfach nicht ändern). Somit ist ein Riese switchmit Objektlaufzeit die Waffe Ihrer Wahl.

Anton Gogolev
quelle
11

In Java wird die aufzurufende Methode (wie die zu verwendende Methodensignatur) zur Kompilierungszeit festgelegt, sodass sie zum Typ der Kompilierungszeit gehört.

Das typische Muster, um dies zu umgehen, besteht darin, den Objekttyp in der Methode mit der Objektsignatur zu überprüfen und mit einer Umwandlung an die Methode zu delegieren.

    public void foo(Object o) {
        if (o instanceof String) foo((String) o);
        if (o instanceof Integer) foo((Integer) o);
        logger.debug("foo(Object o)");
    }

Wenn Sie viele Typen haben und dies nicht verwaltbar ist, ist das Überladen von Methoden wahrscheinlich nicht der richtige Ansatz. Vielmehr sollte die öffentliche Methode nur Object verwenden und eine Art Strategiemuster implementieren, um die entsprechende Behandlung pro Objekttyp zu delegieren.

Yishai
quelle
4

Ich hatte ein ähnliches Problem beim Aufrufen des richtigen Konstruktors einer Klasse namens "Parameter", die mehrere grundlegende Java-Typen wie String, Integer, Boolean, Long usw. annehmen kann. Angesichts eines Arrays von Objekten möchte ich sie in ein Array konvertieren meiner Parameterobjekte durch Aufrufen des spezifischsten Konstruktors für jedes Objekt im Eingabearray. Ich wollte auch den Konstruktorparameter (Objekt o) definieren, der eine IllegalArgumentException auslösen würde. Ich habe natürlich festgestellt, dass diese Methode für jedes Objekt in meinem Array aufgerufen wird.

Die Lösung, die ich benutzte, bestand darin, den Konstruktor durch Reflexion nachzuschlagen ...

public Parameter[] convertObjectsToParameters(Object[] objArray) {
    Parameter[] paramArray = new Parameter[objArray.length];
    int i = 0;
    for (Object obj : objArray) {
        try {
            Constructor<Parameter> cons = Parameter.class.getConstructor(obj.getClass());
            paramArray[i++] = cons.newInstance(obj);
        } catch (Exception e) {
            throw new IllegalArgumentException("This method can't handle objects of type: " + obj.getClass(), e);
        }
    }
    return paramArray;
}

Keine hässliche Instanz, Schalteranweisungen oder Besuchermuster erforderlich! :) :)

Alex Worden
quelle
2

Java untersucht den Referenztyp, wenn versucht wird, die aufzurufende Methode zu bestimmen. Wenn Sie Ihren Code erzwingen möchten, wählen Sie die richtige Methode. Sie können Ihre Felder als Instanzen des bestimmten Typs deklarieren:

Integeri = new Integer(12);
String s = "foobar";
Object o = new Object();

Sie können Ihre Parameter auch als Typ des Parameters umwandeln:

callee.foo(i);
callee.foo((String)s);
callee.foo(((Integer)o);
akf
quelle
1

Wenn es eine genaue Übereinstimmung zwischen der Anzahl und den Arten von Argumenten gibt, die im Methodenaufruf angegeben sind, und der Methodensignatur einer überladenen Methode, wird diese Methode aufgerufen. Sie verwenden Objektreferenzen, daher entscheidet Java beim Kompilieren, dass es für Object param eine Methode gibt, die Object direkt akzeptiert. Also hat es diese Methode dreimal aufgerufen.

Ashish Thukral
quelle