Pourquoi une classe contenant un appel de méthode à une interface manquante dans du code inutilisé provoque-t-elle une erreur de chargement de classe Java?

Je constate un comportement de chargement de classe qui semble être incompatible avec les spécifications de la JVM et je me demande s’il s’agit d’un bogue. Ou sinon, espérer que quelqu’un puisse expliquer pourquoi.

L’exemple de code ci-dessous affiche simplement hello à partir de sa méthode principale. Il a une méthode inutilisée qui contient un appel de méthode à une méthode qui déclare qu’il prend un argument ‘C’ (qui est une interface).

Lorsque la commande principale est exécutée (sans A, B et C dans le chemin de classe), une erreur ClassNotFound est générée pour l’interface C. (Remarque: C n’est jamais réellement nécessaire au moment de l’exécution car il n’est référencé que dans une méthode qui ne s’exécute jamais).

Cela semble être une violation de la spécification JVM

La section 2.17.1 de la 2 e édition de la spécification Java VM indique:

La seule exigence concernant le moment de la résolution est que toutes les erreurs détectées lors de la résolution doivent être émises à un point du programme où le programme effectue une action qui pourrait, directement ou indirectement, nécessiter un lien avec la classe ou l’interface impliquée dans l’erreur.

La section 2.17.3 de la 2 e édition de la spécification Java VM indique:

Le langage de programmation Java permet une souplesse d’implémentation lors de la liaison des activités (et, en raison de la récursivité, du chargement), à condition que la sémantique du langage soit respectée, qu’une classe ou une interface soit complètement vérifiée et préparée avant son initialisation et que les erreurs détectées lors de la liaison sont renvoyées à un point du programme où le programme prend des mesures pouvant nécessiter une liaison avec la classe ou l’interface impliquée dans l’erreur .

Remarque: Si je change le type du paramètre de la définition en classe plutôt qu’en interface, le code se charge et s’exécute correctement.

