Wann wird eine Schnittstelle mit einer Standardmethode initialisiert?

94

Während sie durch die Java Language Specification Suche zu beantworten diese Frage , habe ich gelernt , dass

Bevor eine Klasse initialisiert wird, muss ihre direkte Oberklasse initialisiert werden, aber von der Klasse implementierte Schnittstellen werden nicht initialisiert. In ähnlicher Weise werden die Superschnittstellen einer Schnittstelle nicht initialisiert, bevor die Schnittstelle initialisiert wird.

Aus eigener Neugier habe ich es versucht und wie erwartet wurde die Schnittstelle InterfaceTypenicht initialisiert.

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();
}

Dieses Programm druckt

implemented method

Wenn die Schnittstelle jedoch eine defaultMethode deklariert, erfolgt eine Initialisierung. Betrachten Sie die InterfaceTypeSchnittstelle als

interface InterfaceType {
    public static final ClassInitializer init = new ClassInitializer();

    public default void method() {
        System.out.println("default method");
    }
}

dann würde das gleiche Programm oben gedruckt

static initializer  
implemented method

Mit anderen Worten, das staticFeld der Schnittstelle wird initialisiert ( Schritt 9 in der detaillierten Initialisierungsprozedur ) und der staticInitialisierer des zu initialisierenden Typs wird ausgeführt. Dies bedeutet, dass die Schnittstelle initialisiert wurde.

Ich konnte im JLS nichts finden, was darauf hindeutet, dass dies geschehen sollte. Versteh mich nicht falsch, ich verstehe, dass dies passieren sollte, falls die implementierende Klasse keine Implementierung für die Methode bereitstellt, aber was ist, wenn dies der Fall ist? Fehlt diese Bedingung in der Java-Sprachspezifikation, habe ich etwas verpasst oder interpretiere ich es falsch?

