Cómo se implementa el polimorfismo dentro de la JVM

Se ha preparado una traducción de este artículo específicamente para estudiantes en el curso de Desarrollador Java.





En mi artículo anterior Everything About Method Overloading vs Method Overriding , observamos las reglas y diferencias de la sobrecarga y la anulación de métodos. En este artículo, veremos cómo se manejan la sobrecarga y la anulación de métodos dentro de la JVM.

Por ejemplo, tome las clases del artículo anterior: el padre Mammal (mamífero) y el niño 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"); } //   speak() public void speak(String language) { if (language.equals("Hindi")) System.out.println("Namaste"); else System.out.println("Hello"); } @Override public String toString() { return "Human Class"; } } //           public static void main(String[] args) { Mammal anyMammal = new Mammal(); anyMammal.speak(); // Output - ohlllalalalalalaoaoaoa // 10: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V Mammal humanMammal = new Human(); humanMammal.speak(); // Output - Hello // 23: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V Human human = new Human(); human.speak(); // Output - Hello // 36: invokevirtual #7 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:()V human.speak("Hindi"); // Output - Namaste // 42: invokevirtual #9 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:(Ljava/lang/String;)V } } 

Podemos ver la cuestión del polimorfismo desde dos lados: desde el "lógico" y el "físico". Primero veamos el lado lógico del problema.

Punto de vista lógico


Desde un punto de vista lógico, en la etapa de compilación, el método llamado se considera relacionado con el tipo de referencia. Pero en tiempo de ejecución, se llamará al método del objeto al que se hace referencia.

Por ejemplo, en la línea humanMammal.speak(); el compilador cree que se Mammal.speak() , ya que humanMammal declara como Mammal . Pero en tiempo de ejecución, la JVM sabrá que humanMammal contiene un objeto Human y en realidad invocará el método Human.speak() .

Todo es bastante simple siempre y cuando nos mantengamos en un nivel conceptual. Pero, ¿cómo maneja la JVM todo esto internamente? ¿Cómo calcula la JVM qué método debe llamarse?

También sabemos que los métodos sobrecargados no se denominan polimórficos y se resuelven en tiempo de compilación. Aunque a veces la sobrecarga de métodos se denomina polimorfismo en tiempo de compilación o enlace temprano / estático .

Los métodos anulados (anulación) se resuelven en tiempo de ejecución porque el compilador no sabe si hay métodos anulados en el objeto asignado al enlace.

Punto de vista físico


En esta sección, trataremos de encontrar evidencia "física" para todas las declaraciones anteriores. Para hacer esto, mire el código de bytes que podemos obtener ejecutando javap -verbose OverridingInternalExample . El parámetro -verbose nos permitirá obtener un bytecode más intuitivo correspondiente a nuestro programa java.

El comando anterior mostrará dos secciones de bytecode.

