Não muito longe está a nova
versão 14 do Java, o que significa que é hora de ver quais novos recursos de sintaxe esta versão do Java conterá. Uma dessas possibilidades sintáticas é a
correspondência de padrões do tipo que será implementado usando a
instanceof
aprimorada (estendida) do operador.
Hoje eu gostaria de brincar com esse novo operador e considerar os recursos de seu trabalho com mais detalhes. Como a correspondência de padrões por tipo ainda não entrou no repositório JDK principal, tive que fazer o download do repositório
do projeto Amber , que está desenvolvendo novas construções de sintaxe Java e
compilar o JDK a partir deste repositório.
Portanto, a primeira coisa que faremos é verificar a versão Java para garantir que realmente utilizemos o JDK 14:
> java -version openjdk version "14-internal" 2020-03-17 OpenJDK Runtime Environment (build 14-internal+0-adhoc.osboxes.amber-amber) OpenJDK 64-Bit Server VM (build 14-internal+0-adhoc.osboxes.amber-amber, mixed mode, sharing)
Isso mesmo.
Agora vamos escrever um pequeno pedaço de código com a
instanceof
"old" do operador e executá-lo:
public class A { public static void main(String[] args) { new A().f("Hello, world!"); } public void f(Object obj) { if (obj instanceof String) { String str = (String) obj; System.out.println(str.toLowerCase()); } } }
> java A.java hello, world!
Isso funciona. Esta é uma verificação de tipo padrão seguida por uma conversão. Escrevemos construções semelhantes todos os dias, independentemente da versão do Java que usamos, pelo menos 1.0, pelo menos 13.
Mas agora temos o Java 14 em nossas mãos e vamos reescrever o código usando a
instanceof
melhorada do operador (vou omitir linhas de código repetidas no futuro):
if (obj instanceof String str) { System.out.println(str.toLowerCase()); }
> java --enable-preview --source 14 A.java hello, world!
Ótimo. O código é mais limpo, mais curto, mais seguro e mais legível. Houve três repetições da palavra String, uma delas se tornou. Observe que não esquecemos de especificar os argumentos
--enable-preview --source 14
, como O novo operador é um
recurso de visualização . Além disso, um leitor atento provavelmente percebeu que executamos o arquivo de origem A.java diretamente, sem compilação. Esse recurso
apareceu no Java 11.
Vamos tentar escrever algo mais sofisticado e adicionar uma segunda condição que usa a variável recém-declarada:
if (obj instanceof String str && str.length() > 5) { System.out.println(str.toLowerCase()); }
Compila e funciona. Mas e se você trocar de condições?
if (str.length() > 5 && obj instanceof String str) { System.out.println(str.toLowerCase()); }
A.java:7: error: cannot find symbol if (str.length() > 5 && obj instanceof String str) { ^
Erro de compilação. O que é esperado: a variável
str
ainda não foi declarada, o que significa que não pode ser usada.
A propósito, e quanto à mutabilidade? A variável é final ou não? Tentamos:
if (obj instanceof String str) { str = "World, hello!"; System.out.println(str.toLowerCase()); }
A.java:8: error: pattern binding str may not be assigned str = "World, hello!"; ^
Sim, a variável final. Isso significa que a palavra "variável" não está totalmente correta aqui. E o compilador usa o termo especial "ligação de padrão". Portanto, proponho de agora em diante dizer não "variável", mas "ligação de padrão" (infelizmente, a palavra "ligação" não é muito bem traduzida para o russo).
Com mutabilidade e terminologia resolvidas. Vamos experimentar ainda mais. E se conseguirmos "quebrar" o compilador?
E se você nomear uma variável e uma ligação de padrão com o mesmo nome?
if (obj instanceof String obj) { System.out.println(obj.toLowerCase()); }
A.java:7: error: variable obj is already defined in method f(Object) if (obj instanceof String obj) { ^
É lógico. A sobreposição de uma variável do escopo externo não funciona. Isso é equivalente a como se
obj
a variável
obj
segunda vez no mesmo escopo.
E se sim:
if (obj instanceof String str && obj instanceof String str) { System.out.println(str.toLowerCase()); }
A.java:7: error: illegal attempt to redefine an existing match binding if (obj instanceof String str && obj instanceof String str) { ^
O compilador é tão sólido quanto concreto.
O que mais você pode tentar? Vamos brincar com escopos. Se a ligação for definida na ramificação
if
, ela será definida na ramificação
else
se a condição for invertida?
if (!(obj instanceof String str)) { System.out.println("not a string"); } else { System.out.println(str.toLowerCase()); }
Funcionou. O compilador não é apenas confiável, mas também inteligente.
E se sim?
if (obj instanceof String str && true) { System.out.println(str.toLowerCase()); }
Funcionou novamente. O compilador entende corretamente que a condição se resume a uma
obj instanceof String str
simples de
obj instanceof String str
.
Realmente não é possível "quebrar" o compilador?
Talvez sim?
if (obj instanceof String str || false) { System.out.println(str.toLowerCase()); }
A.java:8: error: cannot find symbol System.out.println(str.toLowerCase()); ^
Sim! Isso já parece um bug. Afinal, todas as três condições são absolutamente equivalentes:
obj instanceof String str
obj instanceof String str && true
obj instanceof String str || false
As regras de escopo de fluxo, por outro lado, são bastante
triviais e talvez esse caso realmente não deva funcionar. Mas se você olha puramente do ponto de vista humano, acho que isso é um bug.
Mas vamos lá, vamos tentar outra coisa. Isso funcionará:
if (!(obj instanceof String str)) { throw new RuntimeException(); } System.out.println(str.toLowerCase());
Compilado. Isso é bom, pois esse código é equivalente ao seguinte:
if (!(obj instanceof String str)) { throw new RuntimeException(); } else { System.out.println(str.toLowerCase()); }
E como as duas opções são equivalentes, o programador espera que elas funcionem da mesma maneira.
E quanto à sobreposição de campos?
public class A { private String str; public void f(Object obj) { if (obj instanceof String str) { System.out.println(str.toLowerCase()); } else { System.out.println(str.toLowerCase()); } } }
O compilador não jurou. Isso é lógico, porque as variáveis locais sempre podem se sobrepor aos campos. Aparentemente, eles também decidiram não abrir exceções para ligações de padrões. Por outro lado, esse código é bastante frágil. Um movimento descuidado, e você pode não perceber como o seu ramo
if
quebrou:
private boolean isOK() { return false; } public void f(Object obj) { if (obj instanceof String str || isOK()) { System.out.println(str.toLowerCase()); } else { System.out.println(str.toLowerCase()); } }
Os dois ramos agora usam o campo
str
, que um programador desatento pode não esperar. Para detectar esses erros o mais cedo possível, use as inspeções no IDE e o destaque de sintaxe diferente para campos e variáveis. Também recomendo que você sempre use o qualificador
this
para campos. Isso adicionará ainda mais confiabilidade.
O que mais é interessante? Como a
instanceof
"antiga", a nova nunca corresponde a
null
. Isso significa que você sempre pode confiar no fato de que os binders de padrões nunca podem ser
null
:
if (obj instanceof String str) { System.out.println(str.toLowerCase());
A propósito, usando esta propriedade, você pode encurtar essas cadeias:
if (a != null) { B b = a.getB(); if (b != null) { C c = b.getC(); if (c != null) { System.out.println(c.getSize()); } } }
Se você usar
instanceof
, o código acima poderá ser reescrito da seguinte maneira:
if (a != null && a.getB() instanceof B b && b.getC() instanceof C c) { System.out.println(c.getSize()); }
Escreva nos comentários o que você pensa sobre esse estilo. Você usaria esse idioma?
E os genéricos?
import java.util.List; public class A { public static void main(String[] args) { new A().f(List.of(1, 2, 3)); } public void f(Object obj) { if (obj instanceof List<Integer> list) { System.out.println(list.size()); } } }
> java --enable-preview --source 14 A.java Note: A.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. 3
Muito interessante Se a
instanceof
"antiga" suportar apenas a
instanceof List
ou a
instanceof List<?>
, A nova funcionará com qualquer tipo específico. Estamos esperando a primeira pessoa cair nessa armadilha:
if (obj instanceof List<Integer> list) { System.out.println("Int list of size " + list.size()); } else if (obj instanceof List<String> list) { System.out.println("String list of size " + list.size()); }
Por que isso não está funcionando?Resposta: falta de genéricos reificados em Java.
IMHO, este é um problema muito sério. Por outro lado, não sei como consertar. Parece que você precisa confiar nas inspeções no IDE novamente.
Conclusões
Em geral, o novo tipo de correspondência de padrões funciona muito bem. A
instanceof
aprimorada do operador permite fazer não apenas testes de tipo, mas também declarar fichários prontos desse tipo, eliminando a necessidade de transmissões manuais. Isso significa que haverá menos ruído no código e será muito mais fácil para o leitor discernir a lógica útil. Por exemplo, a maioria das implementações
equals()
pode ser escrita em uma linha:
public class Point { private final int x, y; … @Override public int hashCode() { return Objects.hash(x, y); } @Override public boolean equals(Object obj) { return obj instanceof Point p && px == this.x && py == this.y; } }
O código acima pode ser escrito ainda mais curto. ComoUsando
entradas que também serão incluídas no Java 14. Falaremos sobre elas na próxima vez.
Por outro lado, vários pontos controversos levantam pequenas questões:
- Regras de escopo não totalmente transparentes (exemplo com
instanceof || false
). - Sobreposição de campos.
instanceof
e genéricos.
No entanto, estes são mais pequenos detalhes do que afirmações sérias. Em suma, os enormes benefícios da nova
instanceof
operador valem definitivamente a sua linguagem adicional. E se ele ainda sair do estado de visualização e se tornar uma sintaxe estável, será uma grande motivação finalmente deixar o Java 8 para a nova versão do Java.
PS: Eu tenho um
canal no Telegram onde escrevo sobre notícias em Java. Peço que você assine.