Sotirios Delimanolis
quelle
4
Meine Vermutung wäre - solche Schnittstellen werden in Bezug auf die Initialisierungsreihenfolge als abstrakte Klassen betrachtet. Ich schrieb dies als Kommentar, da ich nicht sicher bin, ob dies die richtige Aussage ist :)
Alexey Malev
Es sollte in Abschnitt 12.4 des JLS enthalten sein, scheint aber nicht vorhanden zu sein. Ich würde sagen, es fehlt.
Warren Dew
1
Nevermind .... die meiste Zeit, wenn sie nicht verstehen oder keine Erklärung haben, werden sie abstimmen :(. Dies geschieht auf SO allgemein.
NeverGiveUp161
Ich dachte, dass interfacein Java keine konkrete Methode definiert werden sollte. Ich bin also überrascht, dass der InterfaceTypeCode kompiliert wurde.
MaxZoom
@MaxZoom Java 8 ermöglicht defaultMethoden .
Sotirios Delimanolis

Antworten:

85

Dies ist ein sehr interessantes Thema!

Es scheint, dass JLS-Abschnitt 12.4.1 dies definitiv abdecken sollte. Das Verhalten von Oracle JDK und OpenJDK (javac und HotSpot) unterscheidet sich jedoch von den hier angegebenen. Das Beispiel 12.4.1-3 in diesem Abschnitt behandelt insbesondere die Schnittstelleninitialisierung. Das Beispiel wie folgt:

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

Die erwartete Leistung beträgt:

1
j=3
jj=4
3

und tatsächlich bekomme ich die erwartete Ausgabe. Wenn der Schnittstelle jedoch eine Standardmethode hinzugefügt wird I,

interface I {
    int i = 1, ii = Test.out("ii", 2);
    default void method() { } // causes initialization!
}

Die Ausgabe ändert sich zu:

1
ii=2
j=3
jj=4
3

Dies zeigt deutlich, dass die Schnittstelle Idort initialisiert wird, wo sie vorher nicht war! Das bloße Vorhandensein der Standardmethode reicht aus, um die Initialisierung auszulösen. Die Standardmethode muss nicht aufgerufen oder überschrieben oder gar erwähnt werden, und das Vorhandensein einer abstrakten Methode löst keine Initialisierung aus.

Ich spekuliere, dass die HotSpot-Implementierung vermeiden wollte, dass die Überprüfung der Klassen- / Schnittstelleninitialisierung in den kritischen Pfad des invokevirtualAufrufs aufgenommen wird. Vor Java 8 und Standardmethoden invokevirtualkonnte niemals Code in einer Schnittstelle ausgeführt werden, sodass dies nicht auftrat. Man könnte meinen, dass dies Teil der Vorbereitungsphase für Klassen / Schnittstellen ( JLS 12.3.2 ) ist, die Dinge wie Methodentabellen initialisiert. Aber vielleicht ging dies zu weit und führte stattdessen versehentlich eine vollständige Initialisierung durch.

Ich habe diese Frage auf der OpenJDK-Mailingliste für Compiler-Entwickler aufgeworfen . Es gab eine Antwort von Alex Buckley (Herausgeber des JLS), in der er weitere Fragen an die Implementierungsteams von JVM und Lambda stellt. Er merkt auch an, dass es hier einen Fehler in der Spezifikation gibt, der besagt, dass "T eine Klasse ist und eine von T deklarierte statische Methode aufgerufen wird", auch gelten sollte, wenn T eine Schnittstelle ist. Es kann also sein, dass es hier sowohl Spezifikations- als auch HotSpot-Fehler gibt.

Offenlegung : Ich arbeite für Oracle unter OpenJDK. Wenn die Leute denken, dass dies mir einen unfairen Vorteil verschafft, wenn ich die Prämie für diese Frage bekomme, bin ich bereit, flexibel zu sein.

Stuart Marks
quelle
6
Ich bat um offizielle Quellen. Ich denke nicht, dass es offizieller wird. Nehmen Sie sich zwei Tage Zeit, um alle Entwicklungen zu sehen.
Sotirios Delimanolis
48
@StuartMarks " Wenn die Leute denken, dass dies mir einen unfairen Vorteil verschafft usw. " => Wir sind hier, um Antworten auf Fragen zu erhalten, und dies ist eine perfekte Antwort!
Assylias
2
Eine Randnotiz: Die JVM-Spezifikation enthält eine Beschreibung, die der des JLS ähnelt: docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5 Diese sollte ebenfalls aktualisiert werden .
Marco13
2
@assylias und Sotirios, danke für deine Kommentare. Zusammen mit den 14 positiven Stimmen (zum Zeitpunkt dieses Schreibens) zu Assylias 'Kommentar haben sie meine Bedenken hinsichtlich einer möglichen Ungerechtigkeit ausgeräumt.
Stuart Marks
1
@SotiriosDelimanolis Es gibt einige Fehler, die relevant erscheinen, JDK-8043275 und JDK-8043190 , und sie sind in 8u40 als behoben markiert. Das Verhalten scheint jedoch dasselbe zu sein. Es gab auch einige Änderungen an der JVM-Spezifikation, die damit verflochten sind. Daher ist das Update möglicherweise etwas anderes als "Wiederherstellen der alten Initialisierungsreihenfolge".
Stuart Marks
13

Die Schnittstelle wird nicht initialisiert, da das konstante Feld InterfaceType.init, das durch einen nicht konstanten Wert (Methodenaufruf) initialisiert wird, nirgendwo verwendet wird.

Es ist bekannt, dass zur Kompilierungszeit kein konstantes Feld der Schnittstelle verwendet wird und die Schnittstelle keine Standardmethode enthält (in Java-8), sodass die Schnittstelle nicht initialisiert oder geladen werden muss.

Die Schnittstelle wird in folgenden Fällen initialisiert:

  • Das konstante Feld wird in Ihrem Code verwendet.
  • Die Schnittstelle enthält eine Standardmethode (Java 8)

Bei Standardmethoden implementieren Sie InterfaceType. Wenn also InterfaceTypeStandardmethoden enthalten sind, wird es bei der Implementierung der Klasse VERERBT (verwendet) . Und die Initialisierung wird ins Bild kommen.

Wenn Sie jedoch auf ein konstantes Feld der Schnittstelle zugreifen (das auf normale Weise initialisiert wird), ist die Schnittstelleninitialisierung nicht erforderlich.

Betrachten Sie den folgenden 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();
}

Im obigen Fall wird die Schnittstelle initialisiert und geladen, da Sie das Feld verwenden InterfaceType.init.

Ich gebe nicht das Beispiel für die Standardmethode an, wie Sie dies bereits in Ihrer Frage angegeben haben.

Die Java-Sprachspezifikation und das Beispiel finden Sie in JLS 12.4.1 (Beispiel enthält keine Standardmethoden.)


Ich kann JLS für Standardmethoden nicht finden, es gibt möglicherweise zwei Möglichkeiten

  • Java-Leute haben vergessen, den Fall der Standardmethode zu berücksichtigen. (Spezifikationsdokumentfehler.)
  • Sie bezeichnen die Standardmethoden lediglich als nicht konstantes Mitglied der Schnittstelle. (Aber nein erwähnt, wieder, Spezifikation Doc Fehler.)
