Quando viene inizializzata un'interfaccia con un metodo predefinito?


94

Durante la ricerca attraverso il linguaggio Java Specification per rispondere a questa domanda , ho imparato che

Prima che una classe venga inizializzata, la sua superclasse diretta deve essere inizializzata, ma le interfacce implementate dalla classe non vengono inizializzate. Allo stesso modo, le superinterfacce di un'interfaccia non vengono inizializzate prima che l'interfaccia sia inizializzata.

Per mia curiosità, l'ho provato e, come previsto, l'interfaccia InterfaceTypenon è stata inizializzata.

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

Questo programma stampa

implemented method

Tuttavia, se l'interfaccia dichiara un defaultmetodo, viene eseguita l'inizializzazione. Considera l' InterfaceTypeinterfaccia data come

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

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

quindi lo stesso programma sopra verrà stampato

static initializer  
implemented method

In altre parole, il staticcampo dell'interfaccia viene inizializzato ( passaggio 9 nella procedura di inizializzazione dettagliata ) e staticviene eseguito l' inizializzatore del tipo da inizializzare. Ciò significa che l'interfaccia è stata inizializzata.

Non sono riuscito a trovare nulla nel JLS per indicare che ciò dovrebbe accadere. Non fraintendetemi, capisco che ciò dovrebbe accadere nel caso in cui la classe di implementazione non fornisca un'implementazione per il metodo, ma cosa succede se lo fa? Questa condizione manca nelle specifiche del linguaggio Java, mi sono perso qualcosa o lo sto interpretando in modo errato?


4
La mia ipotesi sarebbe: tali interfacce considerano classi astratte in termini di ordine di inizializzazione. L'ho scritto come commento perché non sono sicuro che sia un'affermazione corretta :)
Alexey Malev

Dovrebbe essere nella sezione 12.4 del JLS, ma non sembra esserci. Direi che manca.
Warren Dew

1
Non importa .... la maggior parte delle volte, quando non capiscono o non hanno una spiegazione,
voteranno in negativo

Ho pensato che interfacein Java non si debba definire alcun metodo concreto. Quindi sono sorpreso che il InterfaceTypecodice sia stato compilato.
MaxZoom

Risposte:


85

Questa è una questione molto interessante!

Sembra che la sezione 12.4.1 di JLS dovrebbe coprirlo definitivamente. Tuttavia, il comportamento di Oracle JDK e OpenJDK (javac e HotSpot) è diverso da quanto specificato qui. In particolare, l'Esempio 12.4.1-3 di questa sezione copre l'inizializzazione dell'interfaccia. L'esempio come segue:

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

Il suo output previsto è:

1
j=3
jj=4
3

e infatti ottengo l'output atteso. Tuttavia, se viene aggiunto un metodo predefinito all'interfaccia I,

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

l'output cambia in:

1
ii=2
j=3
jj=4
3

che indica chiaramente che l'interfaccia Iviene inizializzata dove non era prima! La semplice presenza del metodo di default è sufficiente per attivare l'inizializzazione. Il metodo predefinito non deve essere chiamato, sovrascritto o nemmeno menzionato, né la presenza di un metodo astratto attiva l'inizializzazione.

La mia ipotesi è che l'implementazione di HotSpot volesse evitare di aggiungere l'inizializzazione di classe / interfaccia nel percorso critico della invokevirtualchiamata. Prima di Java 8 e dei metodi predefiniti, invokevirtualnon poteva mai finire per eseguire codice in un'interfaccia, quindi questo non si verificava. Si potrebbe pensare che questo faccia parte della fase di preparazione della classe / interfaccia ( JLS 12.3.2 ) che inizializza cose come le tabelle dei metodi. Ma forse questo è andato troppo oltre e accidentalmente ha invece eseguito l'inizializzazione completa.

Ho sollevato questa domanda nella mailing list OpenJDK compilatore-dev. C'è stata una risposta da Alex Buckley (editore di JLS) in cui solleva ulteriori domande rivolte alla JVM e ai team di implementazione lambda. Nota anche che c'è un bug nelle specifiche qui dove si dice che "T è una classe e viene invocato un metodo statico dichiarato da T" dovrebbe applicarsi anche se T è un'interfaccia. Quindi, potrebbe essere che qui ci siano sia specifiche che bug HotSpot.

Divulgazione : lavoro per Oracle su OpenJDK. Se la gente pensa che questo mi dia un vantaggio ingiusto nell'ottenere la taglia associata a questa domanda, sono disposto a essere flessibile al riguardo.


6
Ho chiesto fonti ufficiali. Non credo che diventi più ufficiale di così. Dagli due giorni per vedere tutti gli sviluppi.
Sotirios Delimanolis

48
@StuartMarks " Se la gente pensa che questo mi dia un vantaggio ingiusto, ecc. " => Siamo qui per ottenere risposte alle domande e questa è una risposta perfetta!
assylias

2
Una nota a margine : la specifica JVM contiene una descrizione simile a quella del JLS: docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5 Anche questo dovrebbe essere aggiornato .
Marco13

2
@assylias e Sotirios, grazie per i vostri commenti. Loro, insieme ai 14 voti positivi (al momento della stesura di questo documento) sul commento di assylias, hanno alleviato le mie preoccupazioni su qualsiasi potenziale ingiustizia.
Stuart Marks

1
@SotiriosDelimanolis Ci sono un paio di bug che sembrano rilevanti, JDK-8043275 e JDK-8043190 , e sono contrassegnati come corretti in 8u40. Tuttavia, il comportamento sembra essere lo stesso. C'erano anche alcune modifiche alle specifiche JVM intrecciate con questo, quindi forse la correzione è qualcosa di diverso dal "ripristinare il vecchio ordine di inizializzazione".
Stuart Marks

