En cherchant dans le Java Spécification du langage pour répondre cette question , j'ai appris que
Avant qu'une classe soit initialisée, sa superclasse directe doit être initialisée, mais les interfaces implémentées par la classe ne sont pas initialisées. De même, les superinterfaces d'une interface ne sont pas initialisé avant que l'interface soit initialisée.
Pour ma propre curiosité, je l'ai essayé et, comme prévu, l'interface InterfaceType
n'a pas été initialisée.
public class Example {
public static void main(String[] args) throws Exception {
InterfaceType foo = new InterfaceTypeImpl();
foo.method();
}
}
class InterfaceTypeImpl implements InterfaceType {
@Override
public void method() {
System.out.println("implemented method");
}
}
class ClassInitializer {
static {
System.out.println("static initializer");
}
}
interface InterfaceType {
public static final ClassInitializer init = new ClassInitializer();
public void method();
}
Ce programme imprime
implemented method
Cependant, si l'interface déclare une méthode default
, l'initialisation a lieu. Considérez l'interface InterfaceType
donnée comme
interface InterfaceType {
public static final ClassInitializer init = new ClassInitializer();
public default void method() {
System.out.println("default method");
}
}
alors le même programme ci-dessus imprimerait
static initializer
implemented method
En d'autres termes, le champ static
de l'interface est initialisé ( étape 9 de la procédure d'initialisation détaillée ) et l'initialiseur static
du type en cours d'initialisation est exécuté. Cela signifie que l'interface a été initialisée.
Je n'ai rien trouvé dans le JLS pour indiquer que cela devait se produire. Ne vous méprenez pas, je comprends que cela devrait se produire dans le cas où la classe d'implémentation ne fournit pas d'implémentation pour la méthode, mais que faire si elle le fait? Cette condition manque-t-elle dans la spécification de langage Java, ai-je oublié quelque chose ou est-ce que je l'interprète de manière incorrecte?
C'est une question très intéressante!
Il semble que JLS section 12.4.1 devrait couvrir cela définitivement. Cependant, le comportement d'Oracle JDK et d'OpenJDK (javac et HotSpot) diffère de ce qui est spécifié ici. En particulier, l'exemple 12.4.1-3 de cette section couvre l'initialisation de l'interface. L'exemple comme suit:
interface I {
int i = 1, ii = Test.out("ii", 2);
}
interface J extends I {
int j = Test.out("j", 3), jj = Test.out("jj", 4);
}
interface K extends J {
int k = Test.out("k", 5);
}
class Test {
public static void main(String[] args) {
System.out.println(J.i);
System.out.println(K.j);
}
static int out(String s, int i) {
System.out.println(s + "=" + i);
return i;
}
}
Sa sortie attendue est:
1
j=3
jj=4
3
et en effet j'obtiens la sortie attendue. Cependant, si une méthode par défaut est ajoutée à l'interface I
,
interface I {
int i = 1, ii = Test.out("ii", 2);
default void method() { } // causes initialization!
}
la sortie devient:
1
ii=2
j=3
jj=4
3
ce qui indique clairement que l'interface I
est en cours d'initialisation là où elle n'était pas avant! La simple présence de la méthode par défaut suffit pour déclencher l'initialisation. La méthode par défaut n'a pas besoin d'être appelée ou remplacée ou même mentionnée, et la présence d'une méthode abstraite ne déclenche pas l'initialisation.
Ma spéculation est que l'implémentation HotSpot voulait éviter d'ajouter la vérification d'initialisation de classe/interface dans le chemin critique de l'appel invokevirtual
. Avant Java 8 et méthodes par défaut, invokevirtual
ne pouvait jamais finir par exécuter du code dans une interface, donc cela ne se produisait pas. On pourrait penser que cela fait partie de la classe/étape de préparation de l'interface ( JLS 12.3.2 ) qui initialise des choses comme les tables de méthodes. Mais cela est peut-être allé trop loin et a accidentellement effectué l'initialisation complète à la place.
J'ai posé cette question sur la liste de diffusion OpenJDK compiler-dev. Il y a eu un réponse d'Alex Buckley (éditeur du JLS) dans lequel il soulève plus de questions adressées aux équipes d'implémentation JVM et lambda. Il note également qu'il y a un bug dans la spécification ici où il est dit "T est une classe et une méthode statique déclarée par T est invoquée" devrait également s'appliquer si T est une interface. Donc, il se peut qu'il y ait à la fois des bogues de spécification et de HotSpot.
Divulgation: Je travaille pour Oracle sur OpenJDK. Si les gens pensent que cela me donne un avantage injuste dans l'attribution de la prime à cette question, je suis prêt à être flexible à ce sujet.
L'interface n'est pas initialisée car le champ constant InterfaceType.init
, qui est en cours d'initialisation par une valeur non constante (appel de méthode), n'est utilisé nulle part.
Il est connu au moment de la compilation que le champ constant de l'interface n'est utilisé nulle part et que l'interface ne contient aucune méthode par défaut (en Java-8), il n'est donc pas nécessaire d'initialiser ou de charger l'interface.
L'interface sera initialisée dans les cas suivants,
Dans le cas de Méthodes par défaut , vous implémentez InterfaceType
. Donc, si InterfaceType
contiendra des méthodes par défaut, ce sera INHERITED (utilisé) dans l'implémentation de la classe. Et l'initialisation sera dans l'image.
Mais, si vous accédez à un champ d'interface constant (qui est initialisé de manière normale), l'initialisation de l'interface n'est pas requise.
Pensez à suivre le code.
public class Example {
public static void main(String[] args) throws Exception {
InterfaceType foo = new InterfaceTypeImpl();
System.out.println(InterfaceType.init);
foo.method();
}
}
class InterfaceTypeImpl implements InterfaceType {
@Override
public void method() {
System.out.println("implemented method");
}
}
class ClassInitializer {
static {
System.out.println("static initializer");
}
}
interface InterfaceType {
public static final ClassInitializer init = new ClassInitializer();
public void method();
}
Dans le cas ci-dessus, l'interface sera initialisée et chargée car vous utilisez le champ InterfaceType.init
.
Je ne donne pas l'exemple de méthode par défaut comme vous l'avez déjà donné dans votre question.
La spécification et l'exemple du langage Java sont donnés dans JLS 12.4.1 (l'exemple ne contient pas de méthodes par défaut.)
Je ne trouve pas JLS pour les méthodes par défaut, il peut y avoir deux possibilités
Le fichier instanceKlass.cpp d'OpenJDK contient la méthode d'initialisation InstanceKlass::initialize_impl
qui correspond à la Procédure d'initialisation détaillée dans le JLS, qui se trouve de manière analogue dans la section Initialisation de la spécification JVM.
Il contient une nouvelle étape qui n'est pas mentionnée dans le JLS et pas dans le livre JVM auquel il est fait référence dans le code:
// refer to the JVM book page 47 for description of steps
...
if (this_oop->has_default_methods()) {
// Step 7.5: initialize any interfaces which have default methods
for (int i = 0; i < this_oop->local_interfaces()->length(); ++i) {
Klass* iface = this_oop->local_interfaces()->at(i);
InstanceKlass* ik = InstanceKlass::cast(iface);
if (ik->has_default_methods() && ik->should_be_initialized()) {
ik->initialize(THREAD);
....
}
}
}
Cette initialisation a donc été implémentée explicitement comme une nouvelle étape 7.5 . Cela indique que cette implémentation a suivi certaines spécifications, mais il semble que les spécifications écrites sur le site Web n'aient pas été mises à jour en conséquence.
EDIT: comme référence, le commit (à partir d'octobre 2012!) Où l'étape respective a été incluse dans la mise en œuvre: http://hg.openjdk.Java.net/jdk8/build/hotspot/rev/4735d2c84362
EDIT2: Par coïncidence, j'ai trouvé cela Document sur les méthodes par défaut dans le hotspot qui contient une note latérale intéressante à la fin:
3.7 Divers
Comme les interfaces contiennent désormais du bytecode, nous devons les initialiser au moment où une classe d'implémentation est initialisée.
Je vais essayer de démontrer qu'une initialisation d'interface ne devrait pas provoquer d'effets secondaires sur les canaux secondaires dont dépendent les sous-types, qu'il s'agisse ou non d'un bogue, ou de quelque manière que ce soit Java = le corrige, peu importe l'application dans laquelle les interfaces d'ordre sont initialisées.
Dans le cas d'un class
, il est bien admis qu'il peut provoquer des effets secondaires dont dépendent les sous-classes. Par exemple
class Foo{
static{
Bank.deposit($1000);
...
Toute sous-classe de Foo
s'attendrait à voir 1000 $ dans la banque, n'importe où dans le code de sous-classe. Par conséquent, la superclasse est initialisée avant la sous-classe.
Ne devrions-nous pas faire la même chose pour les superinterfaces également? Malheureusement, l'ordre des superinterfaces n'est pas censé être significatif, il n'y a donc pas d'ordre bien défini pour les initialiser.
Il vaut donc mieux ne pas établir ce type d'effets secondaires dans les initialisations d'interface. Après tout, interface
n'est pas destiné à ces fonctionnalités (champs/méthodes statiques) que nous empilons pour plus de commodité.
Par conséquent, si nous suivons ce principe, nous ne nous soucierons pas de l'ordre d'initialisation des interfaces.