¿Cuándo se inicializa una interfaz con un método predeterminado?

94

Mientras buscaba en la Especificación del lenguaje Java para responder a esta pregunta , aprendí que

Antes de que se inicialice una clase, se debe inicializar su superclase directa, pero las interfaces implementadas por la clase no se inicializan. De manera similar, las superinterfaces de una interfaz no se inicializan antes de que se inicialice la interfaz.

Por mi propia curiosidad, lo probé y, como esperaba, la interfaz InterfaceTypeno se inicializó.

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

Este programa imprime

implemented method

Sin embargo, si la interfaz declara un defaultmétodo, se produce la inicialización. Considere la InterfaceTypeinterfaz dada como

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

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

entonces el mismo programa anterior imprimiría

static initializer  
implemented method

En otras palabras, staticse inicializa el campo de la interfaz ( paso 9 en el Procedimiento de inicialización detallado ) y staticse ejecuta el inicializador del tipo que se está inicializando. Esto significa que se inicializó la interfaz.

No pude encontrar nada en el JLS que indique que esto debería suceder. No me malinterpretes, entiendo que esto debería suceder en caso de que la clase de implementación no proporcione una implementación para el método, pero ¿y si lo hace? ¿Falta esta condición en la Especificación del lenguaje Java, me perdí algo o lo estoy interpretando incorrectamente?

Sotirios Delimanolis
fuente
4
Mi conjetura sería: tales interfaces se consideran clases abstractas en términos de orden de inicialización. Escribí esto como un comentario ya que no estoy seguro de si esta es la declaración correcta :)
Alexey Malev
Debería estar en la sección 12.4 de la JLS, pero no parece estar allí. Yo diría que falta.
Warren Dew
1
No importa .... la mayoría de las veces, cuando no entienden o no tienen una explicación
votarán negativamente
Pensé que interfaceen Java no se debería definir ningún método concreto. Así que me sorprende que el InterfaceTypecódigo se haya compilado.
MaxZoom
@MaxZoom Java 8 permite defaultmétodos .
Sotirios Delimanolis

Respuestas:

85

¡Este es un tema muy interesante!

Parece que la sección 12.4.1 de JLS debería cubrir esto definitivamente. Sin embargo, el comportamiento de Oracle JDK y OpenJDK (javac y HotSpot) difiere de lo que se especifica aquí. En particular, el ejemplo 12.4.1-3 de esta sección cubre la inicialización de la interfaz. El ejemplo de la siguiente manera:

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

Su salida esperada es:

1
j=3
jj=4
3

y de hecho obtengo el resultado esperado. Sin embargo, si se agrega un método predeterminado a la interfaz I,

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

la salida cambia a:

1
ii=2
j=3
jj=4
3

lo que indica claramente que la interfaz Ise está inicializando donde no estaba antes. La mera presencia del método predeterminado es suficiente para activar la inicialización. El método predeterminado no tiene que ser llamado, anulado o incluso mencionado, ni la presencia de un método abstracto desencadena la inicialización.

Mi especulación es que la implementación de HotSpot quería evitar agregar la verificación de inicialización de clase / interfaz en la ruta crítica de la invokevirtualllamada. Antes de Java 8 y los métodos predeterminados, invokevirtualnunca podría terminar ejecutando código en una interfaz, por lo que esto no surgió. Uno podría pensar que esto es parte de la etapa de preparación de la clase / interfaz ( JLS 12.3.2 ) que inicializa cosas como tablas de métodos. Pero quizás esto fue demasiado lejos y accidentalmente hizo una inicialización completa en su lugar.

He planteado esta pregunta en la lista de correo del desarrollador-compilador de OpenJDK. Ha habido una respuesta de Alex Buckley (editor de JLS) en la que plantea más preguntas dirigidas a los equipos de implementación de JVM y lambda. También señala que hay un error en la especificación aquí donde dice "T es una clase y se invoca un método estático declarado por T" también debería aplicarse si T es una interfaz. Por lo tanto, es posible que haya errores de especificación y HotSpot aquí.

Divulgación : trabajo para Oracle en OpenJDK. Si la gente piensa que esto me da una ventaja injusta para obtener la recompensa adjunta a esta pregunta, estoy dispuesto a ser flexible al respecto.

Stuart Marks
fuente
6
Pregunté por fuentes oficiales. No creo que sea más oficial que esto. Dale dos días para ver todos los desarrollos.
Sotirios Delimanolis
48
@StuartMarks " Si la gente piensa que esto me da una ventaja injusta, etc. " => estamos aquí para obtener respuestas a preguntas y esta es la respuesta perfecta.
assylias
2
Una nota al margen: La especificación de JVM contiene una descripción similar a la de JLS: docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5 Esto también debería actualizarse .
Marco13
2
@assylias y Sotirios, gracias por tus comentarios. Ellos, junto con los 14 votos a favor (al momento de escribir este artículo) sobre el comentario de Assylias, han aliviado mis preocupaciones sobre cualquier posible injusticia.
Stuart Marks
1
@SotiriosDelimanolis Hay un par de errores que parecen relevantes, JDK-8043275 y JDK-8043190 , y están marcados como corregidos en 8u40. Sin embargo, el comportamiento parece ser el mismo. También hubo algunos cambios de especificaciones de JVM entrelazados con esto, por lo que quizás la solución sea algo diferente a "restaurar el orden de inicialización anterior".
Stuart Marks
13

