Quand une interface avec une méthode par défaut est-elle initialisée?

94

En cherchant dans la spécification du langage Java pour répondre à cette question , j'ai appris que

Avant qu'une classe ne 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ées avant que l'interface ne soit initialisée.

Pour ma propre curiosité, je l'ai essayé et, comme prévu, l'interface InterfaceTypen'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 defaultméthode, l'initialisation se produit. Considérez l' InterfaceTypeinterface 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 staticchamp de l'interface est initialisé ( étape 9 de la procédure d'initialisation détaillée ) et l' staticinitialiseur 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 devrait arriver. Ne vous méprenez pas, je comprends que cela devrait se produire au cas où la classe d'implémentation ne fournit pas d'implémentation pour la méthode, mais que se passe-t-il si c'est le cas? Cette condition est-elle absente de la spécification du langage Java, ai-je manqué quelque chose ou est-ce que je l'interprète mal?

Sotirios Delimanolis
la source
4
Je suppose que de telles interfaces sont considérées comme des classes abstraites en termes d'ordre d'initialisation. J'ai écrit ceci comme un commentaire car je ne suis pas sûr que cette déclaration soit correcte :)
Alexey Malev
Il devrait être dans la section 12.4 du JLS, mais ne semble pas y être. Je dirais qu'il manque.
Warren Dew
1
Nevermind .... la plupart du temps quand ils ne comprennent pas ou n'ont pas d'explication, ils
voteront contre
Je pensais qu'en interfaceJava ne devrait définir aucune méthode concrète. Je suis donc surpris que le InterfaceTypecode ait été compilé.
MaxZoom
@MaxZoom Java 8 autorise les defaultméthodes .
Sotirios Delimanolis

Réponses:

85

C'est une question très intéressante!

Il semble que la section 12.4.1 de JLS devrait couvrir cela définitivement. Cependant, le comportement d'Oracle JDK et 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;
    }
}

Son résultat attendu 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 change en:

1
ii=2
j=3
jj=4
3

ce qui indique clairement que l'interface Iest en cours d'initialisation là où elle ne l'était pas auparavant! La simple présence de la méthode par défaut suffit à déclencher l'initialisation. La méthode par défaut n'a pas besoin d'être appelée, 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 une vérification d'initialisation de classe / interface dans le chemin critique de l' invokevirtualappel. Avant Java 8 et les méthodes par défaut, invokevirtualon 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 l'étape de préparation de classe / interface ( JLS 12.3.2 ) qui initialise des choses comme les tables de méthodes. Mais peut-être que cela est allé trop loin et a accidentellement effectué une initialisation complète à la place.

J'ai soulevé cette question sur la liste de diffusion OpenJDK compiler-dev. Il y a eu une réponse d'Alex Buckley (rédacteur en chef du JLS) dans laquelle il soulève d'autres questions adressées aux équipes d'implémentation JVM et lambda. Il note également qu'il y a un bogue 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 HotSpot ici.

Divulgation : Je travaille pour Oracle sur OpenJDK. Si les gens pensent que cela me donne un avantage injuste pour obtenir la prime attachée à cette question, je suis prêt à être flexible à ce sujet.

Marques Stuart
la source
6
J'ai demandé des sources officielles. Je ne pense pas que ça devienne plus officiel que ça. Donnez-lui deux jours pour voir tous les développements.
Sotirios Delimanolis
48
@StuartMarks " Si les gens pensent que cela me donne un avantage injuste etc " => nous sommes ici pour obtenir des réponses aux questions et c'est une réponse parfaite!
assylias
2
Remarque: la spécification JVM contient une description similaire à celle du JLS: docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5 Cela devrait également être mis à jour .
Marco13
2
@assylias et Sotirios, merci pour vos commentaires. Ils, ainsi que les 14 votes positifs (au moment de la rédaction de cet article) sur le commentaire d'assylias, ont atténué mes inquiétudes concernant toute injustice potentielle.
Stuart marque le
1
@SotiriosDelimanolis Il y a quelques bogues qui semblent pertinents, JDK-8043275 et JDK-8043190 , et ils sont marqués comme corrigés dans 8u40. Cependant, le comportement semble être le même. Il y avait aussi quelques changements de spécifications JVM liés à cela, donc peut-être que le correctif est autre chose que «restaurer l'ancien ordre d'initialisation».
Stuart marque
13

