Когда интерфейс инициализирован по умолчанию?

При поиске по языковой спецификации Java для ответа на этот вопрос я узнал что

Прежде чем инициализировать класс, его прямой суперкласс должен быть инициализировано, , но интерфейсы, реализованные классом, не являются инициализировано. Аналогично, суперинтерфейсы интерфейса не являются инициализируется до инициализации интерфейса.

Для моего собственного любопытства я попробовал это и, как и ожидалось, интерфейс InterfaceType не был инициализирован.

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

Эта программа печатает

implemented method

Однако, если интерфейс объявляет метод default, тогда происходит инициализация. Рассмотрим интерфейс InterfaceType, указанный как

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

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

тогда одна и та же программа напечатала бы

static initializer  
implemented method

Иными словами, поле static интерфейса инициализируется (шаг 9 в подробной процедуре инициализации) и static выполняется инициализатор инициализированного типа. Это означает, что интерфейс был инициализирован.

Я не мог найти ничего в JLS, чтобы указать, что это должно произойти. Не поймите меня неправильно, я понимаю, что это должно произойти в случае, если класс реализации не обеспечивает реализацию метода, но что, если это произойдет? Является ли это условие отсутствующим в спецификации языка Java, я что-то пропустил или неправильно интерпретирую его?

Ответ 1

Это очень интересная проблема!

Кажется, что JLS раздел 12.4.1 должен охватывать это окончательно. Однако поведение Oracle JDK и OpenJDK (javac и HotSpot) отличается от того, что указано здесь. В частности, пример 12.4.1-3 в этом разделе посвящен инициализации интерфейса. Пример следующим образом:

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

Ожидаемый результат:

1
j=3
jj=4
3

и я получаю ожидаемый результат. Однако, если в интерфейс I добавлен метод по умолчанию,

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

выход изменяется на:

1
ii=2
j=3
jj=4
3

который четко указывает, что интерфейс I инициализируется там, где он не был раньше! Простое наличие метода по умолчанию достаточно, чтобы инициировать инициализацию. Метод по умолчанию не должен вызываться или переопределяться или даже упоминаться, а также отсутствие инициализации абстрактного метода.

Мое предположение заключается в том, что реализация HotSpot не позволяла добавлять проверку инициализации класса/интерфейса на критический путь вызова invokevirtual. До появления Java 8 и методов по умолчанию invokevirtual никогда не закончил выполнение кода в интерфейсе, поэтому этого не произошло. Можно подумать, что это часть этапа подготовки класса/интерфейса (JLS 12.3.2), который инициализирует такие вещи, как таблицы методов. Но, возможно, это зашло слишком далеко и случайно сделало полную инициализацию.

Я поднял этот вопрос в списке рассылки OpenJDK compiler-dev. Был ответ

Раскрытие. Я работаю в Oracle на OpenJDK. Если люди думают, что это дает мне несправедливое преимущество в получении щедрости, связанной с этим вопросом, я готов проявить гибкость в этом вопросе.

Ответ 2

Интерфейс не инициализирован, потому что константное поле InterfaceType.init, которое инициализируется не постоянным значением (вызов метода), нигде не используется.

Во время компиляции известно, что постоянное поле интерфейса нигде не используется, и интерфейс не содержит никакого метода по умолчанию (в java-8), поэтому нет необходимости инициализировать или загружать интерфейс.

Интерфейс будет инициализирован в следующих случаях:

  • постоянное поле используется в вашем коде.
  • Интерфейс содержит метод по умолчанию (Java 8)

В случае Методы по умолчанию вы реализуете InterfaceType. Итак, If InterfaceType будет содержать любые методы по умолчанию, он будет INHERITED (используется) в реализации класса. И инициализация будет в картине.

Но если вы обращаетесь к постоянному полю интерфейса (который инициализируется обычным образом), инициализация интерфейса не требуется.

Рассмотрим следующий код.

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

В приведенном выше случае интерфейс будет инициализирован и загружен, потому что вы используете поле InterfaceType.init.

Я не даю пример метода по умолчанию, как вы уже указали в своем вопросе.

Спецификация и пример языка Java приведены в JLS 12.4.1 (пример не содержит методов по умолчанию).


Я не могу найти JLS для методов по умолчанию, возможны две возможности

  • Java-люди забыли рассмотреть случай метода по умолчанию. (Ошибка спецификации Doc.)
  • Они просто ссылаются на методы по умолчанию как на непостоянный член интерфейс. (Но не упоминается, где, опять ошибка спецификации Doc.)

Ответ 3

Файл instanceKlass.cpp из OpenJDK содержит метод инициализации InstanceKlass::initialize_impl, который соответствует Подробная процедура инициализации в JLS, аналогично найденная в разделе Инициализация в JVM Spec.

Он содержит новый шаг, который не упоминается в JLS, а не в JVM-книге, которая упоминается в коде:

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

Итак, эта инициализация была реализована явно как новый Шаг 7.5. Это указывает на то, что эта реализация соответствовала некоторой спецификации, но кажется, что письменная спецификация на веб-сайте не была соответствующим образом обновлена.

EDIT: как ссылка, фиксация (с октября 2012 года!), где соответствующий шаг был включен в реализацию: http://hg.openjdk.java.net/jdk8/build/hotspot/rev/4735d2c84362

EDIT2: Кстати, я нашел этот Документ о методах по умолчанию в hotspot, который содержит интересную боковую заметку в конце:

3.7 Разное

Поскольку интерфейсы теперь имеют в них байт-код, мы должны инициализировать их на время инициализации класса реализации.

Ответ 4

Я попытаюсь сделать так, что инициализация интерфейса не должна вызывать побочные эффекты побочных каналов, от которых зависят подтипы, поэтому, является ли это ошибкой или нет, или каким бы способом она не исправлялась, она должна не имеет значения для приложения, в котором инициализируются интерфейсы заказа.

В случае class, считается, что он может вызывать побочные эффекты, от которых зависят подклассы. Например

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

Любой подкласс Foo ожидает, что в банке он увидит 1000 долларов в любом месте кода подкласса. Поэтому суперклас инициализируется перед подклассом.

Разве мы не должны делать то же самое для суперинтеза? К сожалению, порядок суперинтерфейсов не должен быть значительным, поэтому нет четкого порядка, в котором их инициализировать.

Таким образом, мы лучше не устанавливаем подобные побочные эффекты при инициализации интерфейса. В конце концов, interface не предназначен для этих функций (статические поля/методы), которые мы собираем для удобства.

Поэтому, если мы будем следовать этому принципу, нам будет неинтересно, в каких интерфейсах порядка инициализируются.