web-dev-qa-db-fra.com

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

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?

92

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.

81
Stuart Marks

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,

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

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

  • Les gens de Java ont oublié de considérer le cas de la méthode par défaut. (Bogue de documentation de spécification.)
  • Ils se réfèrent simplement aux méthodes par défaut en tant que membre non constant de l'interface. (Mais mentionné nulle part, encore une fois le bogue de documentation de spécification.)
13
Not a bug

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.

10
Marco13

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.

1
ZhongYu