var: Inferência de Tipo de Variável Local
Objective
Entender var (JEP 286, Java 10): uma forma de inferência de tipo de variável local, em que o compilador deriva o tipo estático da variável a partir do seu inicializador em tempo de compilação. var não é tipagem dinâmica nem um sinônimo de Object — o tipo inferido é fixado para sempre naquela declaração e é imposto exatamente com o mesmo rigor de se você o tivesse escrito por extenso. Seu valor está em remover ruído de tipo redundante quando o lado direito já declara o tipo (var list = new ArrayList<String>();), não em economizar teclas por economizar.
Use Cases
- Eliminar um nome de tipo duplicado quando uma chamada de construtor ou factory já o anuncia, especialmente para tipos genéricos longos (
var users = new ArrayList<User>();,var index = new ConcurrentHashMap<String, List<Order>>();). - Loops
foraprimorados e try-with-resources, em que o tipo declarado só repete o que já está visível do lado direito (for (var entry : map.entrySet())). - Locais que guardam o resultado de uma chamada encadeada de stream ou builder cujo tipo é verboso, mas óbvio pelas linhas ao redor.
- Capturar o tipo de uma expressão de classe anônima — incluindo membros que ela declara além do seu supertipo — o que não pode ser escrito explicitamente de forma alguma.
Deep Dive
Onde var é permitido
var é legal apenas para variáveis locais que têm um inicializador, além das variáveis de loop e de recurso, que são locais disfarçadas:
javavar greeting = "hello"; // String
var counts = new HashMap<String, Integer>(); // HashMap<String, Integer>
for (var entry : counts.entrySet()) { } // enhanced-for element
for (var i = 0; i < 10; i++) { } // basic-for init clause
try (var in = Files.newInputStream(path)) { } // try-with-resourcesvar é um nome de tipo reservado, não uma palavra-chave, então código existente que usa var como identificador continua compilando:
javaint var = 3; // legal: var is not a keyword
var var = 3; // also legal, and also a terrible ideaOnde var é proibido
Toda posição abaixo é um erro de compilação, porque a inferência só roda contra um inicializador local.
Campos — de instância ou estáticos — não têm inferência nenhuma:
javaclass Account {
var balance = 0L; // error: 'var' is not allowed here
static var RATE = 0.05; // error: 'var' is not allowed here
}javaclass Account {
long balance = 0L; // fixed: write the type
static double RATE = 0.05;
}Parâmetros de método e tipos de retorno fazem parte da assinatura, contra a qual quem chama compila:
javavar total(var amounts) { } // error: 'var' is not allowed here (twice)javalong total(List<Long> amounts) { return 0L; } // fixedParâmetros de cláusula catch nomeiam o tipo sendo capturado, que é o que seleciona o handler:
javatry {
Files.readString(path);
} catch (var e) { // error: 'var' is not allowed here
e.printStackTrace();
}javatry {
Files.readString(path);
} catch (IOException e) { // fixed
e.printStackTrace();
}Uma declaração sem inicializador não dá ao compilador nada de onde inferir — e o mesmo vale para um inicializador de array isolado, que por si só precisa de um tipo alvo:
javavar x; // error: cannot infer type for local variable x
var nums = {1, 2, 3}; // error: array initializer needs an explicit target typejavaString x = null; // fixed: declare the type
var nums = new int[] {1, 2, 3};A sintaxe legada de "colchetes depois do nome" para arrays não pode se combinar com var, e o mesmo vale para uma declaração composta:
javavar arr[] = new int[3]; // error: 'var' is not allowed as an element type of an array
var a = 1, b = 2; // error: 'var' is not allowed in a compound declarationjavavar arr = new int[3]; // fixed
var a = 1;
var b = 2;var em parâmetros de lambda (Java 11+), tudo ou nada
Desde o Java 11, os parâmetros formais de uma lambda podem usar var, o que é o que permite anexar uma anotação ou um modificador a um parâmetro que, de outra forma, teria tipo implícito. A regra é que a lista de parâmetros precisa ser uniforme:
javaBinaryOperator<Integer> ok = (var x, var y) -> x + y; // fine: all varjavaBinaryOperator<Integer> mixed1 = (var x, y) -> x + y; // error: cannot mix 'var' and implicitly typed parameters
BinaryOperator<Integer> mixed2 = (var x, Integer y) -> x + y; // error: cannot mix 'var' and explicitly typed parametersjavaBinaryOperator<Integer> a = (x, y) -> x + y; // fixed: all implicit
BinaryOperator<Integer> b = (Integer x, Integer y) -> x + y; // or all explicit
BinaryOperator<Integer> c = (var x, var y) -> x + y; // or all varUm único parâmetro var também mantém os parênteses — var x -> x não compila, só (var x) -> x.
A inferência lê o tipo estático do inicializador
O tipo inferido é o que o compilador calcula estaticamente para o lado direito, não a classe em tempo de execução do valor:
javaObject o = "hello";
var copy = o; // copy is Object, not String
copy.length(); // error: cannot find symbol 'length' on ObjectjavaCharSequence cs = "hello";
var s = cs.toString(); // s is String — the *declared return type* of toString()É isso que faz a pegadinha do diamond morder. new ArrayList<>() infere seu argumento de tipo a partir de um tipo alvo; com var do lado esquerdo não há tipo alvo, então o único que sobra é Object:
javavar list = new ArrayList<>(); // infers ArrayList<Object>, not a placeholder
list.add("a");
list.add(42); // compiles happily — everything is an Object
String first = list.get(0); // error: incompatible types: Object cannot be converted to Stringjavavar list = new ArrayList<String>(); // fixed: state the type argument
List<String> other = new ArrayList<>(); // or keep the explicit type and let diamond inferDois recursos de inferência que funcionam bem sozinhos se combinam num alargamento silencioso para Object.
var captura tipos de classe anônima
Uma classe anônima tem um tipo que não tem nome, então ele não pode ser escrito em uma declaração. Declarar a variável como seu supertipo descarta esse tipo; var o preserva:
javaObject obj = new Object() {
void greet() { System.out.println("hi"); }
};
obj.greet(); // error: cannot find symbol — Object has no greet()javavar obj = new Object() {
void greet() { System.out.println("hi"); }
};
obj.greet(); // works: obj has the anonymous class's own typeEssa é uma capacidade que var desbloqueia de forma exclusiva, não uma preferência de formatação. O mesmo se aplica a tipos de interseção produzidos por uma expressão condicional, que também não têm um nome que possa ser escrito.
null sozinho não carrega tipo nenhum
null é atribuível a todo tipo de referência, então ele não fixa nada:
javavar x = null; // error: variable initializer is 'null'Um cast fornece o tipo que faltava, e qualquer expressão tipada também:
javavar x = (String) null; // fine: x is String
var y = Optional.<String>empty().orElse(null); // fine: y is StringA captura effectively-final continua igual
A inferência muda como o tipo é escrito, não como a variável se comporta. Uma local var capturada por uma lambda ou classe interna ainda precisa ser final ou effectively final:
javavar name = "ada";
name = "grace"; // reassignment makes it *not* effectively final
Runnable r = () -> System.out.println(name);
// error: local variables referenced from a lambda expression must be final or effectively finaljavavar name = "ada"; // never reassigned → effectively final
Runnable r = () -> System.out.println(name); // fineLocais var também podem ser marcadas como final var quando você quer deixar a restrição explícita.
Trade-offs
- A legibilidade corta nos dois sentidos —
varé uma vitória clara quando o inicializador nomeia o tipo, e uma perda clara quando o inicializador é uma chamada opaca. Na segunda forma, quem lê precisa consultar a assinatura do método ou depender de uma IDE para responder "o que é isso?", que é a crítica mais citada contra o uso excessivo devar:
javavar users = new ArrayList<User>(); // obvious
var result = process(input); // what is result? nothing on this line says- Literais numéricos inferem
int, silenciosamente — uma variável declarada explicitamente deixa o compilador alargar ou estreitar o literal contra o tipo declarado;varnão tem tipo declarado para o qual alargar, então você precisa guiá-lo com um sufixo ou um cast:
javalong id = 0; // widened to long by the declared type
byte flag = 5; // narrowed to byte by the declared type
var id2 = 0; // int — not long
var flag2 = 5; // int — not byte
var id3 = 0L; // fixed: the suffix carries the type
var flag3 = (byte) 5;varmais diamond silenciosamente produzObject— os dois recursos de inferência se cancelam mutuamente, e o erro geralmente aparece longe da declaração, como um erro de compilação confuso ou umaClassCastExceptiondepois de um salto sem checagem:
javavar names = new ArrayList<>(); // ArrayList<Object>, though a List<String> was intended
names.add(42); // no complaint hereCusto zero em runtime — o tipo é resolvido e embutido no arquivo de classe em tempo de compilação, então
varemite exatamente o mesmo bytecode, byte a byte, que a declaração explícita. Não há checagem de tipo em runtime, nem reflexão, nem efeito na compatibilidade binária da classe envolvente; supor o contrário é um mal-entendido comum sobre o recurso.Convenção de equipe importa mais que a regra — como a linguagem permite
varem qualquer lugar em que uma local seja permitida, a consistência precisa vir de um acordo de estilo (por exemplo: usar quando o lado direito é um construtor ou um cast, evitar para chamadas de método simples). Sem um, um código-base acaba misturando os dois estilos linha a linha sem nenhum motivo perceptível.