Seleção de método sobrecarregada com base no tipo real do parâmetro

115

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

Isso imprime foo(Object o)três vezes. Espero que a seleção do método leve em consideração o tipo de parâmetro real (não o declarado). Estou esquecendo de algo? Existe uma maneira de modificar este código para que ele seja impresso foo(12), foo("foobar")e foo(Object o)?

Sergey Mikhanov
fonte

Respostas:

96

Espero que a seleção do método leve em consideração o tipo de parâmetro real (não o declarado). Estou esquecendo de algo?

Sim. Sua expectativa está errada. Em Java, o despacho de método dinâmico acontece apenas para o objeto em que o método é chamado, não para os tipos de parâmetro de métodos sobrecarregados.

Citando a especificação da linguagem Java :

Quando um método é invocado (§15.12), o número de argumentos reais (e quaisquer argumentos de tipo explícito) e os tipos de tempo de compilação dos argumentos são usados, em tempo de compilação, para determinar a assinatura do método que será invocado ( §15.12.2). Se o método a ser invocado for um método de instância, o método real a ser invocado será determinado em tempo de execução, usando a pesquisa de método dinâmica (§15.12.4).

Michael Borgwardt
fonte
4
Você pode explicar a especificação que você citou, por favor. As duas frases parecem contradizer-se. O exemplo acima usa métodos de instância, mas o método que está sendo invocado claramente não está sendo determinado em tempo de execução.
Alex Worden
15
@Alex Worden: o tipo de tempo de compilação dos parâmetros do método é utilizado para determinar a assinatura do método a ser chamado, neste caso foo(Object). Em tempo de execução, a classe do objeto no qual o método é chamado determina qual implementação desse método é chamada, levando em consideração que pode ser uma instância de uma subclasse do tipo declarado que sobrescreve o método.
Michael Borgwardt
86

Como mencionado antes, a resolução de sobrecarga é executada em tempo de compilação.

Java Puzzlers tem um bom exemplo para isso:

Quebra-cabeça 46: O Caso do Construtor Confuso

Este quebra-cabeça apresenta dois construtores confusos. O método principal invoca um construtor, mas qual? A saída do programa depende da resposta. O que o programa imprime ou é mesmo 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);
    }
}

Solução 46: Caso do Construtor Confuso

... O processo de resolução de sobrecarga do Java opera em duas fases. A primeira fase seleciona todos os métodos ou construtores que são acessíveis e aplicáveis. A segunda fase seleciona o mais específico dos métodos ou construtores selecionados na primeira fase. Um método ou construtor é menos específico do que outro se pode aceitar quaisquer parâmetros passados ​​para o outro [JLS 15.12.2.5].

Em nosso programa, ambos os construtores são acessíveis e aplicáveis. O construtor Confusing (Object) aceita qualquer parâmetro passado para Confusing (double []) , portanto, Confusing (Object) é menos específico. (Cada matriz dupla é um objeto , mas nem todo objeto é uma matriz dupla .) O construtor mais específico é, portanto, Confuso (double []) , o que explica a saída do programa.

Este comportamento faz sentido se você passar um valor do tipo double [] ; é contra-intuitivo se você passar null . A chave para entender este quebra-cabeça é que o teste para qual método ou construtor é mais específico não usa os parâmetros reais : os parâmetros que aparecem na invocação. Eles são usados ​​apenas para determinar quais sobrecargas são aplicáveis. Uma vez que o compilador determina quais sobrecargas são aplicáveis ​​e acessíveis, ele seleciona a sobrecarga mais específica, usando apenas os parâmetros formais: os parâmetros que aparecem na declaração.

Para invocar o construtor Confusing (Object) com um parâmetro nulo , escreva novo Confusing ((Object) null) . Isso garante que apenas Confuso (Objeto) seja aplicável. Mais geralmente, para forçar o compilador a selecionar uma sobrecarga específica, lance os parâmetros reais para os tipos declarados dos parâmetros formais.

denis.zhdanov
fonte
4
Espero que não seja tarde demais para dizer - "uma das melhores explicações sobre SOF". Obrigado :)
TheLostMind
5
Acredito que se também adicionássemos o construtor 'private Confusing (int [] iArray)' ele não conseguiria compilar, não é? Porque agora existem dois construtores com a mesma especificidade.
Risser
Se eu usar tipos de retorno dinâmico como entrada de função, ele sempre usará o menos específico ... disse que o método pode ser usado para todos os valores de retorno possíveis ...
kaiser
16

A capacidade de despachar uma chamada para um método com base em tipos de argumentos é chamada de despacho múltiplo . Em Java, isso é feito com o padrão Visitor .

No entanto, como você está lidando com Integers e Strings, não pode incorporar facilmente esse padrão (você simplesmente não pode modificar essas classes). Assim, um gigante switchem tempo de execução de objeto será sua arma de escolha.

Anton Gogolev
fonte
11

Em Java, o método a ser chamado (como em qual assinatura de método usar) é determinado em tempo de compilação, portanto, acompanha o tipo de tempo de compilação.

O padrão típico para contornar isso é verificar o tipo de objeto no método com a assinatura Object e delegar ao método com uma conversão.

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

Se você tiver muitos tipos e isso não for gerenciável, a sobrecarga de método provavelmente não é a abordagem certa, em vez disso, o método público deve apenas pegar Object e implementar algum tipo de padrão de estratégia para delegar a manipulação apropriada por tipo de objeto.

Yishai
fonte
4

Tive um problema semelhante ao chamar o construtor certo de uma classe chamada "Parameter" que poderia receber vários tipos básicos de Java, como String, Integer, Boolean, Long, etc. Dado um array de objetos, quero convertê-los em um array de meus objetos Parameter chamando o construtor mais específico para cada Object na matriz de entrada. Eu também queria definir o parâmetro do construtor (Object o) que lançaria uma IllegalArgumentException. É claro que descobri que esse método está sendo invocado para cada objeto em meu array.

A solução que usei foi procurar o construtor por meio de reflexão ...

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

Nenhuma instância feia, instruções switch ou padrão de visitante necessário! :)

Alex Worden
fonte
2

Java examina o tipo de referência ao tentar determinar qual método chamar. Se você quiser forçar o seu código, escolha o método 'certo', você pode declarar seus campos como instâncias do tipo específico:

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

Você também pode lançar seus parâmetros como o tipo do parâmetro:

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

Se houver uma correspondência exata entre o número e os tipos de argumentos especificados na chamada do método e a assinatura do método de um método sobrecarregado, esse é o método que será chamado. Você está usando referências de objeto, portanto, java decide em tempo de compilação que, para o parâmetro de objeto, há um método que aceita diretamente o objeto. Por isso, chamou esse método 3 vezes.

Ashish Thukral
fonte