Uma tradução deste artigo foi preparada especificamente para estudantes do curso Java Developer.
No meu artigo anterior,
Tudo sobre sobrecarga de método versus substituição de método , examinamos as regras e diferenças de sobrecarga e substituição de método. Neste artigo, veremos como a sobrecarga e a substituição de métodos são tratadas dentro da JVM.
Por exemplo, faça as aulas do artigo anterior: o pai
Mammal
(mamífero) e o filho
Human
(humano).
public class OverridingInternalExample { private static class Mammal { public void speak() { System.out.println("ohlllalalalalalaoaoaoa"); } } private static class Human extends Mammal { @Override public void speak() { System.out.println("Hello"); }
Podemos examinar a questão do polimorfismo de dois lados: do "lógico" e do "físico". Vamos primeiro olhar para o lado lógico da questão.
Ponto de vista lógico
Do ponto de vista lógico, no estágio de compilação, o método chamado é considerado relacionado ao tipo de referência. Mas em tempo de execução, o método do objeto referenciado será chamado.
Por exemplo, na linha
humanMammal.speak();
o compilador pensa que
Mammal.speak()
será chamado, pois
humanMammal
declarado como
Mammal
. Porém, em tempo de execução, a JVM saberá que
humanMammal
contém um objeto
Human
e, na verdade,
Human.speak()
o método
Human.speak()
.
É tudo muito simples, desde que permaneçamos em um nível conceitual. Mas como a JVM lida com tudo isso internamente? Como a JVM calcula qual método deve ser chamado?
Também sabemos que métodos sobrecarregados não são chamados polimórficos e são resolvidos em tempo de compilação. Embora às vezes a sobrecarga de método seja chamada
de polimorfismo em tempo de compilação ou ligação estática / precoce .
Os métodos substituídos (substituição) são resolvidos no tempo de execução porque o compilador não sabe se existem métodos substituídos no objeto atribuído ao link.
Ponto de vista físico
Nesta seção, tentaremos encontrar evidências "físicas" para todas as declarações acima. Para fazer isso, observe o
javap -verbose OverridingInternalExample
que podemos obter executando o
javap -verbose OverridingInternalExample
. O parâmetro
-verbose
nos permitirá obter um bytecode mais intuitivo correspondente ao nosso programa java.
O comando acima mostrará duas seções do bytecode.
1. O conjunto de constantes . Ele contém quase tudo o que é necessário para executar o programa. Por exemplo, referências de método (
#Methodref
), classes (
#Class
), literais de string (
#String
).
2. O bytecode do programa. Instruções de bytecode executáveis.

Por que a sobrecarga de método é chamada de ligação estática
No exemplo acima, o compilador pensa que o método
humanMammal.speak()
será chamado da classe
Mammal
, embora em tempo de execução seja chamado a partir do objeto referenciado em
humanMammal
- será um objeto da classe
Human
.
Observando nosso código e o resultado do
javap
, vemos que diferentes
javap
código são usados para chamar os métodos
humanMammal.speak()
,
human.speak()
e
human.speak("Hindi")
, pois o compilador pode distingui-los com base na referência de classe .
Portanto, no caso de uma sobrecarga de método, o compilador é capaz de identificar instruções de bytecode e endereços de método em tempo de compilação. É por isso que isso é chamado de
ligação estática ou polimorfismo em tempo de compilação.Por que a substituição de método é chamada de ligação dinâmica
Para chamar os
anyMammal.speak()
e
humanMammal.speak()
, o bytecode é o mesmo, pois, do ponto de vista do compilador, os dois métodos são chamados para a classe
Mammal
:
invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
Portanto, agora a questão é: se as duas chamadas tiverem o mesmo bytecode, como a JVM saberá qual método chamar?
A resposta está oculta no próprio bytecode e na instrução
invokevirtual
. De acordo com a especificação da JVM
(nota do tradutor: referência à especificação da JVM 2.11.8 ) :
A instrução invokevirtual chama o método de instância através do envio no tipo (virtual) do objeto. Esse é o envio normal de métodos na linguagem de programação Java.
A JVM usa a
invokevirtual
para chamar nos métodos Java equivalentes aos métodos virtuais C ++. No C ++, para substituir um método em outra classe, o método deve ser declarado como virtual. Porém, em Java, por padrão, todos os métodos são virtuais (exceto os métodos final e estático); portanto, na classe filho, podemos substituir qualquer método.
A instrução
invokevirtual
leva um ponteiro para o método a ser chamado (# 4 é o índice no pool constante).
invokevirtual #4
Mas a referência # 4 refere-se ainda a outro método e classe.
#4 = Methodref #2.#27
Todos esses links são usados juntos para obter uma referência ao método e à classe na qual o método desejado está localizado. Isso também é mencionado na especificação da JVM (
nota do tradutor: referência à especificação da JVM 2.7 ):
A Java Virtual Machine não requer nenhuma estrutura interna específica de objetos.
Em algumas implementações Java da Oracle Virtual Machine, uma referência a uma instância de classe é uma referência a um manipulador, que consiste em um par de links: um aponta para uma tabela de métodos de objeto e um ponteiro para um objeto Class que representa o tipo do objeto e o outro para a área dados no heap contendo dados do objeto.
Isso significa que cada variável de referência contém dois ponteiros ocultos:
- Um ponteiro para uma tabela que contém os métodos do objeto e um ponteiro para o objeto
Class
, por exemplo, [speak(), speak(String) Class object]
- Um ponteiro para a memória no heap alocado para dados do objeto, como valores de campo do objeto.
Mas, novamente, surge a pergunta: como o
invokevirtual
trabalha com isso? Infelizmente, ninguém pode responder a essa pergunta, porque tudo depende da implementação da JVM e varia de JVM para JVM.
Pelo raciocínio acima, podemos concluir que uma referência a um objeto indiretamente contém um link / ponteiro para uma tabela que contém todas as referências aos métodos desse objeto. Java emprestou esse conceito do C ++. Essa tabela é conhecida por vários nomes, como
tabela de método virtual (VMT), tabela de função virtual (vftable), tabela virtual (vtable), tabela de despacho .
Não podemos ter certeza de como a vtable é implementada em Java, porque depende da JVM específica. Mas podemos esperar que a estratégia seja a mesma que em C ++, em que vtable é uma estrutura semelhante a uma matriz que contém nomes de métodos e suas referências. Sempre que a JVM tenta executar um método virtual, solicita seu endereço na vtable.
Para cada classe, existe apenas uma vtable, o que significa que a tabela é única e igual para todos os objetos da classe, semelhante ao objeto Class. Os objetos de classe são discutidos em mais detalhes nos artigos
Por que uma classe Java externa não pode ser estática e
Por que Java é uma linguagem puramente orientada a objetos ou Por que não .
Portanto, existe apenas uma tabela de tabelas para a classe
Object
, que contém todos os 11 métodos (se
registerNatives
não
registerNatives
levados em consideração) e links correspondentes à sua implementação.

Quando a JVM carrega a classe Mammal na memória, cria um objeto
Class
para ela e cria uma tabela que contém todos os métodos da tabela de
Object
com as mesmas referências (já que o
Mammal
não substitui os métodos de
Object
) e adiciona uma nova entrada para o método
speak()
.

Em seguida, a classe da classe
Human
entra, e a JVM copia todas as entradas da vtable da classe
Mammal
para a vtable da classe
Human
e adiciona uma nova entrada para a versão sobrecarregada de
speak(String)
.
A JVM sabe que a classe
Human
substituiu dois métodos:
toString()
de
Object
e
speak()
de
Mammal
. Agora, para esses métodos, em vez de criar novos registros com links atualizados, a JVM alterará os links para métodos existentes no mesmo índice em que estavam presentes anteriormente e manterá os mesmos nomes de métodos.

A instrução
invokevirtual
faz com que a JVM processe o valor na referência ao método # 4 não como um endereço, mas como o nome do método a ser procurado na vtable para o objeto atual.
Espero que agora fique mais claro como a JVM usa o pool constante e a tabela de métodos virtuais para determinar qual método chamar.
Você pode encontrar o código de amostra no repositório do
Github .