Skip to main content

Guia rápido de inferência de tipos locais para Java 10 e versões posteriores!

Escrito por

Stuart Marks

26 de abril de 2018

0 minutos de leitura
Folha de consulta da Snyk que explica a inferência de tipos locais em Java, com três princípios de programação e sete orientações ilustrados por exemplos de código.

Boas-vindas à primeira edição de uma nova série de guias rápidos que publicaremos no blog da Snyk. Vamos oferecer conteúdo para você imprimir e deixar à vista, ajudando você a se tornar uma pessoa desenvolvedora melhor. Nesta primeira edição, logo após o lançamento do Java 10, vamos abordar a tão comentada inferência de tipos para variáveis locais. Você pode baixar a versão em PDF clicando na imagem acima ou neste link! A ideia por trás do recurso de inferência de tipos locais é bem simples: substitua o tipo explícito na declaração pelo novo nome de tipo reservado ‘var’, e o tipo será inferido. Assim, podemos substituir:

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

por:

var outputStream = new ByteArrayOutputStream();

E o tipo de outputStream será inferido como ByteArrayOutputStream. Espere aí: isso quer dizer que agora Java permite tipagem dinâmica? De jeito nenhum! TODA inferência de tipos ocorre em tempo de compilação, e o compilador grava os tipos explícitos no bytecode. Em tempo de execução, Java continua tão estático quanto sempre foi. Como o uso é tão simples, este guia rápido se concentra no aspecto mais importante da inferência de tipos locais: o uso prático. Ele orienta você sobre quando usar tipagem explícita e quando considerar a inferência de tipos.

Enquanto eu preparava este guia rápido, Stuart Marks, engenheiro do JDK na Oracle, escreveu o artigo perfeito, com princípios de programação e orientações sobre o uso da inferência de tipos. Então, quando decidi criar um guia rápido, procurei Stuart para saber se poderíamos incluir as ideias dele e resumir tudo em um material para pessoas desenvolvedoras deixarem à vista e consultarem no dia a dia! Recomendo muito que você leia o artigo completo de Stuart. Vale mesmo a pena!

Princípios

1. Ler código > Escrever código.

Não importa se você leva 10 minutos ou 10 dias para escrever uma linha de código: é quase certo que vai lê-la por muitos anos. O código só será fácil de manter e entender no futuro se for claro, conciso e, acima de tudo, contiver todas as informações necessárias para que seu propósito seja compreendido. O objetivo é maximizar a compreensão.

2. O código deve ser claro com base apenas no contexto local.

Inclua o máximo de informações possível no código para evitar que quem o lê tenha de consultar diferentes partes da base de código para entender o que está acontecendo. Isso pode ser feito com nomes de métodos ou variáveis.

3. A legibilidade do código não deve depender das IDEs.

As IDEs podem ser ótimas. Quero dizer, ótimas mesmo! Elas podem aumentar a produtividade ou a precisão de quem desenvolve. Mas o código precisa ser legível e compreensível sem depender de uma IDE. Muitas vezes, ele é lido fora de uma IDE. Além disso, as IDEs podem diferir na quantidade de informações que oferecem a quem lê. O código deve ser autoexplicativo: precisa ser compreensível por si só, sem ajuda de ferramentas.

A decisão é sua.

Escolher entre declarar o tipo de uma variável explicitamente ou deixar que o compilador Java o determine envolve uma troca. Por um lado, você quer reduzir a poluição visual, o código repetitivo e a burocracia. Por outro, não quer prejudicar a compreensão do código. A declaração do tipo não é a única forma de transmitir informações a quem lê. O nome da variável e a expressão que a inicializa também ajudam. Ao decidir se vale a pena omitir o tipo explícito em cada variável, devemos levar em conta todos os recursos disponíveis.

Orientações

1. Escolha nomes de variáveis que tragam informações úteis.

Essa é uma boa prática em geral, mas é ainda mais importante ao usar var. Em uma declaração com var, o nome da variável pode transmitir informações sobre seu significado e uso. Substituir um tipo explícito por var muitas vezes deve vir acompanhado de um nome melhor para a variável. Em alguns casos, pode ser útil incluir o tipo no nome. Por exemplo:

