Comment le polymorphisme est implémenté dans la JVM

Une traduction de cet article a été préparée spécialement pour les étudiants du cours Java Developer.





Dans mon article précédent Tout sur la surcharge de méthode vs la substitution de méthode , nous avons examiné les rÚgles et les différences de surcharge et de substitution de méthode. Dans cet article, nous verrons comment la surcharge et le remplacement de méthode sont gérés dans la JVM.

Par exemple, prenez les cours de l'article précédent: le parent Mammal (mammifÚre) et l'enfant Human (humain).

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

Nous pouvons considérer la question du polymorphisme de deux cÎtés: du «logique» et du «physique». Voyons d'abord le cÎté logique du problÚme.

Point de vue logique


D'un point de vue logique, au stade de la compilation, la méthode appelée est considérée comme liée au type de référence. Mais au moment de l'exécution, la méthode de l'objet référencé sera appelée.

Par exemple, dans la ligne humanMammal.speak(); le compilateur pense que Mammal.speak() sera appelé, car humanMammal déclaré comme Mammal . Mais au moment de l'exécution, la JVM saura que humanMammal contient un objet Human et invoquera en fait la méthode Human.speak() .

C'est assez simple tant que nous restons à un niveau conceptuel. Mais comment la JVM gÚre-t-elle tout cela en interne? Comment la JVM calcule-t-elle la méthode à appeler?

Nous savons également que les méthodes surchargées ne sont pas appelées polymorphes et se résolvent au moment de la compilation. Bien que parfois la surcharge de méthode soit appelée polymorphisme au moment de la compilation ou liaison précoce / statique .

Les méthodes remplacées (remplacement) sont résolues au moment de l'exécution car le compilateur ne sait pas s'il existe des méthodes remplacées dans l'objet affecté au lien.

Point de vue physique


Dans cette section, nous essaierons de trouver des preuves «physiques» pour toutes les déclarations ci-dessus. Pour ce faire, regardez le bytecode que nous pouvons obtenir en exécutant javap -verbose OverridingInternalExample . Le paramÚtre -verbose nous permettra d'obtenir un bytecode plus intuitif correspondant à notre programme java.

La commande ci-dessus affichera deux sections de bytecode.

1. Le pool de constantes . Il contient presque tout ce qui est nécessaire pour exécuter le programme. Par exemple, les références de méthode ( #Methodref ), les classes ( #Class ), les littéraux de chaßne ( #String ).



2. Le bytecode du programme. Instructions de bytecode exécutables.



Pourquoi la surcharge de méthode est appelée liaison statique


Dans l'exemple ci-dessus, le compilateur pense que la méthode humanMammal.speak() sera appelée à partir de la classe Mammal , bien qu'au moment de l'exécution, elle sera appelée à partir de l'objet référencé dans humanMammal - ce sera un objet de la classe Human .

En regardant notre code et le résultat javap , nous voyons que différents bytecodes sont utilisés pour appeler les méthodes humanMammal.speak() , human.speak() et human.speak("Hindi") , car le compilateur peut les distinguer en fonction de la référence de classe .

Ainsi, en cas de surcharge d'une méthode, le compilateur est capable d'identifier des instructions de bytecode et des adresses de méthode au moment de la compilation. C'est pourquoi cela est appelé liaison statique ou polymorphisme au moment de la compilation.

Pourquoi la substitution de méthode est appelée liaison dynamique


Pour appeler les anyMammal.speak() et humanMammal.speak() , le bytecode est le mĂȘme, car du point de vue du compilateur, les deux mĂ©thodes sont appelĂ©es pour la classe Mammal :

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

Alors maintenant, la question est, si les deux appels ont le mĂȘme bytecode, comment la JVM sait-elle quelle mĂ©thode appeler?

La rĂ©ponse est cachĂ©e dans le bytecode lui-mĂȘme et dans l'instruction invokevirtual . Selon la spĂ©cification JVM (note du traducteur: rĂ©fĂ©rence Ă  la spĂ©cification JVM 2.11.8 ) :
L'instruction invokevirtual appelle la méthode d'instance par répartition sur le type (virtuel) de l'objet. Il s'agit de la répartition normale des méthodes dans le langage de programmation Java.
La JVM utilise l' invokevirtual pour invoquer en Java des mĂ©thodes Ă©quivalentes aux mĂ©thodes virtuelles C ++. En C ++, pour remplacer une mĂ©thode dans une autre classe, la mĂ©thode doit ĂȘtre dĂ©clarĂ©e virtuelle. Mais en Java, par dĂ©faut, toutes les mĂ©thodes sont virtuelles (Ă  l'exception des mĂ©thodes finales et statiques), donc dans la classe enfant, nous pouvons remplacer n'importe quelle mĂ©thode.