Kein Fehler
quelle
Ich suche eine Referenz für die Standardmethode. Das Feld sollte nur zeigen, ob die Schnittstelle initialisiert wurde oder nicht.
Sotirios Delimanolis
@SotiriosDelimanolis Ich habe den Grund als Antwort auf die Standardmethode erwähnt ... aber leider wurde noch kein JLS für die Standardmethode gefunden.
Kein Fehler
Leider suche ich das. Ich habe das Gefühl, dass Ihre Antwort nur Dinge wiederholt, die ich bereits in der Frage angegeben habe, dh. dass eine Schnittstelle initialisiert wird, wenn sie eine defaultMethode enthält und eine Klasse, die die Schnittstelle implementiert, initialisiert wird.
Sotirios Delimanolis
Ich denke, Java-Leute haben vergessen, den Fall der Standardmethode zu berücksichtigen, oder sie bezeichnen die Standardmethoden einfach als nicht konstantes Mitglied der Schnittstelle (meine Annahme, kann in keinem Dokument gefunden werden).
Kein Fehler
1
@KishanSarsechaGajjar: Was meinst du mit nicht konstantem Feld in der Schnittstelle? Jede Variable / jedes Feld in der Schnittstelle ist standardmäßig statisch final.
Lokesh
10

Die Datei instanceKlass.cpp aus dem OpenJDK enthält die Initialisierungsmethode InstanceKlass::initialize_impl, die der detaillierten Initialisierungsprozedur im JLS entspricht, die sich analog im Abschnitt Initialisierung in der JVM-Spezifikation befindet.

Es enthält einen neuen Schritt, der im JLS und nicht im JVM-Buch, auf das im Code verwiesen wird, nicht erwähnt wird:

// 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);
    ....
    }
  }
}

Daher wurde diese Initialisierung explizit als neuer Schritt 7.5 implementiert . Dies weist darauf hin, dass diese Implementierung einer bestimmten Spezifikation entsprach, die schriftliche Spezifikation auf der Website jedoch offenbar nicht entsprechend aktualisiert wurde.

BEARBEITEN: Als Referenz das Commit (ab Oktober 2012!), Bei dem der entsprechende Schritt in die Implementierung aufgenommen wurde: http://hg.openjdk.java.net/jdk8/build/hotspot/rev/4735d2c84362

EDIT2: Zufälligerweise habe ich dieses Dokument über Standardmethoden im Hotspot gefunden, das am Ende eine interessante Randnotiz enthält:

3.7 Verschiedenes

Da Schnittstellen jetzt Bytecode enthalten, müssen sie zum Zeitpunkt der Initialisierung einer implementierenden Klasse initialisiert werden.

Marco13
quelle
1
Danke, dass du das ausgegraben hast. (+1) Es kann sein, dass der neue "Schritt 7.5" versehentlich in der Spezifikation weggelassen wurde oder dass er vorgeschlagen und abgelehnt wurde und die Implementierung nie repariert wurde, um ihn zu entfernen.
Stuart Marks
1

Ich werde versuchen zu behaupten, dass eine Schnittstelleninitialisierung keine Nebenkanal-Nebenwirkungen verursachen sollte, von denen die Subtypen abhängen. Ob dies ein Fehler ist oder nicht oder wie Java ihn behebt, sollte keine Rolle spielen die Anwendung, in der die Schnittstellen initialisiert werden.

Im Fall von a classist es allgemein anerkannt, dass es Nebenwirkungen verursachen kann, von denen Unterklassen abhängen. Beispielsweise

class Foo{
    static{
        Bank.deposit($1000);
...

Jede Unterklasse von Foowürde erwarten, dass sie 1000 USD auf der Bank sehen, irgendwo im Unterklassencode. Daher wird die Oberklasse vor der Unterklasse initialisiert.

Sollten wir das nicht auch für Superintefaces tun? Leider sollte die Reihenfolge der Superschnittstellen nicht signifikant sein, daher gibt es keine genau definierte Reihenfolge, in der sie initialisiert werden können.

Daher sollten wir diese Art von Nebenwirkungen bei Schnittstelleninitialisierungen besser nicht feststellen. Schließlich interfaceist es nicht für diese Funktionen (statische Felder / Methoden) gedacht, auf die wir der Einfachheit halber aufbauen.

Wenn wir diesem Prinzip folgen, ist es uns daher egal, in welcher Reihenfolge Schnittstellen initialisiert werden.

ZhongYu
quelle