Course contents

var and local type inference

How var infers a local variable's type at compile time, where it's allowed, the diamond trap that gives you ArrayList<Object>, and when it helps readability.

FreeLesson10 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: the compiler fills in the blank
  2. Where var is allowed, and where it isn't
  3. How inference works under the hood
  4. Real world: when var helps and when it hurts
  5. Advanced: edge cases
  6. How to say it in the interview

🏷️ What is var?

var (Java 10) lets you declare a local variable without writing its type: var orders = new ArrayList<Order>(); and the compiler works out that orders is an ArrayList<Order>. The type is fixed at compile time and never changes, so var is not dynamic typing: var count = 0; count = "zero"; is a compile error, exactly as if you'd written int.

For this lesson, we'll focus on how the compiler infers the type, where var is and isn't allowed, the diamond pitfall that quietly gives you ArrayList<Object>, and the readability rules I use to decide when var helps. Interviewers ask about var mostly to check one thing: that you don't confuse it with JavaScript's var or with dynamic typing.

Below the line (out of scope)

  • Generic type inference in method calls in depth (Generics module)
  • Lambda and functional interface details (Functional Java module)
  • Pattern matching with var in record patterns (Modern Java module)

The 30-second answer

var is local variable type inference, added in Java 10 (JEP 286). The compiler takes the static type of the initialiser and uses it as the variable's type, so var is pure compile-time sugar: the bytecode is identical to writing the type yourself, and the variable stays strongly typed. It works only for local variables with an initialiser, including for loops and try-with-resources, not for fields, parameters or return types. It can't infer from null, an array initialiser or a lambda, and var list = new ArrayList<>() infers ArrayList<Object>. Since Java 11, var can also be used for lambda parameters, mainly so you can annotate them.

What you’ll learn

  • Why var is compile-time type inference, not dynamic typing, and what ends up in the bytecode
  • Exactly where var is allowed (locals, loops, try-with-resources) and why fields, parameters and return types are excluded
  • The diamond pitfall: why var list = new ArrayList<>() is an ArrayList<Object>
  • Why lambdas need a target type, and what var lambda parameters (Java 11) are for
  • A practical readability guideline for when to use var

01Picture it first: the compiler fills in the blank

Think of var as a form where you leave the "type" box blank and the compiler fills it in by reading the right-hand side, once, at compile time. After that, the box is printed in ink. At runtime there is no var anywhere: the class file only contains the inferred type.

Fig. 1

var inference happens entirely in javac: the initialiser's static type becomes the variable's type, the bytecode stores the concrete type, and later assignments are checked against it

var inference happens entirely in javac: the initialiser's static type becomes the variable's type, the bytecode stores the concrete type, and later assignments are checked against it
var names = new ArrayList<String>();   // ArrayList<String>
var count = 0;                         // int
var total = 0L;                        // long
var ratio = 0.5f;                      // float
var initial = 'A';                     // char
var path = Path.of("/var/log");        // Path

Two consequences follow straight from "the type comes from the initialiser":

  1. The inferred type is the initialiser's exact static type, not an interface you might prefer. names is an ArrayList<String>, not a List<String>. Usually harmless for a local, but worth knowing.
  2. Literals keep their literal type. var count = 0 is an int, and later count += 0.5 compiles (compound assignment casts back) and leaves count at 0.

02Where var is allowed, and where it isn't

Try it yourself first

Before reading on, guess which of these compile.

var a = 10;                          // (1)
var b;                               // (2)
var c = null;                        // (3)
var d = {1, 2, 3};                   // (4)
var e = new int[] {1, 2, 3};         // (5)
var f = () -> 42;                    // (6)
var g = 1, h = 2;                    // (7)
for (var i = 0; i < 3; i++) { }      // (8)
for (var s : List.of("x", "y")) { }  // (9)
try (var in = Files.newInputStream(p)) { } // (10)
var var = 5;                         // (11)

Lines (1), (5), (8), (9), (10) and (11) compile. The rest fail:

Line Why it fails
(2) var b; Nothing to infer from: var needs an initialiser.
(3) var c = null; null has no useful type.
(4) var d = {1, 2, 3}; An array initialiser needs a target type. Write new int[] {...}.
(6) var f = () -> 42; A lambda has no type of its own; it needs a target functional interface.
(7) var g = 1, h = 2; var isn't allowed in a compound declaration.

And outside method bodies, var is never allowed: no fields, no method parameters, no return types, no catch parameters. That's a deliberate design choice. Fields, parameters and return types are part of a class's contract, and the language designers didn't want a change in an initialiser to silently change an API.

Line (11) surprises people. var isn't a keyword, it's a reserved type name. You can still use it as a variable, method or package name, so pre-Java 10 code that had a variable called var keeps compiling. What you can't do any more is name a class var.

03How inference works under the hood

