Kiedy inicjowany jest interfejs z metodą domyślną?

94

Podczas przeszukiwania specyfikacji języka Java, aby odpowiedzieć na to pytanie , dowiedziałem się tego

Zanim klasa zostanie zainicjowana, jej bezpośrednia nadklasa musi zostać zainicjowana, ale interfejsy implementowane przez klasę nie są inicjowane. Podobnie, superinterfejsy interfejsu nie są inicjowane przed zainicjowaniem interfejsu.

Z własnej ciekawości spróbowałem i zgodnie z oczekiwaniami interfejs InterfaceTypenie został zainicjowany.

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

Ten program drukuje

implemented method

Jeśli jednak interfejs deklaruje defaultmetodę, następuje inicjalizacja. Rozważmy InterfaceTypeinterfejs podany jako

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

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

wydrukowałby ten sam program powyżej

static initializer  
implemented method

Innymi słowy, staticpole interfejsu jest inicjalizowane ( krok 9 w szczegółowej procedurze inicjalizacji ) i wykonywany jest staticinicjalizator inicjalizowanego typu. Oznacza to, że interfejs został zainicjowany.

W JLS nie mogłem znaleźć niczego, co wskazywałoby na to, że to powinno się wydarzyć. Nie zrozum mnie źle, rozumiem, że powinno to mieć miejsce w przypadku, gdy klasa implementująca nie zapewnia implementacji metody, ale co, jeśli tak jest? Czy tego warunku brakuje w specyfikacji języka Java, czy coś przeoczyłem, czy też interpretuję to nieprawidłowo?

