Selección de método sobrecargado según el tipo real del parámetro

115

Estoy experimentando con este código:

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

Esto se imprime foo(Object o)tres veces. Espero que la selección del método tenga en cuenta el tipo de parámetro real (no el declarado). ¿Me estoy perdiendo de algo? ¿Hay una manera de modificar el código para que se va a imprimir foo(12), foo("foobar")y foo(Object o)?

Sergey Mikhanov
fuente

Respuestas:

96

Espero que la selección del método tenga en cuenta el tipo de parámetro real (no el declarado). ¿Me estoy perdiendo de algo?

Si. Tu expectativa es incorrecta. En Java, el envío de métodos dinámicos ocurre solo para el objeto en el que se llama al método, no para los tipos de parámetros de los métodos sobrecargados.

Citando la especificación del lenguaje Java :

Cuando se invoca un método (§15.12), el número de argumentos reales (y cualquier argumento de tipo explícito) y los tipos de argumentos en tiempo de compilación se utilizan, en tiempo de compilación, para determinar la firma del método que se invocará ( §15.12.2). Si el método que se va a invocar es un método de instancia, el método real que se va a invocar se determinará en tiempo de ejecución, utilizando la búsqueda de métodos dinámicos (§15.12.4).

Michael Borgwardt
fuente
4
¿Puede explicar la especificación que citó por favor? Las dos frases parecen contradecirse. El ejemplo anterior usa métodos de instancia, pero el método que se invoca claramente no se determina en tiempo de ejecución.
Alex Worden
15
@Alex Worden: el tipo de tiempo de compilación de los parámetros del método se utiliza para determinar la firma del método a llamar, en este caso foo(Object). En tiempo de ejecución, la clase del objeto en el que se llama al método determina qué implementación de ese método se llama, teniendo en cuenta que puede ser una instancia de una subclase del tipo declarado que anula el método.
Michael Borgwardt
86

Como se mencionó anteriormente, la resolución de sobrecarga se realiza en tiempo de compilación.

Java Puzzlers tiene un buen ejemplo de eso:

Rompecabezas 46: El caso del constructor confuso

Este rompecabezas te presenta dos constructores confusos. El método principal invoca un constructor, pero ¿cuál? La salida del programa depende de la respuesta. ¿Qué imprime el programa, o incluso es 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);
    }
}

Solución 46: Caso del constructor confuso

... El proceso de resolución de sobrecargas de Java opera en dos fases. La primera fase selecciona todos los métodos o constructores que son accesibles y aplicables. La segunda fase selecciona el más específico de los métodos o constructores seleccionados en la primera fase. Un método o constructor es menos específico que otro si puede aceptar cualquier parámetro pasado al otro [JLS 15.12.2.5].

En nuestro programa, ambos constructores son accesibles y aplicables. El constructor Confusing (Object) acepta cualquier parámetro pasado a Confusing (doble []) , por lo que Confusing (Object) es menos específico. (Cada matriz doble es un Objeto , pero no todo Objeto es una matriz doble ). Por lo tanto, el constructor más específico es Confuso (doble []) , que explica la salida del programa.

Este comportamiento tiene sentido si pasa un valor de tipo double [] ; es contrario a la intuición si pasa null . La clave para entender este acertijo es que la prueba para qué método o constructor es más específico no usa los parámetros reales : los parámetros que aparecen en la invocación. Se utilizan solo para determinar qué sobrecargas son aplicables. Una vez que el compilador determina qué sobrecargas son aplicables y accesibles, selecciona la sobrecarga más específica, utilizando solo los parámetros formales: los parámetros que aparecen en la declaración.

Para invocar el constructor Confusing (Object) con un parámetro nulo , escriba nuevo Confusing ((Object) null) . Esto asegura que solo se aplique Confuso (Objeto) . De manera más general, para obligar al compilador a seleccionar una sobrecarga específica, convierta los parámetros reales en los tipos declarados de los parámetros formales.

denis.zhdanov
fuente
4
Espero que no sea demasiado tarde para decir: "una de las mejores explicaciones sobre SOF". Gracias :)
TheLostMind
5
Creo que si también agregamos el constructor 'private Confusing (int [] iArray)' no se compilará, ¿no es así? Porque ahora hay dos constructores con la misma especificidad.
Risser
Si uso tipos de retorno dinámicos como entrada de función, siempre usa el menos específico ... dijo que el método que se puede usar para todos los valores de retorno posibles ...
Kaiser
16

La capacidad de enviar una llamada a un método basado en tipos de argumentos se denomina envío múltiple . En Java, esto se hace con el patrón Visitor .

Sin embargo, dado que está tratando con Integersy Strings, no puede incorporar fácilmente este patrón (simplemente no puede modificar estas clases). Por lo tanto, un gigante switchen tiempo de ejecución del objeto será su arma preferida.

Anton Gogolev
fuente
11

En Java, el método a llamar (como en qué firma de método usar) se determina en tiempo de compilación, por lo que va con el tipo de tiempo de compilación.

El patrón típico para solucionar esto es verificar el tipo de objeto en el método con la firma del objeto y delegar en el método con una conversión.

    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 tiene muchos tipos y esto es inmanejable, entonces la sobrecarga de métodos probablemente no sea el enfoque correcto, sino que el método público debería simplemente tomar Object e implementar algún tipo de patrón de estrategia para delegar el manejo apropiado por tipo de objeto.

Yishai
fuente
4

Tuve un problema similar al llamar al constructor correcto de una clase llamada "Parameter" que podría tomar varios tipos básicos de Java como String, Integer, Boolean, Long, etc. Dada una matriz de objetos, quiero convertirlos en una matriz de mis objetos Parameter llamando al constructor más específico para cada Objeto en la matriz de entrada. También quería definir el parámetro del constructor (Object o) que arrojaría una IllegalArgumentException. Por supuesto, encontré que este método se invoca para cada Objeto en mi matriz.

La solución que utilicé fue buscar el constructor a través de la reflexión ...

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

¡No se requieren instancias desagradables, declaraciones de cambio o patrones de visitantes! :)

Alex Worden
fuente
2

Java mira el tipo de referencia cuando intenta determinar a qué método llamar. Si desea forzar su código, elija el método 'correcto', puede declarar sus campos como instancias del tipo específico:

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

También puede convertir sus parámetros como el tipo de parámetro:

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

Si hay una coincidencia exacta entre el número y los tipos de argumentos especificados en la llamada al método y la firma del método de un método sobrecargado, ese es el método que se invocará. Está utilizando referencias de objeto, por lo que Java decide en tiempo de compilación que para Object param, hay un método que acepta directamente Object. Así que llamó a ese método 3 veces.

Ashish Thukral
fuente