List<Customer> x = dbconn.executeQuery(query);

✅ var custList = dbconn.executeQuery(query);

2. Minimize o escopo das variáveis locais.

Limitar o escopo de variáveis locais é uma recomendação de Effective Java (3ª edição), item 57. Ela é ainda mais importante quando se usa var. O problema surge quando a variável tem um escopo amplo, ou seja, há muitas linhas de código entre sua declaração e seu uso. Com a manutenção do código, alterações nos tipos e outros ajustes podem mudar o comportamento. Por exemplo, trocar uma List por um Set pode parecer inofensivo, mas será que o código depende da ordenação mais adiante nesse mesmo escopo? Embora os tipos sejam sempre definidos estaticamente, diferenças sutis entre implementações que usam a mesma interface podem causar problemas. Em vez de simplesmente evitar var nesses casos, altere o código para reduzir o escopo das variáveis locais e só então declare-as com var. Considere este código:

var items = new HashSet<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) { ... }

Agora há um bug no código, porque os conjuntos não têm uma ordem de iteração definida. No entanto, é provável que a pessoa que programou corrija esse bug imediatamente, já que os usos da variável items ficam próximos à declaração. Agora imagine que esse código faça parte de um método grande, com um escopo igualmente amplo para a variável items:

var items = new HashSet<Item>(...);

// ... 100 lines of code ...

items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) { ... }

Agora fica bem mais difícil encontrar o bug, pois a linha que tenta adicionar um item ao final do conjunto não está perto o bastante da declaração do tipo para que o problema fique evidente.

3. Considere usar var quando a inicialização já fornecer informações suficientes a quem lê.

Variáveis locais são frequentemente inicializadas com construtores. Muitas vezes, o nome da classe instanciada se repete como tipo explícito à esquerda. Se o nome do tipo for longo, var deixa o código mais conciso sem perda de informação:

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

✅ var outputStream = new ByteArrayOutputStream();

Também é razoável usar var quando a inicialização é uma chamada de método, como Files.newBufferedReader(…) ou List stringList = List.of("a", "b", "c").

4. Use var para dividir expressões encadeadas ou aninhadas em variáveis locais.

Considere um código que recebe uma coleção de strings e encontra a que aparece com mais frequência. Ele poderia ser assim:

return strings.stream()
              .collect(groupingBy(s -> s, counting()))
              .entrySet()
              .stream()
              .max(Map.Entry.comparingByValue())
              .map(Map.Entry::getKey);

Esse código está correto, mas fica mais legível se for dividido em várias instruções. O problema de dividir o código em instruções como mostrado é:

Map<String, Long> freqMap = strings.stream()
                                   .collect(groupingBy(s -> s, counting()));
Optional<Map.Entry<String, Long>> maxEntryOpt = freqMap.entrySet()
                                                       .stream()
                                                       .max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

Mas provavelmente a pessoa que escreveu o código evitou fazer isso porque a tipagem explícita fica extremamente confusa e desvia a atenção do que importa. Com var, podemos expressar o código de forma mais natural sem pagar o alto preço de declarar explicitamente os tipos das variáveis intermediárias:

✅
var freqMap = strings.stream()
                     .collect(groupingBy(s -> s, counting()));
var maxEntryOpt = freqMap.entrySet()
                         .stream()
                         .max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

É perfeitamente válido preferir o primeiro trecho, com sua longa cadeia de chamadas de métodos. No entanto, em alguns casos, é melhor dividir cadeias longas de métodos.

5. Não se preocupe tanto em “programar para a interface” ao usar variáveis locais.

Um padrão comum em Java é criar uma instância de um tipo concreto, mas atribuí-la a uma variável de tipo interface. Por exemplo:

List<String> list = new ArrayList<>();

Porém, se var for usado, o tipo concreto será inferido em vez da interface:

// Inferred type of list is ArrayList<String>.
✅ var list = new ArrayList<String>();

