何时使用默认方法初始化接口?


94

当搜寻通过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
我的猜测是-这样的接口在初始化顺序方面被视为抽象类。我写这条评论是因为我不确定这是否正确:)
Alexey Malev 2014年

它应该在JLS的12.4节中,但似乎不存在。我会说它不见了。
沃伦·露

1
没关系....很多时候他们不明白或不具备他们将downvote解释的时间:(。这发生在所以一般。
NeverGiveUp161

我认为interface在Java中不应定义任何具体方法。所以我很惊讶InterfaceType代码已经编译。
MaxZoom 2015年

Answers:


85

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

似乎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编译器-dev邮件列表中提出了这个问题Alex Buckley(JLS的编辑)收到了答复,他提出了针对JVM和lambda实现团队的更多问题。他还指出,规范中存在一个错误,该错误表明“ T是一个类,并且调用了T声明的静态方法”,如果T是接口,则也应适用。因此,这里可能同时存在规范和HotSpot错误。

披露:我在OpenJDK上为Oracle工作。如果人们认为这给我带来了悬赏这个问题的不公平优势,那么我愿意对此灵活。


6
我要求提供官方消息。我认为没有比这更正式的了。请花两天时间查看所有进展。
Sotirios Delimanolis 2014年

48
@StuartMarks“ 如果人们认为这给了我不公平的好处,等等 ” =>我们在这里得到问题的答案,这是一个完美的答案!
assylias 2014年

2
附带说明:JVM规范包含与JLS相似的描述:docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5也应更新。
Marco13年

2
@assylias和Sotirios,感谢您的评论。他们,连同14条关于亚述评论的支持(截至撰写本文时),减轻了我对任何潜在不公平现象的担忧。
Stuart Marks 2014年

1
@SotiriosDelimanolis JDK-8043275JDK- 8043190有一些似乎很相关的错误,它们被标记为在8u40中已修复。但是,行为似乎是相同的。与此相关的还有一些JVM Spec更改,因此,此修复程序可能是“恢复旧的初始化顺序”之外的事情。
Stuart Marks 2015年

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人忘记考虑默认方法的情况。(规范文档错误。)
  • 他们只是将默认方法称为接口的非恒定成员。(但没有提到,再次出现了规范文档错误。)

我正在寻找默认方法的参考。该字段只是为了演示接口是否已初始化。
Sotirios Delimanolis 2014年

@SotiriosDelimanolis我在默认方法的答案中提到了原因...但是很遗憾,尚未为默认方法找到任何JLS。
不是错误

不幸的是,这就是我想要的。我觉得您的回答只是重复我在问题中已经说过的事情,即。如果接口包含default方法并初始化实现该接口的类,则该接口将被初始化。
Sotirios Delimanolis 2014年

我认为Java人忘了考虑默认方法的情况,或者他们只是将默认方法称为接口的非恒定成员(我的假设是,在任何文档中都找不到)。
不是错误

1
@KishanSarsechaGajjar:接口中的非常量字段是什么意思?默认情况下,接口中的任何变量/字段都是static final。
Lokesh 2014年

10

所述instanceKlass.cpp从OpenJDK的文件中包含的初始化方法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

EDIT2:巧合的是,我在热点中找到了有关默认方法的文档,该文档的末尾包含一个有趣的注释:

3.7其他

因为接口现在包含字节码,所以我们必须在初始化实现类时对它们进行初始化。


1
感谢您对此进行深入研究。(+1)可能是新的“步骤7.5”在规范中被无意中省略了,或者是它被提议并被拒绝了,并且实现从未被确定要删除它。
斯图尔特(Stuart Marks)2014年

1

我将尝试说明接口初始化不应引起子类型所依赖的任何副作用,因此,无论这是否是错误,还是Java修复的任何方式,都无所谓顺序接口初始化的应用程序。

在a的情况下class,它会引起子类所依赖的副作用是众所周知的。例如

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

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

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

因此,我们最好不要在接口初始化中建立这种副作用。毕竟,interface我们并不为了方便起见而将这些功能(静态字段/方法)用于这些功能。

因此,如果我们遵循该原则,那么对顺序接口进行初始化的情况就不用担心了。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.