/** * This version fails, the method call in neverCalled() is to a method whose * parameter definition is for an Interface */ public class Main { public void neverCalled(){ A a = new A(); B b = new B(); // B implements C //method takeInter is declared to take paramters of type Interface C //This code is causes a ClassNotFound error do be thrown when Main //is loaded if A, B, and C is not in the class path a.takeInter(b); } public static void main(Ssortingng[] args) { System.out.println("Hello..."); } } /** * This version runs, the method call in neverCalled() is to a method whose * parameter definition is for a Class */ public class Main { public void neverCalled(){ A a = new A(); B b = new B(); // B implements C //method takeInter is declared to take paramters of type Interface C //This code is causes a ClassNotFound error do be thrown when Main //is loaded if A, B, and C is not in the class path a.takeClass(b); } public static void main(Ssortingng[] args) { System.out.println("Hello..."); } } public class A { public void takeClass(B in){}; public void takeInter(C in){} } public class B implements C {} public interface C {} 

Ed,

Je n’essayais pas intentionnellement de sortir la citation de son contexte, j’ai extrait ce que je pensais être la partie pertinente. Merci de m’aider à essayer de comprendre cela.

Quoi qu’il en soit, les spécifications me semblent assez claires. Il dit que les erreurs doivent être jetées à un point et non par un point . J’accepte de lire les spécifications de la machine virtuelle après avoir lu ce qui suit au chapitre 8 de Inside The Java Virtual Machine. Cela a donc peut-être altéré mon interprétation.

De, http://www.artima.com/insidejvm/ed2/linkmod.html

Comme décrit au chapitre 7, “Durée de vie d’une classe”, différentes implémentations de la machine virtuelle Java sont autorisées à effectuer une résolution à différents moments de l’exécution d’un programme. Une implémentation peut choisir de tout lier au début en suivant toutes les références symboliques de la classe initiale, puis toutes les références symboliques des classes suivantes, jusqu’à ce que chaque référence symbolique ait été résolue. Dans ce cas, l’application serait complètement liée avant que sa méthode main () ne soit invoquée. Cette approche s’appelle résolution précoce. Sinon, une implémentation peut choisir d’attendre la toute dernière minute pour résoudre chaque référence symbolique. Dans ce cas, la machine virtuelle Java ne résoudra une référence symbolique que lorsqu’elle sera utilisée pour la première fois par le programme en cours d’exécution. Cette approche s’appelle résolution tardive. Les implémentations peuvent également utiliser une stratégie de résolution entre ces deux extrêmes.

Bien qu’une implémentation de machine virtuelle Java ait une certaine liberté dans le choix du moment où résoudre les références symboliques, chaque machine virtuelle Java doit donner l’impression extérieure qu’elle utilise une résolution tardive . Quelle que soit le moment où une machine virtuelle Java exécute sa résolution, elle génère toujours toute erreur résultant de la tentative de résolution d’une référence symbolique au moment de l’exécution du programme où la référence symbolique a réellement été utilisée pour la première fois . De cette manière, l’utilisateur aura toujours l’impression que la résolution a été retardée. Si une machine virtuelle Java effectue une résolution précoce et découvre qu’un fichier de classe est manquant, elle ne signalera pas le fichier de classe manquant en renvoyant l’erreur appropriée plus tard dans le programme lorsqu’un élément de ce fichier de classe est réellement utilisé. Si la classe n’est jamais utilisée par le programme, l’erreur ne sera jamais renvoyée.

Voici un exemple plus simple qui échoue également.

 public class Main { public void neverCalled() { A a = new A(); B b = new B(); a.takeInter(b); } public static void main(Ssortingng[] args) { System.out.println("Hello..."); } } class A { public void takeInter(A in) { } } class B extends A { } class C { } 

dans le code octet

 public void neverCalled(); Code: 0: new #2 // class A 3: dup 4: invokespecial #3 // Method A."":()V 7: astore_1 8: new #4 // class B 11: dup 12: invokespecial #5 // Method B."":()V 15: astore_2 16: aload_1 17: aload_2 18: invokevirtual #6 // Method A.takeInter:(LA;)V 21: return 

Le b est implicitement converti en A et il semble nécessaire de vérifier cela.

Si vous désactivez toutes les vérifications, aucune erreur ne se produit.

 $ rm A.class B.class C.class $ java -Xverify:none -cp . Main Hello... $ java -cp . Main Exception in thread "main" java.lang.NoClassDefFoundError: A 

Votre citation de la section 2.17.1 était totalement hors contexte. C’est en gras ci-dessous. Lorsque lues dans leur contexte, il est clair que “les erreurs … doivent être émises en un point du programme … ” signifie “les erreurs … doivent être émises au moment où le programme atteint un point … “. La phrase, en elle-même, pourrait être libellée mieux – mais ce n’est pas en soi.

L’étape de résolution est facultative au moment du couplage initial. Une implémentation peut résoudre une référence symbolique à partir d’une classe ou d’une interface très tôt liée, même au sharepoint résoudre toutes les références symboliques à partir des classes et des interfaces référencées ultérieurement. (Cette résolution peut entraîner des erreurs résultant d’autres étapes de chargement et de liaison.) Ce choix d’implémentation représente un extrême et est similaire au type de liaison statique qui a été fait pendant de nombreuses années dans des implémentations simples du langage C.

Une implémentation peut plutôt choisir de résoudre une référence symbolique uniquement lorsqu’elle est réellement utilisée; Une utilisation cohérente de cette stratégie pour toutes les références symboliques représenterait la forme de résolution la plus “paresseuse”. Dans ce cas, si Terminator avait plusieurs références symboliques à une autre classe, celles-ci pourraient être résolues une à la fois, voire pas du tout, si ces références n’étaient jamais utilisées lors de l’exécution du programme.

La seule exigence concernant le moment de la résolution est que toutes les erreurs détectées lors de la résolution doivent être émises à un point du programme où le programme effectue une action qui pourrait, directement ou indirectement, nécessiter un lien avec la classe ou l’interface impliquée dans l’erreur. . Dans l’exemple de choix “statique” d’implémentation décrit précédemment, des erreurs de chargement et de liaison pourraient survenir avant l’exécution du programme si elles impliquaient une classe ou une interface mentionnée dans la classe Terminator ou l’une des autres classes et interfaces référencées de manière récursive. Dans un système qui implémentait la résolution “la plus paresseuse”, ces erreurs ne seraient émises que lorsqu’une référence symbolique était utilisée.

Les deux phrases suivantes rendent le sens très clair.