Vinculação dinâmica Java e substituição de método


89

Ontem tive uma entrevista técnica por telefone de duas horas (na qual passei, uhuu!), Mas esqueci completamente a seguinte pergunta sobre vinculação dinâmica em Java. E é duplamente intrigante porque eu costumava ensinar esse conceito para alunos de graduação quando era um TA há alguns anos, então a perspectiva de que eu lhes dei informações erradas é um pouco perturbadora ...

Aqui está o problema que me foi dado:

/* What is the output of the following program? */

public class Test {

  public boolean equals( Test other ) {
    System.out.println( "Inside of Test.equals" );
    return false;
  }

  public static void main( String [] args ) {
    Object t1 = new Test();
    Object t2 = new Test();
    Test t3 = new Test();
    Object o1 = new Object();

    int count = 0;
    System.out.println( count++ );// prints 0
    t1.equals( t2 ) ;
    System.out.println( count++ );// prints 1
    t1.equals( t3 );
    System.out.println( count++ );// prints 2
    t3.equals( o1 );
    System.out.println( count++ );// prints 3
    t3.equals(t3);
    System.out.println( count++ );// prints 4
    t3.equals(t2);
  }
}

Afirmei que a saída deveria ter sido duas instruções de impressão separadas de dentro do equals()método substituído : at t1.equals(t3)e t3.equals(t3). O último caso é bastante óbvio, e com o primeiro caso, embora t1tenha uma referência do tipo Object, ele é instanciado como tipo Test, portanto a vinculação dinâmica deve chamar a forma substituída do método.

Aparentemente não. Meu entrevistador me incentivou a executar o programa sozinho e, vejam só, havia apenas uma única saída do método substituído: na linha t3.equals(t3).

Minha pergunta então é: por quê? Como já mencionei, embora t1seja uma referência do tipo Object (então a vinculação estática invocaria o equals()método de Object ), a vinculação dinâmica deve ter o cuidado de invocar a versão mais específica do método com base no tipo instanciado da referência. o que estou perdendo?


Encontre minha postagem para esta resposta onde tentei o meu melhor para explicar com casos adicionais. Eu realmente aprecio suas contribuições :)
Devendra Lattu

Respostas:


81

Java usa vinculação estática para métodos sobrecarregados e vinculação dinâmica para os substituídos. Em seu exemplo, o método equals está sobrecarregado (tem um tipo de parâmetro diferente de Object.equals ()), portanto, o método chamado é vinculado ao tipo de referência em tempo de compilação.

Alguma discussão aqui

O fato de ser o método de igual não é muito relevante, a não ser que seja um erro comum sobrecarregá-lo em vez de substituí-lo, que você já conhece com base em sua resposta ao problema na entrevista.

Edit: uma boa descrição aqui também. Este exemplo mostra um problema semelhante relacionado ao tipo de parâmetro, mas causado pelo mesmo problema.

Acredito que se a vinculação fosse realmente dinâmica, qualquer caso em que o chamador e o parâmetro fossem uma instância de Test resultaria na chamada do método substituído. Portanto, t3.equals (o1) seria o único caso que não seria impresso.


Muitas pessoas apontam que ele está sobrecarregado e não sobrescrito, mas mesmo assim você esperaria que resolvesse o sobrecarregado corretamente. Até agora, sua postagem é a única que responde à pergunta corretamente, pelo que posso dizer.
Bill K

4
Meu erro foi omitir completamente o fato de que o método está realmente sobrecarregado em vez de substituído. Eu vi "equals ()" e imediatamente pensei que era herdado e substituído. Parece que eu, mais uma vez, acertei o conceito mais amplo e difícil, mas estraguei os detalhes simples. : P
Magsol

14
outro motivo para a existência da anotação @Override.
Matt

1
Repita depois de mim: "Java usa vinculação estática para métodos sobrecarregados e vinculação dinâmica para aqueles substituídos" - +1
Mr_and_Mrs_D

1
então me formei sem saber disso. Obrigado!
Atieh

25

O equalsmétodo de Testnão substitui o equalsmétodo de java.lang.Object. Veja o tipo de parâmetro! A Testclasse está sobrecarregada equalscom um método que aceita a Test.

