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

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


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.


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.


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.


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! :)


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

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.