L'interface n'est pas initialisée car le champ constant InterfaceType.init, qui est initialisé 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,

  • le champ constant est utilisé dans votre code.
  • L'interface contient une méthode par défaut (Java 8)

En cas de méthodes par défaut , vous implémentez InterfaceType. Donc, If InterfaceTypecontiendra des méthodes par défaut, Il sera HÉRITÉ (utilisé) dans la classe d'implémentation. Et l'initialisation sera dans l'image.

Mais, si vous accédez au champ constant de l'interface (qui est initialisé de manière normale), l'initialisation de l'interface n'est pas requise.

Considérez le code suivant.

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 la méthode par défaut comme vous l'avez déjà indiqué 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

  • Les gens de Java ont oublié de considérer le cas de la méthode par défaut. (Bogue du document de spécification.)
  • Ils font simplement référence aux méthodes par défaut comme des membres non constants de l'interface. (Mais je n'ai mentionné nulle part, encore une fois le bogue du document de spécification.)
Pas un bug
la source
Je recherche une référence pour la méthode par défaut. Le champ était juste pour démontrer que l'interface était initialisée ou non.
Sotirios Delimanolis
@SotiriosDelimanolis J'ai mentionné la raison dans la réponse pour la méthode par défaut ... mais malheureusement, aucun JLS n'a encore été trouvé pour la méthode par défaut.
Pas un bug
Malheureusement, c'est ce que je recherche. J'ai l'impression que votre réponse ne fait que répéter les choses que j'ai déjà énoncées dans la question, à savoir. qu'une interface sera initialisée si elle contient une defaultméthode et une classe qui implémente l'interface est initialisée.
Sotirios Delimanolis
Je pense que les gens de Java ont oublié de considérer le cas de la méthode par défaut, ou ils se réfèrent simplement aux méthodes par défaut comme membre non constant de l'interface (mon hypothèse, ne peut trouver dans aucun document).
Pas un bug
1
@KishanSarsechaGajjar: Qu'entendez-vous par champ non constant dans l'interface? Toute variable / champ dans l'interface est statique final par défaut.
Lokesh
10

Le fichier instanceKlass.cpp d'OpenJDK contient la méthode d'initialisation InstanceKlass::initialize_implqui 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 non dans le livre JVM auquel le code fait référence:

// 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 en tant que nouvelle étape 7.5 . Cela indique que cette implémentation a suivi certaines spécifications, mais il semble que la spécification écrite sur le site Web n'a pas été mise à jour en conséquence.

EDIT: À titre de référence, le commit (à partir d'octobre 2012!) Où l'étape respective a été incluse dans l'implémentation: http://hg.openjdk.java.net/jdk8/build/hotspot/rev/4735d2c84362

EDIT2: Par coïncidence, j'ai trouvé ce document sur les méthodes par défaut dans hotspot qui contient une note latérale intéressante à la fin:

3.7 Divers

Étant donné que les interfaces contiennent désormais du bytecode, nous devons les initialiser au moment où une classe d'implémentation est initialisée.

Marco13
la source
1
Merci d'avoir déterré ça. (+1) Il se peut que la nouvelle "étape 7.5" ait été omise par inadvertance de la spécification, ou qu'elle ait été proposée et rejetée et que la mise en œuvre n'ait jamais été corrigée pour la supprimer.
Stuart marque le
1

Je vais essayer de faire valoir qu'une initialisation d'interface ne devrait pas causer d'effets secondaires de canal secondaire dont les sous-types dépendent, par conséquent, qu'il s'agisse d'un bogue ou non, ou de la manière dont Java le corrige, cela ne devrait pas avoir d'importance. l'application dans laquelle les interfaces 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 Foos'attend à 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 devrait-on pas faire la même chose pour les superintéfaces également? Malheureusement, l'ordre des super-interfaces n'est pas censé être significatif, il n'y a donc pas d'ordre bien défini dans lequel les initialiser.

Il vaut donc mieux ne pas établir ce genre d'effets secondaires dans les initialisations d'interface. Après tout, ce interfacen'est pas pour 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 dans lequel les interfaces sont initialisées.

ZhongYu
la source