Se o equalsmétodo tiver a intenção de substituir, ele deve usar a anotação @Override. Isso causaria um erro de compilação para apontar esse erro comum.


Sim, não tenho certeza de por que perdi aquele detalhe simples, mas crucial, mas é exatamente aí que estava o meu problema. Obrigado!
Magsol

+1 por ser a verdadeira resposta aos curiosos resultados do questionador
matt b

Encontre minha postagem para esta resposta onde tentei o meu melhor para explicar com casos adicionais. Eu realmente aprecio suas contribuições :)
Devendra Lattu

6

Curiosamente, no código Groovy (que poderia ser compilado em um arquivo de classe), todas as chamadas, exceto uma, executariam a instrução print. (Aquele que compara um Teste a um Objeto claramente não chamará a função Test.equals (Teste).) Isso ocorre porque o groovy DOES faz tipagem completamente dinâmica. Isso é particularmente interessante porque não possui variáveis ​​que sejam explicitamente digitadas dinamicamente. Eu li em alguns lugares que isso é considerado prejudicial, pois os programadores esperam que o bacana faça a coisa do java.


1
Infelizmente, o preço que Groovy paga por isso é um grande impacto no desempenho, já que toda invocação de método usa reflexão. Esperar que um idioma funcione exatamente da mesma forma que outro é geralmente considerado prejudicial. É preciso estar ciente das diferenças.
Joachim Sauer

Deve ser bom e rápido com invokedynamic no JDK7 (ou mesmo usando uma técnica de implementação semelhante hoje).
Tom Hawtin - tackline

5

Java não oferece suporte a covariância em parâmetros, apenas em tipos de retorno.

Em outras palavras, embora seu tipo de retorno em um método de substituição possa ser um subtipo do que era no substituído, isso não é verdadeiro para os parâmetros.

Se seu parâmetro para igual em Object for Object, colocar um equals com qualquer outra coisa em uma subclasse será um método sobrecarregado, não um método sobrescrito. Portanto, a única situação em que esse método será chamado é quando o tipo estático do parâmetro for Teste, como no caso de T3.

Boa sorte com o processo de entrevista de emprego! Eu adoraria ser entrevistado em uma empresa que faz esse tipo de pergunta em vez das perguntas usuais de algo / estruturas de dados que ensino a meus alunos.


1
Você quer dizer parâmetros contravariantes.
Tom Hawtin - tackline

De alguma forma, ignorei completamente o fato de que diferentes parâmetros de método criam intrinsecamente um método sobrecarregado, não um substituído. Oh, não se preocupe, havia questões de algo / estruturas de dados também. : P E obrigado pela boa sorte, vou precisar! :)
Magsol

4

Acho que a chave está no fato de que o método equals () não está de acordo com o padrão: ele recebe outro objeto Test, não o objeto Object e, portanto, não substitui o método equals (). Isso significa que você realmente o sobrecarregou para fazer algo especial quando recebeu o objeto Test, ao mesmo tempo que deu a ele chamadas de objeto Object Object.equals (Object o). Examinar o código por meio de qualquer IDE deve mostrar dois métodos equals () para Teste.


Isso, e a maioria das respostas, estão perdendo o ponto. A questão não é sobre o fato de que a sobrecarga está sendo usada em vez de substituir. É por isso que não é o método sobrecarregado usado para t1.equals (t3), quando t1 é declarado como Object, mas inicializado para Test.
Robin

4

O método está sobrecarregado em vez de substituído. Equals sempre leva um objeto como parâmetro.

btw, você tem um item sobre isso no java eficaz de Bloch (que você deve possuir).


Java eficaz de Joshua Bloch?
DJClayworth

Eficaz, sim, estava pensando em outra coisa enquanto digitava: D
Gilles

4

Algumas notas em Dynamic Binding (DD) e Static Binding̣̣̣ (SB) depois de pesquisar um pouco:

1. Cronometragem de execução : (Ref.1)

  • DB: em tempo de execução
  • SB: tempo do compilador

2. Usado para :

  • DB: substituindo
  • SB: sobrecarga (estático, privado, final) (Ref.2)

