interfaz - metodo default java
¿Cuándo se inicializa una interfaz con un método predeterminado? (4)
¡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 especificado 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 resultado esperado 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 I
se 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 llamarse, anularse o incluso mencionarse, 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 invokevirtual
call. Antes de Java 8 y los métodos predeterminados, invokevirtual
nunca 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 clase / interfaz ( JLS 12.3.2 ) que inicializa cosas como tablas de métodos. Pero tal vez esto fue demasiado lejos y accidentalmente hizo la inicialización completa en su lugar.
He planteado esta pregunta en la lista de distribución del compilador de OpenJDK. Hubo una respuesta de Alex Buckley (editor del JLS) en el 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 que "T es una clase y un método estático declarado por T es invocado" también debería aplicarse si T es una interfaz. Por lo tanto, es posible que existan 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 asociada a esta pregunta, estoy dispuesto a ser flexible al respecto.
Mientras buscaba en la Especificación del lenguaje Java para responder esta pregunta , aprendí that
Antes de inicializar una clase, su superclase directa debe inicializarse, pero las interfaces implementadas por la clase no se inicializan. De forma similar, las superinterfaces de una interfaz no se inicializan antes de que se inicialice la interfaz.
Para mi propia curiosidad, lo intenté y, como era de esperar, la interfaz InterfaceType
no 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 método default
, entonces la inicialización ocurre. Considere la InterfaceType
InterfaceType 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, el campo static
de la interfaz se inicializa ( that ) y se ejecuta el inicializador static
del tipo que se está inicializando. Esto significa que la interfaz fue inicializada.
No pude encontrar nada en el JLS para indicar que esto debería suceder. No me malinterpreten, 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? ¿Extrañé algo, o lo estoy interpretando incorrectamente?
El archivo instanceKlass.cpp de OpenJDK contiene el método de inicialización InstanceKlass::initialize_impl
que corresponde al Procedimiento de inicialización detallada en el JLS, que se encuentra análogamente en la sección docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5 en la especificación de JVM.
Contiene un nuevo paso que no se menciona en el JLS ni 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);
....
}
}
}
Entonces esta inicialización se ha implementado explícitamente como un nuevo Paso 7.5 . Esto indica que esta implementación siguió algunas especificaciones, pero parece que la especificación escrita en el sitio web no se ha actualizado en consecuencia.
EDITAR: como referencia, el compromiso (desde octubre de 2012!) Donde se ha incluido el paso correspondiente 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 código de bytes en ellas, debemos inicializarlas en el momento en que se inicializa una clase de implementación.
La interfaz no se inicializa porque el campo constante InterfaceType.init
, que se está inicializando mediante un valor no constante (llamada de método), no se usa en ninguna parte.
En el momento de la compilación, se sabe que el campo constante de la interfaz no se usa en ninguna parte, y 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,
- campo constante se usa en tu código.
- La interfaz contiene un método predeterminado (Java 8)
En el caso de los métodos predeterminados , está implementando InterfaceType
. Entonces, si InterfaceType
contendrá 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), no se requiere la inicialización de la interfaz.
Considera seguir el 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 como ya lo mencionó en su pregunta.
La especificación del lenguaje Java y el ejemplo se proporcionan 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 olvidó considerar el caso del método predeterminado. (Error de especificación de Doc.)
- Simplemente refieren los métodos predeterminados como miembro no constante de la interfaz. (Pero mencionó que no, de nuevo, error de especificación Doc.)
Trataré de argumentar que una inicialización de interfaz no debería causar ningún efecto secundario de canal lateral de los que dependan los subtipos, por lo tanto, si esto es un error o no, o cualquiera que sea la forma en que Java lo corrija, no debería importarle la aplicación en la que se inicializan las interfaces de órdenes.
En el caso de una class
, se acepta que puede causar efectos secundarios de los que dependen las subclases. Por ejemplo
class Foo{
static{
Bank.deposit($1000);
...
Cualquier subclase de Foo
esperaría que vean $ 1000 en el banco, en cualquier parte del código de la subclase. Por lo tanto, la superclase se inicializa antes de la subclase.
¿No deberíamos hacer lo mismo para superintefacciones también? Desafortunadamente, el orden de las superinterfaces no debe ser significativo, por lo tanto, no hay un orden bien definido para inicializarlas.
Por lo tanto, es mejor que no establezcamos este tipo de efectos secundarios en las inicializaciones de interfaz. Después de todo, la interface
no está diseñada para estas características (campos / métodos estáticos) que agregamos para mayor comodidad.
Por lo tanto, si seguimos ese principio, no nos importará en qué orden se inicializan las interfaces.