Sotirios Delimanolis
źródło
4
Domyślam się - takie interfejsy uważały klasy abstrakcyjne pod względem kolejności inicjalizacji. Napisałem to jako komentarz, ponieważ nie jestem pewien, czy to jest poprawne stwierdzenie :)
Alexey Malev
Powinien znajdować się w sekcji 12.4 JLS, ale wydaje się, że go tam nie ma. Powiedziałbym, że brakuje.
Warren Dew
1
Nieważne ... przez większość czasu, gdy nie rozumieją lub nie mają wyjaśnienia, głosują przeciw :(. Dzieje się tak ogólnie w SO.
NeverGiveUp161
Pomyślałem, że interfacew Javie nie powinno się definiować żadnej konkretnej metody. Jestem więc zaskoczony, że InterfaceTypekod się skompilował.
MaxZoom
@MaxZoom Java 8 zezwala na defaultmetody .
Sotirios Delimanolis

Odpowiedzi:

85

To bardzo interesująca kwestia!

Wygląda na to, że sekcja 12.4.1 JLS powinna zająć się tym ostatecznie. Jednak zachowanie Oracle JDK i OpenJDK (javac i HotSpot) różni się od tego, co określono tutaj. W szczególności przykład 12.4.1-3 z tej sekcji obejmuje inicjalizację interfejsu. Przykład w następujący sposób:

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

Jego oczekiwany wynik to:

1
j=3
jj=4
3

i rzeczywiście otrzymuję oczekiwany wynik. Jeśli jednak do interfejsu zostanie dodana metoda domyślna I,

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

wynik zmienia się na:

1
ii=2
j=3
jj=4
3

co wyraźnie wskazuje, że interfejs Ijest inicjowany tam, gdzie nie był wcześniej! Sama obecność domyślnej metody wystarczy, aby wywołać inicjalizację. Metoda domyślna nie musi być wywoływana, zastępowana ani nawet wspominana, ani też obecność metody abstrakcyjnej nie powoduje inicjalizacji.

Spekuluję, że implementacja HotSpot chciała uniknąć dodawania sprawdzania inicjalizacji klasy / interfejsu do krytycznej ścieżki invokevirtualpołączenia. Przed Java 8 i metodami domyślnymi invokevirtualnigdy nie można było wykonać kodu w interfejsie, więc tak się nie stało. Można by pomyśleć, że jest to część etapu przygotowania klasy / interfejsu ( JLS 12.3.2 ), który inicjuje takie rzeczy, jak tabele metod. Ale być może to poszło za daleko i zamiast tego przypadkowo spowodowało pełną inicjalizację.

Mam podniósł tę kwestię na liście mailingowej kompilator-dev OpenJDK. Jest odpowiedź od Alexa Buckleya (redaktora JLS), w której zadaje więcej pytań skierowanych do zespołów wdrożeniowych JVM i lambdy. Zauważa również, że w specyfikacji jest błąd, który mówi, że „T jest klasą, a metoda statyczna zadeklarowana przez T jest wywoływana” powinna również obowiązywać, jeśli T jest interfejsem. Może się więc zdarzyć, że są tu zarówno błędy specyfikacji, jak i HotSpot.

Ujawnienie : pracuję dla Oracle na OpenJDK. Jeśli ludzie uważają, że daje mi to nieuczciwą przewagę w uzyskaniu nagrody związanej z tym pytaniem, jestem gotów być elastyczny w tej kwestii.

Stuart Marks
źródło
6
Poprosiłem o oficjalne źródła. Nie sądzę, żeby stało się to bardziej oficjalne niż to. Daj mu dwa dni, aby zobaczyć wszystkie zmiany.
Sotirios Delimanolis
48
@StuartMarks " Jeśli ludzie uważają, że daje mi to nieuczciwą przewagę itp. " => Jesteśmy tutaj, aby uzyskać odpowiedzi na pytania i to jest doskonała odpowiedź!
assylias
2
Uwaga dodatkowa: specyfikacja maszyny JVM zawiera opis podobny do opisu z JLS: docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5 Należy go również zaktualizować .
Marco13
2
@assylias i Sotirios, dzięki za komentarze. Wraz z 14 głosami za (w chwili pisania tego tekstu) w komentarzu asyliów, złagodzili moje obawy dotyczące potencjalnej niesprawiedliwości.
Stuart Marks
1
@SotiriosDelimanolis Istnieje kilka błędów, które wydają się istotne, JDK-8043275 i JDK-8043190 , i zostały one oznaczone jako naprawione w wersji 8u40. Jednak zachowanie wydaje się być takie samo. Było też kilka zmian specyfikacji JVM, które były z tym powiązane, więc być może poprawka jest czymś innym niż „przywróć starą kolejność inicjalizacji”.
Stuart Marks
13

Interfejs nie jest inicjowany, ponieważ pole stałe InterfaceType.init, które jest inicjowane przez wartość zmienną (wywołanie metody), nie jest nigdzie używane.

W czasie kompilacji wiadomo, że stałe pole interfejsu nie jest nigdzie używane, a interfejs nie zawiera żadnej domyślnej metody (w java-8), więc nie ma potrzeby inicjowania ani ładowania interfejsu.

Interfejs zostanie zainicjowany w następujących przypadkach,

  • W kodzie używane jest pole stałe.
  • Interfejs zawiera metodę domyślną (Java 8)

W przypadku domyślnych metod , Ty wdrażają InterfaceType. Więc jeśli InterfaceTypebędzie zawierał jakiekolwiek domyślne metody, będzie INHERITED (używany) w implementacji klasy. Inicjalizacja będzie widoczna na zdjęciu.

Ale jeśli uzyskujesz dostęp do stałego pola interfejsu (które jest inicjowane w normalny sposób), inicjalizacja interfejsu nie jest wymagana.

Rozważ następujący kod.

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

W powyższym przypadku interfejs zostanie zainicjowany i załadowany, ponieważ używasz tego pola InterfaceType.init .

Nie podaję domyślnego przykładu metody, ponieważ podałeś to już w swoim pytaniu.

Specyfikacja i przykład języka Java są podane w JLS 12.4.1 (przykład nie zawiera metod domyślnych).


Nie mogę znaleźć JLS dla metod domyślnych, mogą istnieć dwie możliwości

  • Ludzie Javy zapomnieli rozważyć przypadek metody domyślnej. (Błąd dokumentu specyfikacji).
  • Po prostu odwołują się do metod domyślnych jako niestałych elementów interfejsu. (Ale wspomniałem nie gdzie, ponownie błąd Specification Doc.)
To nie jest błąd
źródło
Szukam odniesienia do metody domyślnej. Pole służyło tylko do zademonstrowania, że ​​interfejs został zainicjowany, czy nie.
Sotirios Delimanolis
@SotiriosDelimanolis W odpowiedzi wspomniałem o przyczynie metody domyślnej ... ale niestety nie znaleziono jeszcze żadnego JLS dla metody domyślnej.
To nie jest błąd
Niestety tego właśnie szukam. Wydaje mi się, że twoja odpowiedź jest po prostu powtórzeniem rzeczy, które już określiłem w pytaniu, tj. że interfejs zostanie zainicjowany, jeśli zawiera defaultmetodę, a klasa implementująca interfejs zostanie zainicjowana.
Sotirios Delimanolis
Myślę, że ludzie java zapomnieli rozważyć przypadek domyślnej metody lub po prostu odnoszą się do domyślnych metod jako niestałych elementów interfejsu (moje założenie, nie można znaleźć w żadnym dokumencie).
To nie jest błąd
1
@KishanSarsechaGajjar: Co masz na myśli mówiąc o zmiennym polu w interfejsie? Każda zmienna / pole w interfejsie jest domyślnie statyczna.
Lokesh
10

Plik instanceKlass.cpp z OpenJDK zawiera metodę inicjalizacji, InstanceKlass::initialize_implktóra odpowiada szczegółowej procedurze inicjalizacji w JLS, którą analogicznie można znaleźć w pliku inicjalizacji sekcji w specyfikacji JVM.

Zawiera nowy krok, o którym nie ma mowy w JLS, a nie w książce JVM, do której odnosi się kod:

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

Tak więc ta inicjalizacja została zaimplementowana jawnie jako nowy krok 7.5 . Oznacza to, że ta implementacja była zgodna z pewną specyfikacją, ale wydaje się, że pisemna specyfikacja na stronie internetowej nie została odpowiednio zaktualizowana.

EDYCJA: Jako odniesienie, zatwierdzenie (od października 2012!), W którym odpowiedni krok został uwzględniony w implementacji: http://hg.openjdk.java.net/jdk8/build/hotspot/rev/4735d2c84362

EDIT2: Przypadkowo znalazłem ten dokument o domyślnych metodach w hotspocie, który zawiera interesującą uwagę na końcu:

3.7 Różne

Ponieważ interfejsy mają teraz w sobie kod bajtowy, musimy je zainicjować w momencie inicjalizacji klasy implementującej.

Marco13
źródło
1
Dzięki za odkopanie tego. (+1) Może się zdarzyć, że nowy „krok 7.5” został nieumyślnie pominięty w specyfikacji lub że został zaproponowany i odrzucony, a implementacja nigdy nie została naprawiona, aby go usunąć.
Stuart Marks
1

Postaram się wykazać, że inicjalizacja interfejsu nie powinna powodować żadnych skutków ubocznych kanału bocznego, od których zależą podtypy, dlatego czy jest to błąd, czy nie, lub jakikolwiek sposób naprawi go Java, nie powinno to mieć znaczenia aplikacja, w której są inicjowane interfejsy.

W przypadku a class, powszechnie przyjmuje się, że może powodować skutki uboczne, od których zależą podklasy. Na przykład

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

Dowolna podklasa Foo spodziewałaby się, że zobaczy w banku 1000 USD w dowolnym miejscu kodu podklasy. Dlatego nadklasa jest inicjowana przed podklasą.

Czy nie powinniśmy zrobić tego samego również dla superintefacji? Niestety kolejność superinterfejsów nie ma być znacząca, dlatego nie ma dobrze zdefiniowanej kolejności ich inicjalizacji.

Dlatego lepiej nie ustalać tego rodzaju skutków ubocznych podczas inicjalizacji interfejsu. W końcu interfacenie jest przeznaczony dla tych funkcji (pól / metod statycznych), które stosujemy dla wygody.

Dlatego jeśli będziemy postępować zgodnie z tą zasadą, nie będzie nas obchodziła kolejność inicjalizacji interfejsów.

ZhongYu
źródło