Referência:

  1. Execute o resolvedor médio que método prefere usar
  2. Porque não pode substituir o método com o modificador static, private ou final
  3. http://javarevisited.blogspot.com/2012/03/what-is-static-and-dynamic-binding-in.html

2

Se outro método for adicionado que substitui em vez de sobrecarregar, isso explicará a chamada de vinculação dinâmica em tempo de execução.

/ * Qual é a saída do programa a seguir? * /

public class DynamicBinding {
    public boolean equals(Test other) {
        System.out.println("Inside of Test.equals");
        return false;
    }

    @Override
    public boolean equals(Object other) {
        System.out.println("Inside @override: this is dynamic binding");
        return false;
    }

    public static void main(String[] args) {
        Object t1 = new Test();
        Object t2 = new Test();
        Test t3 = new Test();
        Object o1 = new Object();

        int count = 0;
        System.out.println(count++);// prints 0
        t1.equals(t2);
        System.out.println(count++);// prints 1
        t1.equals(t3);
        System.out.println(count++);// prints 2
        t3.equals(o1);
        System.out.println(count++);// prints 3
        t3.equals(t3);
        System.out.println(count++);// prints 4
        t3.equals(t2);
    }
}


0

A resposta à pergunta "por quê?" é assim que a linguagem Java é definida.

Para citar o artigo da Wikipedia sobre covariância e contravariância :

A covariância do tipo de retorno é implementada na linguagem de programação Java versão J2SE 5.0. Os tipos de parâmetro devem ser exatamente os mesmos (invariante) para a substituição do método, caso contrário, o método será sobrecarregado com uma definição paralela.

Outras línguas são diferentes.


Meu problema era aproximadamente equivalente a ver 3 + 3 e escrever 9, depois ver 1 + 1 e escrever 2. Eu entendo como a linguagem Java é definida; neste caso, por qualquer motivo, confundi completamente o método com algo que ele não era, embora tenha evitado esse erro em outro lugar no mesmo problema.
Magsol

0

É muito claro que não há conceito de substituição aqui. É uma sobrecarga de método. o Object()método da classe Object leva parâmetro de referência do tipo Object e este equal()método leva parâmetro de referência do tipo Test.


-1

Vou tentar explicar isso por meio de dois exemplos que são as versões estendidas de alguns dos exemplos que encontrei online.

public class Test {

    public boolean equals(Test other) {
        System.out.println("Inside of Test.equals");
        return false;
    }

    @Override
    public boolean equals(Object other) {
        System.out.println("Inside of Test.equals ot type Object");
        return false;
    }

    public static void main(String[] args) {
        Object t1 = new Test();
        Object t2 = new Test();
        Test t3 = new Test();
        Object o1 = new Object();

        int count = 0;
        System.out.println(count++); // prints 0
        o1.equals(t2);

        System.out.println("\n" + count++); // prints 1
        o1.equals(t3);

        System.out.println("\n" + count++);// prints 2
        t1.equals(t2);

        System.out.println("\n" + count++);// prints 3
        t1.equals(t3);

        System.out.println("\n" + count++);// prints 4
        t3.equals(o1);

        System.out.println("\n" + count++);// prints 5
        t3.equals(t3);

        System.out.println("\n" + count++);// prints 6
        t3.equals(t2);
    }
}

Aqui, para linhas com valores de contagem 0, 1, 2 e 3; temos referência de Object para o1 e t1 no equals()método. Portanto, em tempo de compilação, o equals()método do arquivo Object.class será limitado.

No entanto, embora a referência de t1 seja Object , ela tem inicialização da classe Test .
Object t1 = new Test();.
Portanto, em tempo de execução, ele chama o public boolean equals(Object other)que é um

método sobrescrito

. insira a descrição da imagem aqui

Agora, para valores de contagem como 4 e 6, é novamente direto que t3 que tem referência e inicialização de Teste está chamando o equals()método com parâmetro como referências de objeto e é um

método sobrecarregado

ESTÁ BEM!

Novamente, para entender melhor qual método o compilador chamará, apenas clique no método e o Eclipse destacará os métodos de tipos semelhantes que ele pensa que chamarão no momento da compilação. Se ele não for chamado em tempo de compilação, esses métodos são um exemplo de substituição de método.

insira a descrição da imagem aqui

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.