Sélection de méthode surchargée en fonction du type réel du paramètre

115

J'expérimente ce 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);

Cela s'imprime foo(Object o)trois fois. Je m'attends à ce que la sélection de la méthode prenne en considération le type de paramètre réel (et non déclaré). Est-ce que je manque quelque chose? Existe-t-il un moyen de modifier ce code pour qu'il s'imprime foo(12), foo("foobar")et foo(Object o)?

Sergey Mikhanov
la source

Réponses:

96

Je m'attends à ce que la sélection de la méthode prenne en considération le type de paramètre réel (et non déclaré). Est-ce que je manque quelque chose?

Oui. Votre attente est fausse. En Java, la répartition dynamique des méthodes n'a lieu que pour l'objet sur lequel la méthode est appelée, pas pour les types de paramètres des méthodes surchargées.

Citant la spécification du langage Java :

Lorsqu'une méthode est invoquée (§15.12), le nombre d'arguments réels (et tout argument de type explicite) et les types d'arguments à la compilation sont utilisés, au moment de la compilation, pour déterminer la signature de la méthode qui sera invoquée ( §15.12.2). Si la méthode à invoquer est une méthode d'instance, la méthode réelle à invoquer sera déterminée au moment de l'exécution, en utilisant la recherche de méthode dynamique (§15.12.4).

Michael Borgwardt
la source
4
Pouvez-vous expliquer la spécification que vous avez citée s'il vous plaît. Les deux phrases semblent se contredire. L'exemple ci-dessus utilise des méthodes d'instance, mais la méthode appelée n'est clairement pas déterminée au moment de l'exécution.
Alex Worden
15
@Alex Worden: le type de compilation des paramètres de la méthode est utilisé pour déterminer la signature de la méthode à appeler, dans ce cas foo(Object). Lors de l'exécution, la classe de l' objet sur laquelle la méthode est appelée détermine quelle implémentation de cette méthode est appelée, en tenant compte du fait qu'il peut s'agir d'une instance d'une sous-classe du type déclaré qui remplace la méthode.
Michael Borgwardt
86

Comme mentionné précédemment, la résolution de surcharge est effectuée au moment de la compilation.

Java Puzzlers a un bel exemple pour cela:

Puzzle 46: Le cas du constructeur déroutant

Ce puzzle vous présente deux constructeurs déroutants. La méthode main invoque un constructeur, mais lequel? La sortie du programme dépend de la réponse. Qu'est-ce que le programme imprime, ou est-il même légal?

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

Solution 46: Cas du constructeur déroutant

... Le processus de résolution de surcharge de Java fonctionne en deux phases. La première phase sélectionne toutes les méthodes ou constructeurs accessibles et applicables. La deuxième phase sélectionne le plus spécifique des méthodes ou des constructeurs sélectionnés dans la première phase. Une méthode ou un constructeur est moins spécifique qu'un autre s'il peut accepter des paramètres passés à l'autre [JLS 15.12.2.5].

Dans notre programme, les deux constructeurs sont accessibles et applicables. Le constructeur Confusing (Object) accepte tout paramètre passé à Confusing (double []) , donc Confusing (Object) est moins spécifique. (Chaque double tableau est un objet , mais tous les objets ne sont pas un double tableau .) Le constructeur le plus spécifique est donc Confus (double []) , ce qui explique la sortie du programme.

Ce comportement est logique si vous transmettez une valeur de type double [] ; il est contre-intuitif si vous passez null . La clé pour comprendre ce puzzle est que le test pour lequel la méthode ou le constructeur est le plus spécifique n'utilise pas les paramètres réels : les paramètres apparaissant lors de l'invocation. Ils ne sont utilisés que pour déterminer les surcharges applicables. Une fois que le compilateur a déterminé quelles surcharges sont applicables et accessibles, il sélectionne la surcharge la plus spécifique, en utilisant uniquement les paramètres formels: les paramètres apparaissant dans la déclaration.

Pour appeler le constructeur Confusing (Object) avec un paramètre null , écrivez new Confusing ((Object) null) . Cela garantit que seul Confusing (Object) est applicable. Plus généralement, pour forcer le compilateur à sélectionner une surcharge spécifique, transtypez les paramètres réels en types déclarés des paramètres formels.

denis.zhdanov
la source
4
J'espère qu'il n'est pas trop tard pour dire - "l'une des meilleures explications sur SOF". Merci :)
TheLostMind
5
Je crois que si nous ajoutions également le constructeur «private Confusing (int [] iArray)», la compilation échouerait, n'est-ce pas? Car maintenant il y a deux constructeurs avec la même spécificité.
Risser
Si j'utilise des types de retour dynamiques comme entrée de fonction, il utilise toujours le moins spécifique ... a dit que la méthode qui peut être utilisée pour toutes les valeurs de retour possibles ...
kaiser
16

La capacité d'envoyer un appel à une méthode en fonction de types d'arguments est appelée répartition multiple . En Java, cela se fait avec le modèle de visiteur .

Cependant, puisque vous avez affaire à Integers et Strings, vous ne pouvez pas facilement incorporer ce modèle (vous ne pouvez tout simplement pas modifier ces classes). Ainsi, un géant switchsur le temps d'exécution des objets sera votre arme de choix.

Anton Gogolev
la source
11

En Java, la méthode à appeler (comme la signature de méthode à utiliser) est déterminée au moment de la compilation, elle va donc avec le type de compilation.

Le modèle typique pour contourner ce problème consiste à vérifier le type d'objet dans la méthode avec la signature Object et à déléguer à la méthode avec un cast.

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

Si vous avez de nombreux types et que cela est ingérable, alors la surcharge de méthode n'est probablement pas la bonne approche, mais la méthode publique devrait simplement prendre Object et implémenter une sorte de modèle de stratégie pour déléguer la gestion appropriée par type d'objet.

Yishai
la source
4

J'ai eu un problème similaire en appelant le bon constructeur d'une classe appelée "Parameter" qui pourrait prendre plusieurs types Java de base tels que String, Integer, Boolean, Long, etc. Étant donné un tableau d'objets, je veux les convertir en un tableau de mes objets Parameter en appelant le constructeur le plus spécifique pour chaque objet du tableau d'entrée. Je voulais également définir le paramètre du constructeur (Object o) qui lèverait une IllegalArgumentException. J'ai bien sûr trouvé que cette méthode était invoquée pour chaque objet de mon tableau.

La solution que j'ai utilisée était de rechercher le constructeur par réflexion ...

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

Aucune vilaine instanceof, instruction de commutation ou modèle de visiteur requis! :)

Alex Worden
la source
2

Java examine le type de référence lorsqu'il tente de déterminer la méthode à appeler. Si vous souhaitez forcer votre code, vous choisissez la `` bonne '' méthode, vous pouvez déclarer vos champs comme des instances du type spécifique:

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

Vous pouvez également convertir vos paramètres en type de paramètre:

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

S'il existe une correspondance exacte entre le nombre et les types d'arguments spécifiés dans l'appel de méthode et la signature de méthode d'une méthode surchargée, c'est la méthode qui sera appelée. Vous utilisez des références Object, donc java décide au moment de la compilation que pour Object param, il existe une méthode qui accepte directement Object. Donc, il a appelé cette méthode 3 fois.

Ashish Thukral
la source