Por que esse java inválido é? Tipo de saída do operador ternário
-
18-09-2019 - |
Pergunta
Confira este código.
// Print object and recurse if iterable
private static void deep_print(Object o) {
System.out.println(o.getClass().toString() + ", " + o.toString());
boolean iter = false;
Iterable<?> i1 = null;
Object[] i2 = null;
if (o instanceof Iterable<?>) {
iter = true;
i1 = (Iterable<?>) o;
} else if (o instanceof Object[]) {
iter = true;
i2 = (Object[]) o;
}
if (iter) {
for (Object o_ : i2 == null ? i1 : i2) deep_print(o_); // ERROR: Can only iterate over an array or an instance of java.lang.Iterable
}
Eu sei como resolvê -lo. Eu só quero saber por que isso acontece. O compilador não deve simplesmente verificar todas as saídas possíveis?
Solução
O resultado estático do tipo de expressão (i2 == null) ? i1 : i2
é o ancestral comum de i1
e i2
o que é objeto. UMA for
A declaração requer o Tipo estático da expressão ser um Iterable
ou um tipo de matriz. Não é esse o caso, então você recebe um erro de compilação.
Agora, se você está perguntando por que o compilador não deduz isso (i2 == null) ? i1 : i2
sempre será uma matriz ou um iterável:
- A especificação do idioma Java (JLS) não permite isso.
- Se o JLS permitisse (mas não exigisse), diferentes compiladores se comportariam de maneira diferente, dependendo de quão bons eles eram no teorema da prova. MAU.
- Se o JLS exigia, o compilador terá que incorporar um teorema sofisticado provador`1 (Slow Bad Slow), e você encontra o "menor inconveniente" de resolver o problema de parada2 (MAU MAU MAU).
- De fato, o compilador precisa saber qual dos dois tipos de tipo a expressão tem, porque precisa gerar código diferente em cada caso.
Hipoteticamente, se o sistema do tipo Java fosse um pouco diferente, este caso em particular poderia ser tratado melhor. Especificamente, se Java apoiado Tipos de dados algébricos então seria possível declarar o
como sendo "uma matriz de objeto ou um iterável" ... e o for
O loop seria verificável pelo tipo.
1 - suponha que o
foi inicializado como o = (x * x < 0) ? new Object() : new Object[0]
. Determinando que isso sempre resultará em um Object[]
O exemplo implica uma pequena prova que envolve o fato de que o quadrado de um número (real) não é negativo. Esse é um exemplo simples, é possível construir exemplos arbitrariamente complicados, exigindo provas difíceis arbitrárias.
2 - O problema de interrupção é matematicamente comprovado como uma função incomputável. Em outras palavras, existem funções em que é matematicamente impossível provar se eles terminam ou não.
Outras dicas
Para ilustrar a resposta de Stephen C, considere o seguinte código:
void test() {
Iterable<Integer> i1 = new ArrayList<Integer>();
Object[] i2 = { 1, 2, 3 };
method1(false ? i1 : i2);
method1(true ? i1 : i2);
}
void method1(Object o) {
System.out.println("method1(Object) called");
}
void method1(Object[] o) {
System.out.println("method1(Object[]) called");
}
void method1(Iterable<?> o) {
System.out.println("method1(Iterable<?>) called");
}
Esta é a saída do teste ():
method1(Object) called
method1(Object) called
Como a sobrecarga do método é feita em tempo de compilação, você pode ver que o tipo estático da expressão do operador ternário é objeto, uma vez que os tipos de operandos diferem. Portanto, quando você faz:
for (Object o_ : i2 == null ? i1 : i2)
Você está realmente pedindo ao compilador que gere um loop foreach sobre um objeto, o que é ilegal.
Nesse caso, a expressão condicional tem o tipo de limite superior inferior dos dois tipos, que é Object
, e o loop foreach não funciona no tipo Object
Você realmente deve adicionar dois métodos deepprint sobrecarregados (objeto []) e deepprint (iterator i) e fazer o despacho em seu Deepprint (objeto). Sim, devido à natureza de como funciona o para/cada loop, você precisará copiar e colar o mesmo código com pequenas alterações.
Tentar colocar tudo isso em um grande método é um cheiro.
Você pode evitar a duplicação de código envolvendo o objeto [] em um Arrays.asList (objeto []) e depois ter um iterável em todos os casos. Sim, é um pouco mais lento do que trabalhar em uma matriz, mas tem a vantagem de estar seco, o que sempre deve ser a primeira aproximação, IMHO.
Então você acabaria com algo assim:
Iterable<?> it = null;
if (o instanceof Iterable<?>) {
it = (Iterable<?>) o;
} else if (o instanceof Object[]) {
it = Arrays.asList((Object[]) o);
}
if (it != null) {
for (Object o_ : it) deep_print(o_);
}
Por uma questão de simplicidade (veja a resposta de Stephen C sobre como é mais simples para os escritores do compilador, mas também é um design de linguagem mais simples), o operador ternário assume que o tipo é o menor demoninador comum entre os dois tipos de retorno, em vez de acompanhar um dos dois tipos de retorno. Considere o caso de uma sobrecarga de método. O método correto é determinado e com o tempo de compilação. Isso significa que o compilador deve poder tomar uma decisão sobre o tipo de retorno declarado no horário de compilação e não pode atrasar essa decisão de executar o tempo.