var: Local Variable Type Inference
Objective
Understand var (JEP 286, Java 10): a local variable type inference form where the compiler derives the variable's static type from its initializer at compile time. var is not dynamic typing and not a synonym for Object — the inferred type is fixed forever at that declaration and is enforced exactly as strictly as if you had typed it out. Its value is removing redundant type noise where the right-hand side already states the type (var list = new ArrayList<String>();), not saving keystrokes for their own sake.
Use Cases
- Dropping a duplicated type name when a constructor or factory call already announces it, especially for long generic types (
var users = new ArrayList<User>();,var index = new ConcurrentHashMap<String, List<Order>>();). - Enhanced
forloops and try-with-resources, where the declared type only repeats what is already visible on the right (for (var entry : map.entrySet())). - Locals holding the result of a chained stream or builder call whose type is verbose but obvious from the surrounding lines.
- Capturing the type of an anonymous class expression — including members it declares beyond its supertype — which cannot be written out explicitly at all.
Deep Dive
Where var is allowed
var is legal only for local variables that have an initializer, plus the loop and resource variables that are locals in disguise:
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 is a reserved type name, not a keyword, so existing code that uses var as an identifier still compiles:
javaint var = 3; // legal: var is not a keyword
var var = 3; // also legal, and also a terrible ideaWhere var is forbidden
Every position below is a compile error, because inference only ever runs against a local initializer.
Fields — instance or static — have no inference at all:
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;
}Method parameters and return types are part of the signature, which callers compile against:
javavar total(var amounts) { } // error: 'var' is not allowed here (twice)javalong total(List<Long> amounts) { return 0L; } // fixedCatch clause parameters name the type being caught, which is what selects the 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();
}A declaration with no initializer gives the compiler nothing to infer from — and so does a bare array initializer, which itself needs a target type:
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};The legacy "brackets after the name" array syntax cannot combine with var, and neither can a compound declaration:
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 in lambda parameters (Java 11+), all or nothing
Since Java 11 a lambda's formal parameters may use var, which is what lets you attach an annotation or a modifier to an otherwise implicitly typed parameter. The rule is that the parameter list must be uniform:
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 varA single var parameter also keeps its parentheses — var x -> x does not compile, only (var x) -> x.
Inference reads the static type of the initializer
The inferred type is whatever the compiler statically computes for the right-hand side, not the runtime class of the value:
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()This is what makes the diamond gotcha bite. new ArrayList<>() infers its type argument from a target type; with var on the left there is no target type, so the only thing left is 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 inferTwo inference features that each work fine alone combine into a silent widening to Object.
var captures anonymous class types
An anonymous class has a type that has no name, so it cannot be written in a declaration. Declaring the variable as its supertype throws that type away; var keeps it:
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 typeThis is a capability var uniquely unlocks rather than a formatting preference. The same applies to intersection types produced by a conditional expression, which likewise have no writable name.
null alone carries no type
null is assignable to every reference type, so it pins nothing down:
javavar x = null; // error: variable initializer is 'null'A cast supplies the missing type, and so does any typed expression:
javavar x = (String) null; // fine: x is String
var y = Optional.<String>empty().orElse(null); // fine: y is StringEffectively-final capture is unchanged
Inference changes how the type is written, not how the variable behaves. A var local captured by a lambda or inner class must still be final or 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); // finevar locals can also be marked final var when you want the restriction stated explicitly.
Trade-offs
- Readability cuts both ways —
varis a clear win when the initializer names the type, and a clear loss when the initializer is an opaque call. In the second form the reader has to consult the method signature or lean on an IDE to answer "what is this?", which is the most commonly cited criticism of overusingvar:
javavar users = new ArrayList<User>(); // obvious
var result = process(input); // what is result? nothing on this line says- Numeric literals infer
int, silently — an explicitly declared variable lets the compiler widen or narrow the literal against the declared type;varhas no declared type to widen toward, so you must steer it with a suffix or a 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;varplus diamond quietly yieldsObject— the two inference features cancel each other out, and the mistake usually surfaces far from the declaration as a confusing compile error or aClassCastExceptionafter an unchecked hop:
javavar names = new ArrayList<>(); // ArrayList<Object>, though a List<String> was intended
names.add(42); // no complaint hereZero runtime cost — the type is resolved and baked into the class file at compile time, so
varemits byte-for-byte the same bytecode as the explicit declaration. There is no runtime type check, no reflection, and no effect on binary compatibility of the enclosing class; assuming otherwise is a common misreading of the feature.Team convention matters more than the rule — because the language permits
vareverywhere a local is allowed, consistency has to come from a style agreement (for example: use it when the right-hand side is a constructor or a cast, avoid it for bare method calls). Without one, a codebase ends up mixing both styles line by line for no discernible reason.