13

L'interfaccia non è inizializzata perché il campo della costante InterfaceType.init, che viene inizializzato da un valore non costante (chiamata al metodo), non viene utilizzato da nessuna parte.

È noto in fase di compilazione che il campo costante dell'interfaccia non viene utilizzato da nessuna parte e l'interfaccia non contiene alcun metodo predefinito (in java-8), quindi non è necessario inizializzare o caricare l'interfaccia.

L'interfaccia verrà inizializzata nei seguenti casi,

  • il campo costante viene utilizzato nel codice.
  • L'interfaccia contiene un metodo predefinito (Java 8)

In caso di metodi predefiniti , stai implementando InterfaceType. Quindi, se InterfaceTypeconterrà metodi predefiniti, sarà INHERITED (utilizzato) nella classe di implementazione. E l'inizializzazione sarà nell'immagine.

Tuttavia, se si accede al campo costante dell'interfaccia (che viene inizializzato in modo normale), l'inizializzazione dell'interfaccia non è richiesta.

Considera il seguente codice.

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

Nel caso precedente, l'interfaccia verrà inizializzata e caricata perché stai utilizzando il campo InterfaceType.init.

Non sto fornendo l'esempio del metodo predefinito poiché lo hai già indicato nella tua domanda.

La specifica del linguaggio Java e l'esempio sono forniti in JLS 12.4.1 (l'esempio non contiene metodi predefiniti).


Non riesco a trovare JLS per i metodi predefiniti, potrebbero esserci due possibilità

  • Le persone Java hanno dimenticato di considerare il caso del metodo predefinito. (Specification Doc bug.)
  • Fanno semplicemente riferimento ai metodi predefiniti come membri non costanti dell'interfaccia. (Ma non menzionato da nessuna parte, ancora una volta il bug del documento delle specifiche.)

Sto cercando un riferimento per il metodo predefinito. Il campo era solo per dimostrare che l'interfaccia era stata inizializzata o meno.
Sotirios Delimanolis

@SotiriosDelimanolis Ho menzionato il motivo nella risposta per il metodo predefinito ... ma sfortunatamente non è stato ancora trovato alcun JLS per il metodo predefinito.
Non è un bug

Sfortunatamente, è quello che sto cercando. Mi sembra che la tua risposta stia solo ripetendo le cose che ho già affermato nella domanda, ad es. che un'interfaccia verrà inizializzata se contiene un defaultmetodo e una classe che implementa l'interfaccia viene inizializzata.
Sotirios Delimanolis

Penso che le persone Java abbiano dimenticato di considerare il caso del metodo predefinito, oppure si limitano a fare riferimento ai metodi predefiniti come membri non costanti dell'interfaccia (la mia ipotesi, non può trovare in nessun documento).
Non è un bug il

1
@KishanSarsechaGajjar: Cosa intendi per campo non costante nell'interfaccia? Qualsiasi variabile / campo nell'interfaccia è statico finale per impostazione predefinita.
Lokesh

10

Il file instanceKlass.cpp da OpenJDK contiene il metodo di inizializzazione InstanceKlass::initialize_implche corrisponde alla procedura di inizializzazione dettagliata in JLS, che si trova analogamente nella sezione Inizializzazione nella specifica JVM.

Contiene un nuovo passaggio che non è menzionato nel JLS e non nel libro JVM a cui si fa riferimento nel codice:

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

Quindi questa inizializzazione è stata implementata esplicitamente come nuovo passaggio 7.5 . Ciò indica che questa implementazione ha seguito alcune specifiche, ma sembra che le specifiche scritte sul sito Web non siano state aggiornate di conseguenza.

EDIT: come riferimento, il commit (da ottobre 2012!) In cui il rispettivo passaggio è stato incluso nell'implementazione: http://hg.openjdk.java.net/jdk8/build/hotspot/rev/4735d2c84362

EDIT2: Casualmente, ho trovato questo documento sui metodi predefiniti in hotspot che contiene una nota a margine interessante alla fine:

3.7 Varie

Poiché le interfacce ora contengono un bytecode, è necessario inizializzarle nel momento in cui viene inizializzata una classe di implementazione.


1
Grazie per aver scoperto questo. (+1) È possibile che il nuovo "passaggio 7.5" sia stato inavvertitamente omesso dalle specifiche, o che sia stato proposto e rifiutato e l'implementazione non sia mai stata corretta per rimuoverlo.
Stuart Marks

1

Cercherò di dimostrare che un'inizializzazione dell'interfaccia non dovrebbe causare effetti collaterali del canale laterale da cui dipendono i sottotipi, quindi, indipendentemente dal fatto che si tratti di un bug o meno, o in qualunque modo Java lo risolva, non dovrebbe importare l'applicazione in cui vengono inizializzate le interfacce dell'ordine.

Nel caso di a class, è ben accettato che possa causare effetti collaterali da cui dipendono le sottoclassi. Per esempio

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

Qualsiasi sottoclasse di Foosi aspetterebbe di vedere $ 1000 in banca, ovunque nel codice della sottoclasse. Pertanto la superclasse viene inizializzata prima della sottoclasse.

Non dovremmo fare la stessa cosa anche per le superintefaccia? Sfortunatamente, l'ordine delle superinterfacce non dovrebbe essere significativo, quindi non esiste un ordine ben definito in cui inizializzarle.

Quindi è meglio non stabilire questo tipo di effetti collaterali nelle inizializzazioni dell'interfaccia. Dopotutto, interfacenon è pensato per queste funzionalità (campi / metodi statici) che accumuliamo per comodità.

Pertanto, se seguiamo questo principio, non ci preoccuperemo in quale ordine vengono inizializzate le interfacce.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.