1. El grupo de constantes . Contiene casi todo lo necesario para ejecutar el programa. Por ejemplo, referencias de métodos ( #Methodref ), clases ( #Class ), literales de cadena ( #String ).



2. El código de bytes del programa. Instrucciones ejecutables de bytecode.



¿Por qué la sobrecarga de métodos se llama enlace estático?


En el ejemplo anterior, el compilador cree que el método humanMammal.speak() desde la clase Mammal , aunque en tiempo de ejecución se humanMammal desde el objeto al que se hace referencia en humanMammal , será un objeto de la clase Human .

Mirando nuestro código y el resultado de javap , vemos que se usa un humanMammal.speak() diferente para llamar a los métodos humanMammal.speak() , human.speak() y human.speak("Hindi") , ya que el compilador puede distinguirlos en función de la referencia de clase .

Por lo tanto, en el caso de una sobrecarga de método, el compilador puede identificar las instrucciones de código de bytes y las direcciones de método en tiempo de compilación. Es por eso que esto se llama enlace estático o polimorfismo en tiempo de compilación.

Por qué la anulación de métodos se llama enlace dinámico


Para llamar a los anyMammal.speak() y humanMammal.speak() , el humanMammal.speak() es el mismo, ya que desde el punto de vista del compilador, ambos métodos son llamados para la clase Mammal :

 invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V 

Entonces, la pregunta es, si ambas llamadas tienen el mismo código de bytes, ¿cómo sabe la JVM a qué método llamar?

La respuesta está oculta en el invokevirtual y en la instrucción invokevirtual . De acuerdo con la especificación JVM (nota del traductor: referencia a la especificación JVM 2.11.8 ) :
La instrucción invokevirtual llama al método de instancia mediante el despacho en el tipo (virtual) del objeto. Este es el envío normal de métodos en el lenguaje de programación Java.
La JVM usa la invokevirtual para invocar en métodos Java equivalentes a los métodos virtuales C ++. En C ++, para anular un método en otra clase, el método debe declararse como virtual. Pero en Java, por defecto, todos los métodos son virtuales (excepto los métodos finales y estáticos), por lo que en la clase secundaria podemos anular cualquier método.

La instrucción invokevirtual toma un puntero al método que se llamará (# 4 es el índice en el grupo constante).

 invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V 

Pero la referencia # 4 se refiere además a otro método y clase.

 #4 = Methodref #2.#27 // org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V #2 = Class #25 // org/programming/mitra/exercises/OverridingInternalExample$Mammal #25 = Utf8 org/programming/mitra/exercises/OverridingInternalExample$Mammal #27 = NameAndType #35:#17 // speak:()V #35 = Utf8 speak #17 = Utf8 ()V 

Todos estos enlaces se utilizan juntos para obtener una referencia al método y la clase en la que se encuentra el método deseado. Esto también se menciona en la especificación JVM ( nota del traductor: referencia a la especificación 2.7 de JVM ):
La máquina virtual Java no requiere ninguna estructura interna específica de objetos.
En algunas implementaciones de Java Virtual Machine de Oracle, una referencia a una instancia de clase es una referencia a un controlador, que en sí consiste en un par de enlaces: uno apunta a una tabla de métodos de objetos y un puntero a un objeto de Clase que representa el tipo del objeto, y el otro al área datos en el montón que contiene datos de objetos.

Esto significa que cada variable de referencia contiene dos punteros ocultos:

  1. Un puntero a una tabla que contiene los métodos del objeto y un puntero al objeto Class , por ejemplo, [speak(), speak(String) Class object]
  2. Un puntero a la memoria en el montón asignado para datos de objeto, como los valores de campo de objeto.

Pero nuevamente surge la pregunta: ¿cómo funciona invokevirtual con esto? Desafortunadamente, nadie puede responder esta pregunta, porque todo depende de la implementación de la JVM y varía de JVM a JVM.

Del razonamiento anterior, podemos concluir que una referencia a un objeto contiene indirectamente un enlace / puntero a una tabla que contiene todas las referencias a los métodos de este objeto. Java tomó prestado este concepto de C ++. Esta tabla se conoce por varios nombres, como tabla de método virtual (VMT), tabla de función virtual (vftable), tabla virtual (vtable), tabla de despacho .

No podemos estar seguros de cómo se implementa vtable en Java, porque depende de la JVM específica. Pero podemos esperar que la estrategia sea casi la misma que en C ++, donde vtable es una estructura tipo matriz que contiene nombres de métodos y sus referencias. Cada vez que la JVM intenta ejecutar un método virtual, solicita su dirección en la tabla vtable.

Para cada clase, solo hay una vtable, lo que significa que la tabla es única e igual para todos los objetos de la clase, similar al objeto Class. Los objetos de clase se analizan con más detalle en los artículos Por qué una clase externa de Java no puede ser estática y Por qué Java es un lenguaje puramente orientado a objetos o por qué no .

Por lo tanto, solo hay una vtable para la clase Object , que contiene los 11 métodos (si no se tienen en cuenta registerNatives ) y los enlaces correspondientes a su implementación.



Cuando JVM carga la clase Mammal en la memoria, crea un objeto Class para él y crea una vtable que contiene todos los métodos de la vtable de la clase Object con las mismas referencias (ya que Mammal no anula los métodos desde Object ) y agrega una nueva entrada para el método speak() .



Luego entra la clase de la clase Human , y la JVM copia todas las entradas de la tabla de la clase Mammal a la tabla de la clase Human y agrega una nueva entrada para la versión sobrecargada de speak(String) .

La JVM sabe que la clase Human ha anulado dos métodos: toString() desde Object y speak() desde Mammal . Ahora, para estos métodos, en lugar de crear nuevos registros con enlaces actualizados, la JVM cambiará los enlaces a los métodos existentes en el mismo índice en el que estaban presentes anteriormente y conservará los mismos nombres de métodos.



La instrucción invokevirtual hace que la JVM procese el valor en la referencia al método # 4 no como una dirección, sino como el nombre del método a buscar en la tabla para el objeto actual.
Espero que ahora esté más claro cómo la JVM usa el grupo constante y la tabla de métodos virtuales para determinar a qué método llamar.
Puede encontrar el código de muestra en el repositorio de Github .

Source: https://habr.com/ru/post/467197/


All Articles