何时是初始化默认方法的接口?

时间:2014-04-15 23:02:42

标签: java interface java-8 default-method

在搜索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语言规范是否缺少这种情况,我是否遗漏了某些内容,或者我是否错误地解释了它?

4 个答案:

答案 0 :(得分:81)

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

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

我在OpenJDK compiler-dev邮件列表上raised this question。曾经是reply from Alex Buckley(JLS的编辑),他提出了更多针对JVM和lambda实施团队的问题。他还指出,这里的规范中存在一个错误,它表示" T是一个类,并且调用T声明的静态方法"如果T是一个接口,也应该适用。因此,这里可能存在规范和HotSpot错误。

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

答案 1 :(得分:13)

接口未初始化,因为常量字段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字段,因此将初始化和加载接口。

我没有给出你在问题中已经给出的默认方法示例。

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


我找不到JLS for Default方法,可能有两种可能性

  • Java人们忘了考虑默认方法的情况。 (规范Doc bug。)
  • 他们只是将默认方法称为非常量成员 接口。 (但没有提到哪里,再次说明Doc Doc bug。)

答案 2 :(得分:10)

OpenJDK中的instanceKlass.cpp文件包含与JLS中Detailed Initialization Procedure对应的初始化方法InstanceKlass::initialize_impl,类似于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);
    ....
    }
  }
}

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

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

EDIT2:巧合的是,我发现这个Document about default methods in hotspot最后包含了一个有趣的旁注:

  

3.7其他

     

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

答案 3 :(得分:1)

我将尝试假设接口初始化不应导致子类型所依赖的任何副通道副作用,因此,无论这是否是错误,或者Java修复它的方式,它应该对于初始化订单接口的应用程序无关紧要。

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

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

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

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

因此,我们最好不要在界面初始化中建立这种副作用。毕竟,interface并不意味着我们为方便起见而使用的这些功能(静态字段/方法)。

因此,如果我们遵循这个原则,我们就不用担心初始化哪个顺序接口。