Substitua Object por var: o que poderia dar errado?

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.

Source: https://habr.com/ru/post/pt469111/


All Articles