Recentemente, deparei-me com uma situação em que a substituição de Object por var em um programa Java 10 gera uma exceção em tempo de execução. Fiquei interessado em quantas maneiras diferentes de obter esse efeito e resolvi esse problema para a comunidade:
Acabou que você pode conseguir o efeito de maneiras diferentes. Embora todos sejam um pouco complicados, é interessante recordar as várias sutilezas do idioma com o exemplo de uma tarefa desse tipo. Vamos ver quais métodos foram encontrados.
Deputados
Entre os entrevistados, havia muitas pessoas famosas e não muito. Este é Sergey bsideup Egorov, funcionário da Pivotal, palestrante, um dos criadores dos contêineres de teste. Este é Victor Polishchuk , famoso por relatos sobre o maldito empreendimento. Também observou Nikita Artyushov, do Google; Dmitry Mikhailov e Maccimo . Mas fiquei especialmente satisfeito com a chegada de Wouter Coekaerts . Ele é conhecido por seu artigo no ano passado , onde percorreu o sistema de tipos Java e contou como ele estava irremediavelmente quebrado. Parte deste artigo jbaruch e eu até usamos no quarto lançamento do Java Puzzlers .
Tarefa e soluções
Portanto, a essência da nossa tarefa é esta: existe um programa Java no qual há uma declaração de uma variável no formato Object x = ...
(padrão honesto java.lang.Object
, sem substituições de tipo). O programa compila, executa e imprime algo como "Ok". Substituímos Object
por var
, exigindo inferência automática de tipo, após o qual o programa continua a compilar, mas trava no lançamento com uma exceção.
As soluções podem ser divididas em dois grupos. No primeiro, depois de substituir por var, a variável se torna primitiva (ou seja, era originalmente a caixa automática). O segundo tipo permanece objeto, mas mais específico que Object
. Aqui você pode destacar um subgrupo interessante que usa genéricos.
Boxe
Como distinguir um objeto de um primitivo? Existem muitas maneiras diferentes. O mais fácil é verificar a identidade. Esta solução foi proposta por Nikita:
Object x = 1000; if (x == new Integer(1000)) throw new Error(); System.out.println("Ok");
Quando x
é um objeto, certamente não pode ser igual por referência ao novo objeto new Integer(1000)
. E se for um primitivo, de acordo com as regras da linguagem, o new Integer(1000)
se desdobra imediatamente também em um primitivo, e os números são comparados como primitivos.
Outra maneira são os métodos sobrecarregados. Você pode escrever o seu próprio, mas Sergey criou uma opção mais elegante: use a biblioteca padrão. O método List.remove
é List.remove
, sobrecarregado e pode remover um elemento por índice se uma primitiva for passada ou um elemento por valor se um objeto for passado. Isso levou repetidamente a erros em programas reais se você usar a List<Integer>
. Para nossa tarefa, a solução pode ser assim:
Object x = 1000; List<?> list = new ArrayList<>(); list.remove(x); System.out.println("Ok");
Agora estamos tentando remover o elemento inexistente 1000 da lista vazia, esta é apenas uma ação inútil. Mas se substituirmos Object
por var
, chamaremos outro método que remove o elemento com o índice 1000. E isso já leva a IndexOutOfBoundsException
.
O terceiro método é o operador de conversão de tipos. Podemos converter com êxito outra primitiva em um tipo primitivo, mas um objeto é convertido apenas se houver um invólucro sobre o mesmo tipo para o qual iremos converter (o anboxing acontecerá). Na verdade, precisamos do efeito oposto: uma exceção é no caso de um primitivo, e não no de um objeto, mas usar try-catch é fácil de conseguir, que Viktor usou:
Object x = 40; try { throw new Error("Oops :" + (char)x); } catch (ClassCastException e) { System.out.println("Ok"); }
Aqui, ClassCastException
é o comportamento esperado, o programa sai normalmente. Mas depois de usar var
essa exceção desaparece e lançamos outra coisa. Gostaria de saber se isso é inspirado no código real da empresa sangrenta?
Outra opção de conversão de tipo foi proposta por Wouter. Você pode usar a estranha lógica do operador ?:
. É verdade que o código fornece apenas resultados diferentes, então você precisa modificá-lo de alguma forma para que haja uma exceção. Então, parece-me, com bastante elegância:
Object x = 1.0; System.out.println(String.valueOf(false ? x : 100000000000L).substring(12) + "Ok");
A diferença entre esse método é que não usamos o valor de x
diretamente, mas o tipo x
afeta o tipo da expressão false ? x : 100000000000L
false ? x : 100000000000L
. Se x
é um Object
, o tipo de expressão inteira é Object
e, em seguida, temos apenas o boxe, String.valueOf()
sequência de 100000000000
, para a qual a substring(12)
é uma sequência vazia. Se você usar var
, o tipo x
se tornará double
e, portanto, o tipo false ? x : 100000000000L
false ? x : 100000000000L
também false ? x : 100000000000L
double
, ou seja, 100000000000L
se transformará em 1.0E11
, com menos de 12 caracteres, portanto, chamar substring
leva a uma StringIndexOutOfBoundsException
.
Finalmente, aproveitamos o fato de que uma variável pode realmente ser alterada após a criação. E na variável de objeto, diferente da primitiva, você pode colocar null
. Colocar null
em uma variável é fácil; existem várias maneiras. Mas aqui, Wouter também adotou uma abordagem criativa usando o ridículo método Integer.getInteger
:
Object x = 1; x = Integer.getInteger("moo"); System.out.println("Ok");
Nem todo mundo sabe que esse método lê uma propriedade do sistema chamada moo
e, se houver, tenta convertê-la em um número, caso contrário, ele retorna null
. Se não houver propriedade, atribuímos null
silenciosamente ao objeto, mas caímos de NullPointerException
ao tentar atribuí-lo a um primitivo (ocorre o anboxing automático). Poderia ter sido mais fácil, é claro. Versão aproximada x = null;
ele não rastreia - não é compilado, mas o compilador o engolirá agora:
Object x = 1; x = (Integer)null; System.out.println("Ok");
Tipo de objeto
Suponha que você não possa mais jogar com primitivas. O que mais você pode pensar?
Bem, primeiro, o método mais simples de sobrecarga proposto por Dmitry:
public static void main(String[] args) { Object x = "Ok"; sayWhat(x); } static void sayWhat(Object x) { System.out.println(x); } static void sayWhat(String x) { throw new Error(); }
A vinculação de métodos sobrecarregados em Java ocorre estaticamente, no estágio de compilação. O método sayWhat sayWhat(Object)
é sayWhat(Object)
, mas se inferirmos o tipo x
automaticamente, a String
sayWhat(String)
e, portanto, o método sayWhat(String)
mais específico será vinculado.
Outra maneira de fazer uma chamada ambígua em Java é usando argumentos variáveis (varargs). Wouter lembrou isso novamente:
Object x = new Object[] {}; Arrays.asList(x).get(0); System.out.println("Ok");
Quando o tipo de variável é Object
, o compilador pensa que é um argumento variável e agrupa a matriz em outra matriz de um elemento, para que get()
concluído com êxito. Se você usa var
, o tipo Object[]
exibido e não haverá quebra automática. Dessa forma, obtemos uma lista vazia e a chamada get()
falhará.
Maccimo optou pelo hardcore: ele decidiu chamar println
através da API MethodHandle:
Object x = "Ok"; MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual( PrintStream.class, "println", MethodType.methodType(void.class, Object.class)); mh.invokeExact(System.out, x);
O método invokeExact
e vários outros métodos do java.lang.invoke
possuem a chamada "assinatura polimórfica". Embora seja declarado como o método usual vararg invokeExact(Object... args)
, ele não ocorre no empacotamento padrão da matriz. Em vez disso, uma assinatura é gerada no bytecode que corresponde aos tipos de argumentos realmente transmitidos. O método invokeExact
projetado para uma chamada super-rápida de identificadores de método, portanto, não realiza nenhuma transformação de argumento padrão, como conversão ou boxe. O tipo de método do identificador deve corresponder exatamente à assinatura da chamada. Isso é verificado no tempo de execução e, como no caso de var
correspondência é interrompida, obtemos uma WrongMethodTypeException
.
Genéricos
Obviamente, a parametrização de tipos pode adicionar um brilho a qualquer tarefa em Java. Dmitry trouxe uma solução semelhante ao código que me deparei inicialmente. A decisão de Dmitry é detalhada, então mostrarei o meu:
public static void main(String[] args) { Object x = foo(new StringBuilder()); System.out.println(x); } static <T> T foo(T x) { return (T)"Ok"; }
O tipo T
é produzido como StringBuilder
, mas nesse código o compilador não é necessário para inserir uma verificação de tipo no dial-peer no bytecode. É suficiente para ele que StringBuilder
possa ser atribuído a Object
, o que significa que está tudo bem. Ninguém é contra o fato de que o método com o valor de retorno StringBuilder
realmente retornou a string se você atribuiu o resultado a uma variável do tipo Object
qualquer maneira. O compilador avisa honestamente que você tem um elenco não verificado, o que significa que ele lava as mãos. No entanto, ao substituir x
por var
tipo x
também é exibido como StringBuilder
, e não é mais possível sem a verificação de tipo, porque atribuir algo mais à variável de tipo StringBuilder
é inútil. Como resultado, após alterar para var
programa trava com segurança com uma ClassCastException
.
Wouter sugeriu uma variante desta solução usando métodos padrão:
Object o = ((List<String>)(List)List.of(1)).get(0); System.out.println("Ok");
Por fim, outra opção da Wouter:
Object x = ""; TreeSet<?> set = Stream.of(x) .collect(toCollection(() -> new TreeSet<>((a, b) -> 0))); if (set.contains(1)) { System.out.println("Ok"); }
Aqui, dependendo do uso de var
ou Object
o tipo de fluxo é exibido como Stream<Object>
ou como Stream<String>
. Assim, o tipo TreeSet
e o tipo lambda do comparador são exibidos. No caso de var
, as strings devem chegar ao lambda, portanto, ao gerar uma representação de tempo de execução lambda, uma conversão de tipo é inserida automaticamente, o que fornece uma ClassCastException
ao tentar converter uma unidade em uma string.
Em geral, o resultado foi muito chato. Se você pode encontrar métodos fundamentalmente diferentes para quebrar o var
, escreva nos comentários.