java 何时初始化具有默认方法的接口?

yshpjwxd  于 2022-12-02  发布在  Java
关注(0)|答案(4)|浏览(131)

在搜索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方法,则会进行初始化。

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语言规范中缺少了这种条件,还是我遗漏了什么,或者我解释错了它?

watbbzwu

watbbzwu1#

This is a very interesting issue!
It seems like JLS section 12.4.1 ought to cover this definitively. However, the behavior of Oracle JDK and OpenJDK (javac and HotSpot) differs from what's specified here. In particular, the Example 12.4.1-3 from this section covers interface initialization. The example as follows:

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

Its expected output is:

1
j=3
jj=4
3

and indeed I get the expected output. However, if a default method is added to interface I ,

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

the output changes to:

1
ii=2
j=3
jj=4
3

which clearly indicates that interface I is being initialized where it wasn't before! The mere presence of the default method is enough to trigger the initialization. The default method doesn't have to be called or overridden or even mentioned, nor does the presence of an abstract method trigger initialization.
My speculation is that the HotSpot implementation wanted to avoid adding class/interface initialization checking into the critical path of the invokevirtual call. Prior to Java 8 and default methods, invokevirtual could never end up executing code in an interface, so this didn't arise. One might think this is part of the class/interface preparation stage (JLS 12.3.2) which initializes things like method tables. But perhaps this went too far and accidentally did full initialization instead.
I've raised this question on the OpenJDK compiler-dev mailing list. There's been a reply from Alex Buckley (editor of the JLS) in which he raises more questions directed at the JVM and lambda implementation teams. He also notes that there's a bug in the spec here where it says "T is a class and a static method declared by T is invoked" should also apply if T is an interface. So, it might be that there are both specification and HotSpot bugs here.

Disclosure*: I work for Oracle on OpenJDK. If people think this gives me an unfair advantage at getting the bounty attached to this question, I'm willing to be flexible about it.*

jv2fixgn

jv2fixgn2#

接口未初始化,因为常量字段InterfaceType.init(由非常量值(方法调用)初始化)未在任何位置使用。
在编译时已知接口的常量字段在任何地方都不使用,并且接口不包含任何默认方法(在java-8中),因此不需要初始化或加载接口。
在下列情况下将初始化接口,

  • 常量字段。
  • 接口包含默认方法(Java 8)

默认方法的情况下,您正在实现InterfaceType。因此,如果InterfaceType将包含任何默认方法,它将在实现类中INHERITED(used)。并且初始化将进入图片。
但是,如果您正在访问接口的常量字段(以正常方式初始化),则不需要进行接口初始化。
考虑以下代码。

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
我没有给出默认方法的例子,因为你在问题中已经给出了。

JLS 12.4.1中给出了Java语言规范和示例(示例不包含默认方法。)

找不到默认方法的JLS,可能有两种可能

  • Java的人们忘记考虑缺省方法的情况。(Specification Doc bug。)
  • 他们只是引用默认方法作为接口的非常数成员。(但没有提到在哪里,再次规范文档bug。)
2o7dmzc5

2o7dmzc53#

OpenJDK中的instanceKlass.cpp文件包含初始化方法InstanceKlass::initialize_impl,该方法对应于JLS中的详细初始化过程,类似地可以在JVM规范的初始化部分中找到。
它包含一个新步骤,该步骤在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实现。这表明此实现遵循某些规范,但网站上的书面规范似乎尚未相应更新。
编辑:作为参考,提交(从2012年10月!),其中相应的步骤已被包括在实现中:http://hg.openjdk.java.net/jdk8/build/hotspot/rev/4735d2c84362
编辑2:巧合的是,我发现了这个Document about default methods in hotspot,它在结尾处包含了一个有趣的边注:
3.7其他事项
因为接口中现在有字节码,所以我们必须在初始化实现类时初始化它们。

cotxawn7

cotxawn74#

我将尝试说明接口初始化不应导致子类型所依赖的任何副通道副作用,因此,无论这是否是一个bug,或者Java以何种方式修复它,接口初始化的顺序对应用程序来说都不重要。
class的情况下,它会导致子类依赖的副作用,这一点是公认的。

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

Foo的任何子类都希望在子类代码的任何地方看到银行里有1000美元,因此超类在子类之前初始化。
我们不应该对超接口做同样的事情吗?不幸的是,超接口的顺序并不重要,因此没有明确定义的初始化顺序。
因此,我们最好不要在接口初始化中建立这种副作用。毕竟,interface并不适用于我们为了方便而添加的这些特性(静态字段/方法)。
因此,如果我们遵循这个原则,我们就不会关心接口初始化的顺序。

相关问题