L'instruction invokevirtual prend un pointeur sur la méthode à appeler (# 4 est l'index dans le pool constant).

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

Mais la référence # 4 se réfÚre en outre à une autre méthode et classe.

 #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 

Tous ces liens sont utilisés ensemble pour obtenir une référence à la méthode et à la classe dans laquelle se trouve la méthode souhaitée. Ceci est également mentionné dans la spécification JVM ( note du traducteur: référence à la spécification JVM 2.7 ):
La machine virtuelle Java ne nécessite aucune structure interne spécifique d'objets.
Dans certaines implĂ©mentations Java Virtual Machine d'Oracle, une rĂ©fĂ©rence Ă  une instance de classe est une rĂ©fĂ©rence Ă  un gestionnaire, qui se compose lui-mĂȘme d'une paire de liens: l'un pointe vers une table de mĂ©thodes d'objet et un pointeur vers un objet Class reprĂ©sentant le type de l'objet, et l'autre vers la zone donnĂ©es sur le tas contenant des donnĂ©es d'objet.

Cela signifie que chaque variable de référence contient deux pointeurs masqués:

  1. Un pointeur vers une table qui contient les méthodes de l'objet et un pointeur vers l'objet Class , par exemple, [speak(), speak(String) Class object]
  2. Un pointeur vers la mémoire sur le tas alloué pour les données d'objet, telles que les valeurs de champ d'objet.

Mais encore une fois la question se pose: comment fonctionne invokevirtual avec cela? Malheureusement, personne ne peut rĂ©pondre Ă  cette question, car tout dĂ©pend de la mise en Ɠuvre de la JVM et varie de JVM Ă  JVM.

Du raisonnement ci-dessus, nous pouvons conclure qu'une référence à un objet contient indirectement un lien / pointeur vers une table qui contient toutes les références aux méthodes de cet objet. Java a emprunté ce concept à C ++. Cette table est connue sous différents noms, tels que table de méthode virtuelle (VMT), table de fonction virtuelle (vftable), table virtuelle (vtable), table de répartition .

Nous ne pouvons pas ĂȘtre sĂ»rs de la façon dont vtable est implĂ©mentĂ© en Java, car cela dĂ©pend de la JVM spĂ©cifique. Mais nous pouvons nous attendre Ă  ce que la stratĂ©gie soit Ă  peu prĂšs la mĂȘme qu'en C ++, oĂč vtable est une structure de type tableau qui contient les noms de mĂ©thode et leurs rĂ©fĂ©rences. Chaque fois que la JVM essaie d'exĂ©cuter une mĂ©thode virtuelle, elle demande son adresse dans la table virtuelle.

Pour chaque classe, il n'y a qu'une seule table virtuelle, ce qui signifie que la table est unique et identique pour tous les objets de la classe, similaire Ă  l'objet Class. Les objets de classe sont abordĂ©s plus en dĂ©tail dans les articles Pourquoi une classe Java externe ne peut pas ĂȘtre statique et Pourquoi Java est-il purement orientĂ© objet ou pourquoi pas ?

Ainsi, il n'y a qu'une seule table virtuelle pour la classe Object , qui contient les 11 méthodes (si registerNatives ne registerNatives pas prises en compte) et les liens correspondant à leur implémentation.



Lorsque la JVM charge la classe Mammal en mĂ©moire, elle crĂ©e un objet Class pour elle et crĂ©e une vtable qui contient toutes les mĂ©thodes de la vtable de la classe Object avec les mĂȘmes rĂ©fĂ©rences (puisque Mammal ne remplace pas les mĂ©thodes d' Object ) et ajoute une nouvelle entrĂ©e pour la mĂ©thode speak() .



Ensuite, la classe de la classe Human entre en jeu et la JVM copie toutes les entrées de la vtable de la classe Mammal dans la vtable de la classe Human et ajoute une nouvelle entrée pour la version surchargée de speak(String) .

La JVM sait que la classe Human a remplacĂ© deux mĂ©thodes: toString() d' Object et speak() de Mammal . Maintenant, pour ces mĂ©thodes, au lieu de crĂ©er de nouveaux enregistrements avec des liens mis Ă  jour, la machine virtuelle Java modifie les liens vers les mĂ©thodes existantes dans le mĂȘme index dans lequel elles Ă©taient prĂ©cĂ©demment prĂ©sentes et conserve les mĂȘmes noms de mĂ©thode.



L'instruction invokevirtual amÚne la JVM à traiter la valeur dans la référence à la méthode # 4 non pas comme une adresse, mais comme le nom de la méthode à rechercher dans la table virtuelle pour l'objet actuel.
J'espÚre qu'il est désormais plus clair comment la JVM utilise le pool constant et la table de méthodes virtuelles pour déterminer la méthode à appeler.
Vous pouvez trouver l'exemple de code dans le référentiel Github .

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


All Articles