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.

Advertisement

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.

var is resolved entirely by javac: the class file and the JVM never see itSourcevar users = new ArrayList of Userjavac attributiontype the initializer firstUpward projectionremove capture variablesDeclared type fixedArrayList of User, foreverClass filesame bytecode as explicit typeLocalVariableTablerecords the inferred typeJVM at run timeno inference, no costInference uses only the initializer, never later assignments or how the variable is used.
How var is handled. javac types the initializer, applies upward projection to remove capture variables, and fixes the variable's type. Everything after that is identical to an explicitly typed local.

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.

PositionAllowed?Example or reason
Local variable with initializerYesvar count = 0;
Enhanced for loop variableYesfor (var e : map.entrySet())
Basic for loop initializerYesfor (var i = 0; i < n; i++)
try-with-resourcesYestry (var in = Files.newInputStream(p))
Lambda parameters (Java 11)Yes, all or none(var a, var b) -> a + b
Record pattern components (Java 21)Yesif (o instanceof Point(var x, var y))
Fields, method parameters, return typesNoPart of the API; must be explicit
catch parameterNocatch (var e) is rejected
No initializerNovar x; has nothing to infer from
null initializerNovar x = null; has no usable type
Array initializerNovar a = {1, 2}; needs a target type
Lambda or method reference initializerNovar f = () -> 1; needs a functional interface target
Compound declarationNovar a = 1, b = 2;
Array brackets on the nameNovar 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.

Advertisement

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 customers rather than list.
  • Keep the scope of var variables small, so the initializer stays close to every use.
  • Use var when the initializer makes the type obvious: constructor calls, factory methods named after the type, and literals of an intended type.
  • Use var to 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

ProblemHow it shows upPrevention
Accumulator from a literalSilent overflow or truncation via += narrowingExplicit type or a typed literal
Diamond without type argumentsArrayList<Object>; casts or errors far awayPut the type arguments on the right
Type changes when a method changesCall sites keep compiling with new semanticsExplicit types where the exact type matters
Opaque method returnsReader cannot tell what var x = load(); holdsKeep the type or improve the name
Wide scopeInitializer far from usageShorter methods, narrower scopes
Integer divisionvar r = a / b; is int when both are intCast 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

  1. In jshell, type the inferred-type examples above and check each result with /vars.
  2. Search your codebase for var declarations initialised with 0, 0.0f or similar literals that are later updated with compound operators, and fix the types.
  3. Search for var combined with <>() and add explicit type arguments.
  4. Write a one-paragraph team policy based on the OpenJDK style guidelines, and configure your IDE or linter to match it.
  5. Refactor one long stream pipeline into named var steps and compare readability in review.
  6. If you are on Java 21 or later, try var in record patterns and, on Java 22 or later, _ for unused components.
Key takeaway: var is compile-time local type inference: javac takes the static type of the initializer, fixes it, and emits exactly the same bytecode as an explicit declaration. It works only for locals with an initializer, loop variables, try-with-resources, lambda parameters (all or none) and pattern components, never for fields, parameters or return types. The inferred type is the concrete one, diamonds without type arguments become Object, and literals pick int or double whether you meant to or not, which makes compound-assignment accumulators the main trap. Use it where the right-hand side already states the type, and keep explicit types where they carry meaning. Continue with <a href="java_project_amber.html">Project Amber</a> and <a href="java_records.html">Java records</a>.