Quando uma interface com um método padrão é inicializada?

94

Enquanto pesquisava na Especificação da linguagem Java para responder a esta pergunta , aprendi que

Antes de uma classe ser inicializada, sua superclasse direta deve ser inicializada, mas as interfaces implementadas pela classe não são inicializadas. Da mesma forma, as superinterfaces de uma interface não são inicializadas antes de a interface ser inicializada.

Por curiosidade própria, experimentei e, como era de se esperar, a interface InterfaceTypenão foi inicializada.

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();
}

Este programa imprime

implemented method

No entanto, se a interface declara um defaultmétodo, a inicialização ocorre. Considere a InterfaceTypeinterface fornecida como

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

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

então o mesmo programa acima iria imprimir

static initializer  
implemented method

Em outras palavras, o staticcampo da interface é inicializado ( etapa 9 no Procedimento de inicialização detalhado ) e o staticinicializador do tipo que está sendo inicializado é executado. Isso significa que a interface foi inicializada.

Não consegui encontrar nada no JLS que indicasse que isso deveria acontecer. Não me entenda mal, eu entendo que isso deve acontecer caso a classe de implementação não forneça uma implementação para o método, mas e se fornecer? Esta condição está faltando na Especificação da linguagem Java, eu perdi algo ou estou interpretando incorretamente?

Sotirios Delimanolis
fonte
4
Meu palpite seria - tais interfaces consideradas classes abstratas em termos de ordem de inicialização. Escrevi isso como um comentário, pois não tenho certeza se esta é a afirmação correta :)
Alexey Malev
Deveria estar na seção 12.4 do JLS, mas não parece estar lá. Eu diria que está faltando.
Warren Dew de
1
Esqueça ... na maioria das vezes quando eles não entendem ou não têm uma explicação, eles votam negativamente :( .Isso acontece geralmente no SO.
NeverGiveUp161
Achei que interfaceem Java não deveria definir nenhum método concreto. Portanto, estou surpreso que o InterfaceTypecódigo foi compilado.
MaxZoom
@MaxZoom Java 8 permite defaultmétodos .
Sotirios Delimanolis

Respostas:

85

Este é um assunto muito interessante!

Parece que a seção 12.4.1 do JLS deve cobrir isso definitivamente. No entanto, o comportamento do Oracle JDK e OpenJDK (javac e HotSpot) difere do que está especificado aqui. Em particular, o Exemplo 12.4.1-3 desta seção cobre a inicialização da interface. O exemplo a seguir:

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;
    }
}

Sua produção esperada é:

1
j=3
jj=4
3

e, de fato, obtenho a saída esperada. No entanto, se um método padrão for adicionado à interface I,

interface I {
    int i = 1, ii = Test.out("ii", 2);
    default void method() { } // causes initialization!
}

a saída muda para:

1
ii=2
j=3
jj=4
3

que indica claramente que interface I está sendo inicializada onde não estava antes! A mera presença do método padrão é suficiente para acionar a inicialização. O método padrão não precisa ser chamado, substituído ou mesmo mencionado, nem a presença de uma inicialização de gatilho de método abstrato.

Minha especulação é que a implementação do HotSpot queria evitar adicionar verificação de inicialização de classe / interface no caminho crítico da invokevirtualchamada. Antes do Java 8 e dos métodos padrão, invokevirtualnunca poderia terminar executando o código em uma interface, então isso não aconteceu. Pode-se pensar que isso faz parte do estágio de preparação de classe / interface ( JLS 12.3.2 ), que inicializa coisas como tabelas de métodos. Mas talvez isso tenha ido longe demais e, acidentalmente, tenha feito a inicialização completa.

Eu levantei essa questão na lista de discussão de devedores do compilador OpenJDK. Houve uma resposta de Alex Buckley (editor do JLS) em que ele levanta mais questões direcionadas às equipes de implementação de JVM e lambda. Ele também observa que há um bug na especificação aqui onde diz "T é uma classe e um método estático declarado por T é invocado" também deve se aplicar se T for uma interface. Portanto, pode ser que haja bugs de especificação e de HotSpot aqui.

Divulgação : Eu trabalho para a Oracle em OpenJDK. Se as pessoas acharem que isso me dá uma vantagem injusta em obter a recompensa associada a essa pergunta, estou disposto a ser flexível quanto a isso.

Stuart Marks
fonte
6
Eu perguntei por fontes oficiais. Não acho que fique mais oficial do que isso. Espere dois dias para ver todos os desenvolvimentos.
Sotirios Delimanolis
48
@StuartMarks " Se as pessoas pensarem que isso me dá uma vantagem injusta etc " => estamos aqui para obter respostas às perguntas e esta é uma resposta perfeita!
Assylias
2
Uma observação lateral: a especificação JVM contém uma descrição semelhante à do JLS: docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5 Isso também deve ser atualizado .
Marco13
2
@assylias e Sotirios, obrigado por seus comentários. Eles, junto com os 14 votos positivos (até o momento desta redação) no comentário de Assylias, aliviaram minhas preocupações sobre qualquer potencial injustiça.
Stuart Marks
1
@SotiriosDelimanolis Existem alguns bugs que parecem relevantes, JDK-8043275 e JDK-8043190 , e eles estão marcados como corrigidos em 8u40. No entanto, o comportamento parece ser o mesmo. Houve também algumas alterações nas especificações JVM entrelaçadas com isso, então talvez a correção seja algo diferente de "restaurar a ordem de inicialização antiga".
Stuart Marks
13

