96

在搜索 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 语言规范中是否缺少此条件,是我遗漏了什么,还是我错误地解释了它?

4

4 回答 4

89

这是一个非常有趣的问题!

似乎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

事实上我得到了预期的输出。但是,如果将默认方法添加到 interface 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 编译器开发邮件列表上提出了这个问题。Alex Buckley(JLS 的编辑)已经回复,他在其中提出了更多针对 JVM 和 lambda 实现团队的问题。他还指出,这里的规范中有一个错误,其中说“T 是一个类,并且调用了 T 声明的静态方法”如果 T 是一个接口也应该适用。因此,这里可能同时存在规范和 HotSpot 错误。

披露:我在 OpenJDK 上为 Oracle 工作。如果人们认为这给了我一个不公平的优势来获得这个问题的赏金,我愿意对此采取灵活的态度。

于 2014-04-22T22:04:36.330 回答
13

接口未初始化,因为InterfaceType.init由非常量值(方法调用)初始化的常量字段未在任何地方使用。

在编译时就知道接口的常量字段不会在任何地方使用,并且接口不包含任何默认方法(在 java-8 中),因此不需要初始化或加载接口。

接口将在以下情况下初始化,

  • 您的代码中使用了常量字段。
  • 接口包含一个默认方法(Java 8)

默认方法的情况下,您正在实施InterfaceType。因此,如果InterfaceType将包含任何默认方法,它将在实现类中被继承(使用) 。初始化将进入图片。

但是,如果您正在访问接口的常量字段(以正常方式初始化),则不需要接口初始化。

考虑以下代码。

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 人忘记考虑默认方法的情况。(规范文档错误。)
  • 他们只是将默认方法称为接口的非常量成员。(但没有提到哪里,又是规范文档错误。)
于 2014-04-16T06:08:33.663 回答
10

OpenJDK 中的instanceKlass.cpp文件包含与JLS 中的详细初始化过程InstanceKlass::initialize_impl相对应的初始化方法,类似地可以在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);
    ....
    }
  }
}

因此,此初始化已明确实施为新的Step 7.5。这表明此实现遵循了一些规范,但似乎网站上的书面规范尚未相应更新。

编辑:作为参考,提交(从 2012 年 10 月开始!)其中相应步骤已包含在实施中:http: //hg.openjdk.java.net/jdk8/build/hotspot/rev/4735d2c84362

EDIT2:巧合的是,我发现了这个关于热点中默认方法的文档,其中最后包含一个有趣的旁注:

3.7 杂项

因为接口现在有字节码,所以我们必须在初始化实现类的时候初始化它们。

于 2014-04-23T09:31:20.077 回答
1

我将尝试说明接口初始化不应导致子类型所依赖的任何旁道副作用,因此,无论这是否是一个错误,或者无论 Java 以何种方式修复它,都无关紧要初始化订单接口的应用程序。

在 a 的情况下class,人们普遍认为它会导致子类所依赖的副作用。例如

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

任何子类Foo都希望他们会在银行中的任何子类代码中看到 1000 美元。因此,超类在子类之前初始化。

我们不应该对超级接口做同样的事情吗?不幸的是,超接口的顺序并不重要,因此没有明确定义的顺序来初始化它们。

所以我们最好不要在接口初始化中建立这种副作用。毕竟,interface这并不意味着我们为了方便而堆积的这些特性(静态字段/方法)。

因此,如果我们遵循该原则,我们将不关心接口的初始化顺序。

于 2014-04-22T22:38:06.280 回答