Operador ternário Java vs if / else em <compatibilidade JDK8

113

Recentemente estou lendo o código-fonte do Spring Framework. Algo que não consigo entender vai aqui:

public Member getMember() {
    // NOTE: no ternary expression to retain JDK <8 compatibility even when using
    // the JDK 8 compiler (potentially selecting java.lang.reflect.Executable
    // as common type, with that new base class not available on older JDKs)
    if (this.method != null) {
        return this.method;
    }
    else {
        return this.constructor;
    }
}

Este método é um membro da classe org.springframework.core.MethodParameter. O código é fácil de entender, enquanto os comentários são difíceis.

NOTA: nenhuma expressão ternária para reter a compatibilidade JDK <8, mesmo quando usando o compilador JDK 8 (potencialmente selecionando java.lang.reflect.Executablecomo tipo comum, com essa nova classe base não disponível em JDKs mais antigos)

Qual é a diferença entre usar expressão ternária e usar if...else...construção neste contexto?

jddxf
fonte

Respostas:

103

Quando você pensa sobre o tipo de operandos, o problema se torna mais aparente:

this.method != null ? this.method : this.constructor

tem como tipo o tipo mais especializado comum de ambos os operandos, ou seja, o tipo mais especializado comum a ambos this.methode this.constructor.

No Java 7 java.lang.reflect.Member, entretanto, a biblioteca de classes do Java 8 apresenta um novo tipo java.lang.reflect.Executableque é mais especializado do que o genérico Member. Portanto, com uma biblioteca de classes Java 8, o tipo de resultado da expressão ternária é em Executablevez de Member.

Algumas versões (pré-lançamento) do compilador Java 8 parecem ter produzido uma referência explícita ao Executablecódigo gerado interno ao compilar o operador ternário. Isso acionaria um carregamento de classe e, portanto, em ClassNotFoundExceptiontempo de execução ao executar com uma biblioteca de classes <JDK 8, porque Executableexiste apenas para JDK ≥ 8.

Conforme observado por Tagir Valeev nesta resposta , este é na verdade um bug nas versões de pré-lançamento do JDK 8 e já foi corrigido, portanto, a if-elsesolução alternativa e o comentário explicativo agora estão obsoletos.

Observação adicional: pode-se chegar à conclusão de que esse bug do compilador estava presente antes do Java 8. No entanto, o código de bytes gerado para o ternário pelo OpenJDK 7 é o mesmo que o código de bytes gerado pelo OpenJDK 8. Na verdade, o tipo de expressão fica completamente sem menção em tempo de execução, o código é realmente apenas teste, ramificação, carga, retorno sem nenhuma verificação adicional acontecendo. Portanto, tenha certeza de que isso não é um problema (mais) e, de fato, parece ter sido um problema temporário durante o desenvolvimento do Java 8.

dhke
fonte
1
Então, como o código compilado com o JDK 1.8 pode ser executado no JDK 1.7. Eu sei que o código compilado com a versão inferior do JDK pode ser executado na versão superior do JDK sem problemas. Viz-e-versa?
jddxf
1
@jddxf Tudo está bem, desde que você especifique a versão do arquivo de classe apropriada e não use nenhuma funcionalidade que não esteja disponível nas versões posteriores. O problema está fadado a ocorrer, no entanto, se tal uso acontecer implicitamente como neste caso.
dhke,
13
@jddxf, use -source / -target javac options
Tagir Valeev
1
Obrigado a todos, especialmente dhke e Tagir Valeev, que deram uma explicação completa
jddxf
30

Isso foi introduzido em um commit bastante antigo em 3 de maio de 2013, quase um ano antes do lançamento oficial do JDK-8. O compilador estava sob intenso desenvolvimento naquela época, então tais problemas de compatibilidade podiam ocorrer. Acho que a equipe do Spring acabou de testar a compilação do JDK-8 e tentou consertar os problemas, embora na verdade sejam problemas do compilador. No lançamento oficial do JDK-8, isso se tornou irrelevante. Agora, o operador ternário neste código funciona bem como esperado (nenhuma referência à Executableclasse no arquivo .class compilado está presente).

Atualmente, coisas semelhantes aparecem no JDK-9: algum código que pode ser bem compilado no JDK-8 falha com o JDK-9 javac. Acho que muitos desses problemas serão corrigidos até o lançamento.

Tagir Valeev
fonte
2
+1. Então, isso era um bug no compilador inicial? Aquele comportamento, onde se referia Executable, estava violando algum aspecto das especificações? Ou será que a Oracle percebeu que poderia mudar esse comportamento de uma forma que ainda estaria de acordo com as especificações e sem quebrar a compatibilidade com versões anteriores?
ruakh
2
@ruakh, acho que foi o bug. Em bytecode (em Java-8 ou anterior) é completamente desnecessário converter explicitamente para um Executabletipo intermediário. Em Java-8, o conceito de inferência de tipo de expressão mudou drasticamente e esta parte foi completamente reescrita, portanto, não é tão surpreendente que as primeiras implementações tivessem bugs.
Tagir Valeev
7

A principal diferença é que um if elsebloco é uma instrução, enquanto o ternário (mais conhecido como o operador condicional em Java) é uma expressão .

Uma instrução pode fazer coisas como returno chamador em alguns dos caminhos de controle. Uma expressão pode ser usada em uma atribuição:

int n = condition ? 3 : 2;

Portanto, as duas expressões no ternário após a condição precisam ser coagíveis para o mesmo tipo. Isso pode causar alguns efeitos estranhos em Java, especialmente com a caixa automática e a conversão automática de referência - é a isso que o comentário em seu código postado se refere. A coerção das expressões no seu caso seria para um java.lang.reflect.Executabletipo (já que é o tipo mais especializado ) e que não existe nas versões anteriores do Java.

Estilisticamente, você deve usar um if elsebloco se o código for semelhante a uma instrução e um ternário se for semelhante a uma expressão.

Claro, você pode fazer um if elsebloco se comportar como uma expressão se usar uma função lambda.

Bate-Seba
fonte
6

O tipo de valor de retorno em uma expressão ternária é afetado pelas classes pai, que mudaram conforme descrito em Java 8.

É difícil ver por que um elenco não poderia ter sido escrito.

Marquês de Lorne
fonte