La interfaz no se inicializa porque el campo constante InterfaceType.init, que se inicializa mediante un valor no constante (llamada al método), no se utiliza en ningún lugar.

Se sabe en el momento de la compilación que el campo constante de la interfaz no se usa en ningún lugar y que la interfaz no contiene ningún método predeterminado (en java-8), por lo que no es necesario inicializar o cargar la interfaz.

La interfaz se inicializará en los siguientes casos,

  • El campo constante se usa en su código.
  • La interfaz contiene un método predeterminado (Java 8)

En caso de métodos predeterminados , está implementando InterfaceType. Entonces, si InterfaceTypecontendrá cualquier método predeterminado, será HEREDADO (usado) en la implementación de la clase. Y la inicialización estará en la imagen.

Pero, si está accediendo al campo constante de la interfaz (que se inicializa de manera normal), la inicialización de la interfaz no es necesaria.

Considere el siguiente código.

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

En el caso anterior, la interfaz se inicializará y cargará porque está utilizando el campo InterfaceType.init.

No estoy dando el ejemplo del método predeterminado, ya que ya lo dio en su pregunta.

La especificación y el ejemplo del lenguaje Java se dan en JLS 12.4.1 (El ejemplo no contiene métodos predeterminados).


No puedo encontrar JLS para los métodos predeterminados, puede haber dos posibilidades

  • La gente de Java se olvidó de considerar el caso del método predeterminado. (Error del documento de especificación).
  • Simplemente se refieren a los métodos predeterminados como miembro no constante de la interfaz. (Pero no mencioné ningún lugar, nuevamente el error del documento de especificación).
No es un error
fuente
Estoy buscando una referencia para el método predeterminado. El campo fue solo para demostrar que la interfaz se inicializó o no.
Sotirios Delimanolis
@SotiriosDelimanolis Mencioné el motivo en la respuesta para el método predeterminado ... pero desafortunadamente aún no se ha encontrado ningún JLS para el método predeterminado.
No es un error
Desafortunadamente, eso es lo que estoy buscando. Siento que su respuesta es simplemente repetir cosas que ya dije en la pregunta, es decir. que una interfaz se inicializará si contiene un defaultmétodo y se inicializa una clase que implementa la interfaz.
Sotirios Delimanolis
Creo que la gente de Java se olvidó de considerar el caso del método predeterminado, o simplemente se refieren a los métodos predeterminados como un miembro no constante de la interfaz (mi suposición, no se puede encontrar en ningún documento).
No es un error
1
@KishanSarsechaGajjar: ¿Qué quieres decir con campo no constante en la interfaz? Cualquier variable / campo en la interfaz es estático final por defecto.
Lokesh
10

El archivo instanceKlass.cpp de OpenJDK contiene el método de inicialización InstanceKlass::initialize_implque corresponde al procedimiento de inicialización detallado en JLS, que se encuentra de forma análoga en la sección Inicialización de la especificación de JVM.

Contiene un nuevo paso que no se menciona en el JLS y no en el libro de JVM al que se hace referencia en el código:

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

Por tanto, esta inicialización se ha implementado explícitamente como un nuevo Paso 7.5 . Esto indica que esta implementación siguió alguna especificación, pero parece que la especificación escrita en el sitio web no se ha actualizado en consecuencia.

EDITAR: Como referencia, el compromiso (¡de octubre de 2012!) Donde se ha incluido el paso respectivo en la implementación: http://hg.openjdk.java.net/jdk8/build/hotspot/rev/4735d2c84362

EDIT2: Casualmente, encontré este documento sobre métodos predeterminados en hotspot que contiene una nota al margen interesante al final:

3.7 Varios

Debido a que las interfaces ahora tienen un código de bytes, debemos inicializarlas en el momento en que se inicializa una clase de implementación.

Marco13
fuente
1
Gracias por desenterrar esto. (+1) Puede ser que el nuevo "paso 7.5" se haya omitido inadvertidamente de la especificación, o que se propuso y rechazó y la implementación nunca se corrigió para eliminarlo.
Stuart Marks
1

Intentaré argumentar que la inicialización de una interfaz no debería causar ningún efecto secundario de canal lateral de los que dependan los subtipos, por lo tanto, ya sea que se trate de un error o no, o de cualquier forma en que Java lo solucione, no debería importar. la aplicación en la que se inicializan las interfaces.

En el caso de a class, está bien aceptado que puede causar efectos secundarios de los que dependen las subclases. Por ejemplo

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

Cualquier subclase de Fooesperaría ver $ 1000 en el banco, en cualquier parte del código de la subclase. Por lo tanto, la superclase se inicializa antes que la subclase.

¿No deberíamos hacer lo mismo con las superintefaces también? Desafortunadamente, el orden de las superinterfaces no se supone que sea significativo, por lo tanto, no existe un orden bien definido para inicializarlas.

Así que es mejor que no establezcamos este tipo de efectos secundarios en las inicializaciones de la interfaz. Después de todo, interfaceno está diseñado para estas características (campos / métodos estáticos) que acumulamos por conveniencia.

Por lo tanto, si seguimos ese principio, no nos preocupará en qué orden se inicializan las interfaces.

ZhongYu
fuente