A interface não é inicializada porque o campo constante InterfaceType.init, que está sendo inicializado por um valor não constante (chamada de método), não é usado em nenhum lugar.

É sabido em tempo de compilação que o campo constante da interface não é usado em nenhum lugar, e a interface não contém nenhum método padrão (em java-8), portanto não há necessidade de inicializar ou carregar a interface.

A interface será inicializada nos seguintes casos,

  • campo constante é usado em seu código.
  • A interface contém um método padrão (Java 8)

No caso de métodos padrão , você está implementando InterfaceType. Portanto, If InterfaceTypeconterá quaisquer métodos padrão, Ele será INERIDO (usado) na implementação da classe. E a inicialização entrará em cena.

Porém, se você estiver acessando o campo constante da interface (que é inicializado de maneira normal), a inicialização da interface não é necessária.

Considere o seguinte código.

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();
}

No caso acima, a Interface será inicializada e carregada porque você está usando o campo InterfaceType.init .

Não estou dando o exemplo de método padrão, pois você já deu isso em sua pergunta.

A especificação e o exemplo da linguagem Java são fornecidos em JLS 12.4.1 (o exemplo não contém métodos padrão).


Não consigo encontrar JLS para métodos padrão, pode haver duas possibilidades

  • O pessoal de Java esqueceu de considerar o caso do método padrão. (Bug de especificação do Doc.)
  • Eles apenas se referem aos métodos padrão como membros não constantes da interface. (Mas não mencionou onde, novamente bug de especificação do Doc.)
Não é um bug
fonte
Estou procurando uma referência para o método padrão. O campo era apenas para demonstrar se a interface foi inicializada ou não.
Sotirios Delimanolis de
@SotiriosDelimanolis Mencionei o motivo na resposta para Método padrão ... mas infelizmente nenhum JLS ainda não encontrado para o método padrão.
Não é um bug de
Infelizmente, é isso que estou procurando. Eu sinto que sua resposta é apenas repetir coisas que já disse na pergunta, ou seja, que uma interface será inicializada se contiver um defaultmétodo e uma classe que implementa a interface for inicializada.
Sotirios Delimanolis
Acho que as pessoas de java se esqueceram de considerar o caso do método padrão, ou apenas se referem aos métodos padrão como membro não constante da interface (minha suposição, não consigo encontrar em nenhum documento).
Não é um bug de
1
@KishanSarsechaGajjar: O que você quer dizer com campo não constante na interface? Qualquer variável / campo na interface é final estático por padrão.
Lokesh de
10

O arquivo instanceKlass.cpp do OpenJDK contém o método de inicialização InstanceKlass::initialize_implque corresponde ao Procedimento de inicialização detalhado no JLS, que é encontrado analogamente no Initialization seção na Especificação JVM.

Ele contém uma nova etapa que não é mencionada no JLS e não no livro JVM que é referido no código:

// 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);
    ....
    }
  }
}

Portanto, essa inicialização foi implementada explicitamente como uma nova Etapa 7.5 . Isso indica que essa implementação seguiu algumas especificações, mas parece que a especificação escrita no site não foi atualizada de acordo.

EDITAR: Como referência, o commit (a partir de outubro de 2012!) Onde a respectiva etapa foi incluída na implementação: http://hg.openjdk.java.net/jdk8/build/hotspot/rev/4735d2c84362

EDIT2: Coincidentemente, encontrei este documento sobre métodos padrão em ponto de acesso que contém uma observação interessante no final:

3.7 Diversos

Como as interfaces agora têm bytecode nelas, devemos inicializá-las no momento em que uma classe de implementação é inicializada.

Marco13
fonte
1
Obrigado por desenterrar isso. (+1) Pode ser que a nova "etapa 7.5" tenha sido inadvertidamente omitida da especificação, ou que tenha sido proposta e rejeitada e a implementação nunca tenha sido corrigida para removê-la.
Stuart Marks
1

Vou tentar argumentar que uma inicialização de interface não deve causar nenhum efeito colateral de canal lateral dos quais os subtipos dependem, portanto, se isso é um bug ou não, ou de qualquer maneira que o Java o conserte, não deve importar o aplicativo no qual as interfaces de ordem são inicializadas.

No caso de a class, é bem aceito que pode causar efeitos colaterais dos quais dependem as subclasses. Por exemplo

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

Qualquer subclasse de Fooesperaria ver $ 1000 no banco, em qualquer lugar do código da subclasse. Portanto, a superclasse é inicializada antes da subclasse.

Não deveríamos fazer a mesma coisa com as superintefaces também? Infelizmente, a ordem das superinterfaces não deve ser significativa, portanto, não há uma ordem bem definida para inicializá-las.

Portanto, é melhor não estabelecermos esse tipo de efeito colateral nas inicializações de interface. Afinal, interfacenão se destina a esses recursos (campos / métodos estáticos) que acumulamos por conveniência.

Portanto, se seguirmos esse princípio, não haverá preocupação para nós em que ordem as interfaces são inicializadas.

ZhongYu
fonte