Java 10 added var for local variables through JEP 286, and Java 11 extended it to lambda parameters through JEP 323. On the surface the feature is tiny: write var instead of a type, and the compiler fills the type in. In practice it raises real questions. What type does the compiler choose? Does it change runtime behaviour? Where is it illegal, and why? When does it make code clearer, and when does it hide something the reader needed?
This article answers those questions from the compiler's point of view, with examples you can paste into jshell. It goes well beyond the short var section in Project Amber, the umbrella project that delivered it, and ends with a team policy you can adopt.
What var is, and what it is not
var is local variable type inference. When javac sees var x = expr;, it computes the static type of expr and gives x that type, exactly as if you had written it. The variable is still statically typed: you cannot later assign a value of an incompatible type. It is not a dynamic type, not like JavaScript's var, and not an Object in disguise.
Technically var is a reserved type name, not a keyword. That choice preserved compatibility: a variable, method or package named var still compiles, so var var = 1; is legal, if unkind to readers. What became illegal in Java 10 is using var as the name of a class, interface or type parameter, which was already against naming conventions.
The feature is resolved entirely at compile time. The class file contains the same bytecode it would with an explicit type; the inferred type is recorded in the LocalVariableTable debug attribute (when compiled with -g), just as an explicit type would be. There is no runtime cost and no runtime behaviour change. You can confirm this by compiling both versions and comparing javap -c output.
Where var is allowed
The rule of thumb: var works where the compiler has an initializer to look at and the variable is local. Everything else is excluded on purpose, because fields, parameters and return types form a class's API, and inferring them would let an implementation change silently alter a contract.
| Position | Allowed? | Example or reason |
|---|---|---|
| Local variable with initializer | Yes | var count = 0; |
| Enhanced for loop variable | Yes | for (var e : map.entrySet()) |
| Basic for loop initializer | Yes | for (var i = 0; i < n; i++) |
| try-with-resources | Yes | try (var in = Files.newInputStream(p)) |
| Lambda parameters (Java 11) | Yes, all or none | (var a, var b) -> a + b |
| Record pattern components (Java 21) | Yes | if (o instanceof Point(var x, var y)) |
| Fields, method parameters, return types | No | Part of the API; must be explicit |
| catch parameter | No | catch (var e) is rejected |
| No initializer | No | var x; has nothing to infer from |
| null initializer | No | var x = null; has no usable type |
| Array initializer | No | var a = {1, 2}; needs a target type |
| Lambda or method reference initializer | No | var f = () -> 1; needs a functional interface target |
| Compound declaration | No | var a = 1, b = 2; |
| Array brackets on the name | No | var a[] = new int[3]; |
The lambda and array-initializer cases are the same problem from opposite directions. Those expressions have no type of their own; they get one from the declared target. With var there is no target, so there is nothing to infer.
What type gets inferred
This is where most surprises live. The compiler uses the initializer's type and nothing else. Later assignments, method calls on the variable and how you use it later are all ignored.
var list = new ArrayList<String>(); // ArrayList<String>, not List<String>
var raw = new ArrayList<>(); // ArrayList<Object>: the diamond has no target to infer from
var none = Collections.emptyList(); // List<Object>, for the same reason
var i = 10; // int
var l = 10L; // long
var b = (byte) 10; // byte; a plain 10 would be int
var d = 1 / 3; // int, value 0, not 0.333...
var ch = 'a'; // char
var o = new Object() { int hits = 0; };
o.hits++; // legal: o has the anonymous class type
var mixed = flag ? 1 : "one"; // an intersection type (Serializable, Comparable and more)Three consequences follow. First, var gives you the concrete class, so list above is an ArrayList. Later code can call ArrayList-only methods such as ensureCapacity, which ties the method to that implementation. Inside a short method this rarely matters; it is why the style guidance says not to worry much about programming to the interface for locals, while keeping small scopes.
Second, the diamond and generic methods need a target to infer type arguments. Without one you get Object, and the mistake shows up later as a cast or a compile error far from the declaration. Always give the type argument explicitly when you combine var with new ...<>().
Third, some inferred types cannot be written down at all: anonymous class types and intersection types. Being able to call o.hits on an anonymous object is occasionally useful in a test, but it is a curiosity, not a design tool. If the compiler infers a capture type, such as CAP#1 from a wildcard, javac applies upward projection to replace it with a denotable supertype, so var never gives a variable a type you could not write by hand, apart from those anonymous and intersection cases.
The compound-assignment trap
The most dangerous var bug compiles cleanly. Compound assignment operators such as += contain an implicit narrowing cast back to the variable's type:
long[] sizes = {3_000_000_000L, 2_000_000_000L};
var total = 0; // inferred as int
for (long s : sizes) {
total += s; // compiles: means total = (int) (total + s)
}
System.out.println(total); // prints a wrapped, wrong number
long safe = 0; // explicit type, or: var safe = 0L;
for (long s : sizes) safe += s;The same thing happens with var ratio = 0; followed by ratio += 0.5;, which keeps the value at zero because each result is cast back to int. An explicit int has the same problem, but writing the type forces you to choose it. With var the type comes from a literal that was probably chosen without thinking. Rule: never initialise an accumulator with a bare literal through var; write the explicit type or a typed literal such as 0L or 0.0.
A worked refactoring
Here is a typical method before and after. The goal is not to replace every type, but to remove repetition where the right-hand side already says what the type is.
// Before
Map<String, List<Order>> byCustomer = new HashMap<String, List<Order>>();
for (Map.Entry<String, List<Order>> entry : source.entrySet()) {
BufferedReader reader = Files.newBufferedReader(Path.of(entry.getKey()));
OrderSummary summary = summarise(entry.getValue());
int failures = 0;
// ...
}
// After
var byCustomer = new HashMap<String, List<Order>>(); // type is on the right
for (var entry : source.entrySet()) { // obvious from the loop
try (var reader = Files.newBufferedReader(Path.of(entry.getKey()))) {
OrderSummary summary = summarise(entry.getValue()); // keep: method name hides the type
int failures = 0; // keep: accumulator from a literal
// ...
}
}Each decision follows one question: can a reader who sees only this line, without an IDE, tell what the type is and why it matters? The constructor call and the entry loop pass. The summarise call does not, because the method name does not reveal its return type, so the explicit type stays. The counter keeps its type for the reason in the previous section.
var with generics and streams
var shines with long generic types and intermediate results that would otherwise need a wall of angle brackets. It is also useful for splitting a long stream pipeline into named steps without paying the type-writing cost:
var paid = orders.stream()
.filter(o -> o.status() == Status.PAID)
.toList(); // List<Order>
var byCountry = paid.stream()
.collect(Collectors.groupingBy(Order::country,
Collectors.summingLong(Order::cents))); // Map<String, Long>Named intermediate variables make debugging easier, because you can inspect each step. The risk is that a later change to a method signature silently changes the inferred type. If summingLong were replaced by averagingDouble, byCountry would become Map<String, Double> and every downstream use would either still compile with different semantics or fail far away. Where the exact type is part of the meaning, write it. For more on how Java infers type arguments in these chains, see Java generics.
var in lambdas, patterns and unnamed variables
Java 11 allowed var on implicitly typed lambda parameters. The type is the same one that would be inferred without it, so the only reason to write it is to attach annotations or modifiers, which need a type position to sit on:
BiFunction<String, String, String> join = (@NonNull var a, @NonNull var b) -> a + b;
// Rejected: you cannot mix forms in one lambda
// (var a, b) -> ... (var a, String b) -> ...The annotation in that example stands for whichever nullness annotation your project uses; Java itself does not ship one. For the rest of the lambda syntax, see Java lambda expressions.
Java 21 made record patterns final (JEP 440), and var is allowed for a component you want bound without restating its type: if (shape instanceof Rect(var topLeft, var size)). Java 22 added unnamed variables and patterns (JEP 456), so a component or variable you do not need can be written as _, as in case Rect(var topLeft, _) ->. Together they let deconstruction patterns stay short even when records have many components; pattern matching in Java covers the full feature.
Readability rules that hold up
The OpenJDK project published Local Variable Type Inference: Style Guidelines (by Stuart Marks) alongside the feature. Its principles are worth adopting directly: reading code matters more than writing it; code should be understandable from local reasoning; readability should not depend on an IDE; and explicit types are a trade-off, not a sin. Its concrete guidelines condense to these:
- Choose variable names that carry the information the type used to carry, such as
customersrather thanlist. - Keep the scope of
varvariables small, so the initializer stays close to every use. - Use
varwhen the initializer makes the type obvious: constructor calls, factory methods named after the type, and literals of an intended type. - Use
varto break up long chained or nested expressions into named steps. - Do not worry too much about programming to the interface for locals.
- Take care with the diamond and with generic methods, and take care with literals.
Code review is where these rules get enforced. Most IDEs and static analysers can flag or suggest var; pick one setting for the codebase so the choice is not re-argued in every pull request.
Failure modes and trade-offs
| Problem | How it shows up | Prevention |
|---|---|---|
| Accumulator from a literal | Silent overflow or truncation via += narrowing | Explicit type or a typed literal |
| Diamond without type arguments | ArrayList<Object>; casts or errors far away | Put the type arguments on the right |
| Type changes when a method changes | Call sites keep compiling with new semantics | Explicit types where the exact type matters |
| Opaque method returns | Reader cannot tell what var x = load(); holds | Keep the type or improve the name |
| Wide scope | Initializer far from usage | Shorter methods, narrower scopes |
| Integer division | var r = a / b; is int when both are int | Cast or use a double operand |
The trade-off is simple to state. var removes redundancy and lets you name intermediate values cheaply, at the cost of moving type information from the declaration to the initializer. When the initializer carries that information, nothing is lost. When it does not, the reader pays. And because the bytecode is identical, the decision is purely about people, never about performance.
What to do next
- In
jshell, type the inferred-type examples above and check each result with/vars. - Search your codebase for
vardeclarations initialised with0,0.0for similar literals that are later updated with compound operators, and fix the types. - Search for
varcombined with<>()and add explicit type arguments. - Write a one-paragraph team policy based on the OpenJDK style guidelines, and configure your IDE or linter to match it.
- Refactor one long stream pipeline into named
varsteps and compare readability in review. - If you are on Java 21 or later, try
varin record patterns and, on Java 22 or later,_for unused components.