O código que usa a variável list pode passar a depender da implementação concreta. Se a inicialização da variável mudar no futuro, o tipo inferido também pode mudar, causando erros ou bugs no código subsequente que usa a variável.

Isso é menos problemático quando a orientação 2 é seguida: se o escopo da variável local for pequeno, os riscos de a implementação concreta “vazar” e afetar o código subsequente serão limitados.

6. Tenha cuidado ao usar var com o operador diamante ou métodos genéricos.

Tanto var quanto o recurso “diamante” permitem omitir informações explícitas de tipo quando elas podem ser deduzidas de outras informações disponíveis. Porém, usados em conjunto, podem acabar omitindo todas as informações úteis de que o compilador precisa para inferir corretamente o tipo desejado.

Considere o seguinte:

PriorityQueue<Item> itemQueue = new PriorityQueue<Item>();
PriorityQueue<Item> itemQueue = new PriorityQueue<>();
✅ var itemQueue = new PriorityQueue<Item>();

// DANGEROUS: infers as PriorityQueue<Object>
❌ var itemQueue = new PriorityQueue<>();

Os métodos genéricos também usam inferência de tipos com tanto sucesso que é raro quem programa fornecer argumentos de tipo explícitos. A inferência em métodos genéricos depende do tipo de destino quando não há argumentos reais do método que forneçam informações suficientes sobre o tipo. Em uma declaração com var, não há tipo de destino, então pode ocorrer um problema semelhante ao do operador diamante. Por exemplo:

// DANGEROUS: infers as List<Object>
❌ var list = List.of();

Tanto com o operador diamante quanto com métodos genéricos, os argumentos reais do construtor ou método podem fornecer informações adicionais sobre o tipo, permitindo inferir o tipo pretendido. Isso acrescenta um nível a mais de indireção, mas o resultado continua previsível. Assim:

// OK: itemQueue infers as PriorityQueue<String>
Comparator<String> comp = ... ;
✅ var itemQueue = new PriorityQueue<>(comp);

7. Tenha cuidado ao usar var com literais.

É improvável que usar var com literais traga grandes vantagens, pois os nomes dos tipos costumam ser curtos. Ainda assim, var pode ser útil em alguns casos, por exemplo, para alinhar nomes de variáveis.

Não há problema em usar literais booleanos, de caracteres, longos e de strings. O tipo inferido desses literais é preciso, então o significado de var é inequívoco. É preciso ter cuidado especial quando a inicialização usa um valor numérico, principalmente um literal inteiro. Com um tipo explícito à esquerda, o valor numérico pode ser ampliado ou reduzido silenciosamente para tipos diferentes de int. Com var, o valor será inferido como int, o que pode não ser o desejado.

// ORIGINAL
boolean ready = true;
char ch = '\ufffd';
long sum = 0L;
String label = "wombat";
byte flags = 0;
short mask = 0x7fff;
long base = 17;

✅ var ready = true;
✅ var ch    = '\ufffd';
✅ var sum   = 0L;
✅ var label = "wombat";

// DANGEROUS: all infer as int
❌ var flags = 0;
❌ var mask = 0x7fff;
❌ var base = 17;

Conclusão

Usar var nas declarações pode melhorar o código ao reduzir a poluição visual e destacar informações mais importantes. Por outro lado, usar var indiscriminadamente pode piorar as coisas. Quando usado corretamente, var ajuda a aprimorar um bom código, deixando-o mais curto e claro sem prejudicar a compreensão. Ao usar var, pergunte a si mesmo: o código ficou mais ambíguo ou está claramente compreensível sem exigir muita investigação?

Baixe o guia rápido agora!

Comece a jogar Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.

Leia mais

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

illustration hero ai
Blog

O furacão da IA chegou

A IA está acelerando tanto a criação de software quanto os ataques cibernéticos. As lideranças devem proteger agentes e código desde o início, aplicar controles em tempo de execução e validar as defesas de forma independente.

feature insights context
Blog

A prevenção é essencialmente um problema resolvido?

A prevenção em código gerado por agentes está resolvida do ponto de vista arquitetural — mas escolher controles que protejam a segurança sem desacelerar o desenvolvimento continua sendo um desafio.