javac attributes the initialiser expression exactly as it would anywhere else, takes its static type, and then applies one adjustment called upward projection: if that type mentions captured wildcards or other types you can't write in source, it's replaced with the closest type you could write. Then the variable is declared with that type and everything downstream is checked normally.

var names = new ArrayList<String>();
names.add("atlas");
names.add(42);            // compile error: int can't be converted to String
names = new LinkedList<String>(); // compile error: LinkedList is not an ArrayList

Compile that (minus the errors) and run javap -v: the local variable table records names as ArrayList<String>. The bytecode is byte-for-byte what you'd get with the explicit type. There is zero runtime cost and zero runtime flexibility.

The diamond pitfall

Try it yourself first

What's the type of ids, and what happens on the last line?

var ids = new ArrayList<>();
ids.add(42);
ids.add("not an id");     // ?
long first = ids.get(0);  // ?

The diamond <> normally infers its type argument from the target type on the left. With var there's no target, so the inference falls back to the bound: Object. ids is an ArrayList<Object>, so adding a String compiles happily, and long first = ids.get(0) fails to compile because get returns Object. You've thrown away type safety without a single warning.

var ids = new ArrayList<Long>();      // fine: the type argument is on the right
List<Long> ids2 = new ArrayList<>();  // fine: the diamond infers from the left
Where candidates slip

Many candidates are quick to say "var with the diamond infers the generic type". It's frequently a mistake because the diamond needs something to infer from, and var removes the left-hand side. The rule I follow: never combine var with <>. Put the type argument on the right, or don't use var. The same trap exists with generic factory methods: var list = List.of() is a List<Object>.

Lambdas need a target type, and var lambda parameters (Java 11)

A lambda only means something once the compiler knows which functional interface it implements, so var f = () -> 42; can't work. Give it a target instead:

Supplier<Integer> answer = () -> 42;                     // target type on the left
var answer2 = (Supplier<Integer>) () -> 42;              // works, but the cast is noise

BiFunction<Integer, Integer, Integer> add = (var x, var y) -> x + y;           // Java 11+
BiFunction<String, String, String> join = (@NonNull var x, @NonNull var y) -> x + y;

Java 11 (JEP 323) allowed var for lambda parameters. The types are still inferred from the target, exactly as with (x, y) ->. The real reason it exists is the second line: you can't put an annotation on an implicitly typed parameter, but you can on a var one. Two rules: it's all or nothing ((var x, y) and (var x, int y) don't compile), and you can't use it without parentheses.

04Real world: when var helps and when it hurts

In the real world

On teams I've worked with, var arguments in code review almost never come from the compiler; they come from readability. The bugs it causes are rare (mostly the diamond pitfall), but code where every line is var x = service.process(input) makes reviewers open the IDE just to learn the types. The fix is a team guideline, not a ban.

The guideline I recommend, based on the OpenJDK team's own style notes:

Use var when... Avoid var when...
The type is obvious from the right-hand side: var user = new User(...), var path = Path.of(...) The type isn't visible: var result = process(data);
The type is long and noisy: var entries = map.entrySet() instead of Set<Map.Entry<String, List<Order>>> The exact numeric type matters: var timeout = 30 (is it seconds? an int? a long?)
Loop variables and try-with-resources: for (var entry : map.entrySet()) You'd need <> or List.of() on the right
The variable's scope is a few lines The variable lives across a long method

A good naming habit makes var work much better: var customerIds = ... tells the reader more than List<Long> list = ... ever did.

Tip

If you find yourself wanting var for a field or return type, that's a sign the type is too complicated. Introduce a record or a small named type instead.

05Advanced: edge cases

  • Non-denotable types. var can hold a type you can't write. With an anonymous class, var counter = new Object() { int hits; }; lets you call counter.hits++ later, because the inferred type is the anonymous class itself. Clever, rarely a good idea.
  • Intersection types. var x = flag ? 1 : "one"; infers something like Serializable & Comparable<...> after projection. Legal, but a strong hint you meant Object or a better design.
  • final var works: final var limit = 10;. Plain var declarations aren't implicitly final.
  • Capture types. For a List<?> items, var first = items.get(0); infers Object thanks to upward projection, rather than an unwritable capture type.

06How to say it in the interview

When I ask about var, I'm listening for the phrase "compile-time" in the first sentence. After that, the strong signals are knowing where it's not allowed and why (fields and signatures are API), and mentioning the diamond pitfall without being prompted.

In the interview

"var is local variable type inference from Java 10. The compiler takes the static type of the initialiser and uses it as the variable's type, so the variable is still statically typed and the bytecode is identical to writing the type by hand. It only works for locals with an initialiser, including loops and try-with-resources, not for fields, parameters or return types, because those are part of an API. It can't infer from null, an array initialiser or a lambda, and combined with the diamond it gives you ArrayList<Object>, so I put the type argument on the right. I use it when the type is obvious from the right-hand side and avoid it when it hides the type."

Here's how this might look if you sketch it on the whiteboard:

On the whiteboard

var (Java 10)