【问题标题】:When is an interface with a default method initialized?何时初始化具有默认方法的接口?
【发布时间】:2014-04-15 23:02:42
【问题描述】:

在搜索 Java 语言规范以回答 this question 时,我了解到 that

在一个类被初始化之前,它的直接超类必须是 已初始化,但类实现的接口不是 已初始化。 类似地,接口的超接口不是 在接口初始化之前初始化。

出于自己的好奇,我试了一下,果然没有初始化接口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字段被初始化(step 9 in the Detailed Initialization Procedure)并且被初始化类型的static初始化器被执行。这意味着接口已初始化。

我在 JLS 中找不到任何表明应该发生这种情况的内容。不要误会我的意思,我知道这应该发生在实现类没有为该方法提供实现的情况下,但如果它提供了怎么办? Java 语言规范中是否缺少此条件,是我遗漏了什么,还是我解释错误?

【问题讨论】:

  • 我的猜测是——这样的接口在初始化顺序方面被认为是抽象类。我将其写为评论,因为我不确定这是否是正确的陈述:)
  • 它应该在 JLS 的第 12.4 节中,但似乎没有。我会说它不见了。
  • 没关系....大多数情况下,当他们不理解或没有解释时,他们会投反对票:(。这通常发生在 SO 上。
  • 我认为Java中的interface不应该定义任何具体的方法。所以我很惊讶InterfaceType 代码已经编译。

标签: java interface java-8 default-method


【解决方案1】:

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

似乎JLS section 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) 的一部分,它初始化方法表之类的东西。但也许这太过分了,不小心做了完整的初始化。

我的 raised this question 在 OpenJDK 编译器开发邮件列表中。有一个reply from Alex Buckley(JLS 的编辑)在其中提出了更多针对 JVM 和 lambda 实现团队的问题。他还指出,这里的规范中有一个错误,其中说“T 是一个类,并且调用了 T 声明的静态方法”如果 T 是一个接口也应该适用。因此,这里可能同时存在规范和 HotSpot 错误。

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

【讨论】:

  • 我询问了官方消息来源。我认为没有比这更正式的了。给它两天时间看看所有的发展。
  • @StuartMarks "如果人们认为这给了我不公平的优势,等等" =>我们来这里是为了得到问题的答案,这是一个完美的答案!
  • 附注:JVM 规范包含与 JLS 类似的描述:docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5 这也应该更新。
  • @assylias 和 Sotirios,感谢您的 cmets。他们以及对 assylias 评论的 14 次投票(截至撰写本文时)减轻了我对任何潜在不公平的担忧。
  • @SotiriosDelimanolis 有几个似乎相关的错误,JDK-8043275JDK-8043190,它们在 8u40 中被标记为已修复。但是,行为似乎是相同的。还有一些与此交织在一起的 JVM 规范更改,因此修复可能不是“恢复旧的初始化顺序”。
【解决方案2】:

接口没有被初始化,因为常量字段 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 人忘记考虑默认方法的情况。 (规范文档错误。)
  • 他们只是将默认方法称为非常量成员 界面。 (但没有提到哪里,又是规范文档错误。)

【讨论】:

  • 我正在寻找默认方法的参考。该字段只是为了证明接口是否已初始化。
  • @SotiriosDelimanolis 我在回答默认方法时提到了原因......但不幸的是,还没有找到默认方法的任何 JLS。
  • 不幸的是,这就是我要找的。我觉得你的回答只是重复我在问题中已经说过的事情,即。如果接口包含default 方法并且实现该接口的类已初始化,则该接口将被初始化。
  • 我认为java人忘记考虑默认方法的情况,或者他们只是将默认方法称为接口的非常量成员(我的假设,在任何文档中都找不到)。
  • @KishanSarsechaGajjar:接口中的非常量字段是什么意思?默认情况下,接口中的任何变量/字段都是静态最终的。
【解决方案3】:

来自 OpenJDK 的 instanceKlass.cpp 文件包含初始化方法 InstanceKlass::initialize_impl,对应于 JLS 中的 Detailed Initialization Procedure,类似地可以在 JVM 规范的 Initialization 部分找到。

它包含一个新的步骤,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

EDIT2:巧合的是,我发现了这个Document about default methods in hotspot,它的末尾有一个有趣的旁注:

3.7 杂项

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

【讨论】:

  • 感谢您挖掘这个。 (+1) 可能是新的“步骤 7.5”被无意中从规范中省略了,或者它被提出并被拒绝,并且从未修复实现以将其删除。
【解决方案4】:

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

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

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 2015-01-11
    • 2017-05-26
    • 2017-08-16
    • 1970-01-01
    • 2020-02-08
    • 1970-01-01
    • 2021-07-28
    • 2022-01-19
    • 2018-01-07
    相关资源
    最近更新 更多