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

2022-08-31 10:40:09

在搜索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

但是,如果接口声明了一个方法,则确实会进行初始化。将给定的接口视为defaultInterfaceType

interface InterfaceType {
    public static final ClassInitializer init = new ClassInitializer();

    public default void method() {
        System.out.println("default method");
    }
}

然后上面的相同程序将打印

static initializer  
implemented method

换句话说,接口的字段被初始化(详细初始化过程中的步骤 9),并且正在初始化的类型初始值设定项被执行。这意味着接口已初始化。staticstatic

我在JLS中找不到任何东西来表明这应该发生。不要误会我的意思,我知道这应该发生在实现类没有为方法提供实现的情况下,但是如果它这样做呢?Java语言规范中是否缺少此条件,我是否遗漏了某些内容,或者我错误地解释了它?


答案 1

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

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

事实上,我得到了预期的输出。但是,如果将默认方法添加到接口 ,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 实现希望避免将类/接口初始化检查添加到调用的关键路径中。在Java 8和默认方法之前,永远不会最终在接口中执行代码,因此没有出现这种情况。有人可能会认为这是类/接口准备阶段(JLS 12.3.2)的一部分,它初始化了方法表之类的东西。但也许这太过分了,意外地做了完全初始化。invokevirtualinvokevirtual

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

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


答案 2

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

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

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

  • 常量字段在代码中使用。
  • 接口包含默认方法(Java 8)

如果是默认方法,则您正在实现 。因此,If 将包含任何默认方法,它将在实现类时被继承(使用)。初始化将进入画面。InterfaceTypeInterfaceType

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

请考虑以下代码。

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,可能有两种可能性

  • Java人忘了考虑默认方法的情况。(规范文档错误。
  • 它们只是将默认方法引用为接口的非常量成员。(但没有提到哪里,再次规范文档错误。