You type ArrayList<HashMap<String, List<Order>>> on the left, then new ArrayList<HashMap<String, List<Order>>>() on the right. The constructor already named the type. The left-hand side is noise.
Java 10 lets you write var there. The compiler still knows the type; the reader still sees it — on the right.
What var is not is a license to hide types. If the right-hand side does not name the type, write the type on the left.
var users = new ArrayList<User>(); // constructor names it
Map<UserId, User> byId = directory.load(); // load() does not
This post is the decision rule: when local-variable type inference clarifies, when it hides, and the declarations where var is simply illegal.
When it shipped
| Release | Status | Spec |
|---|---|---|
| Java 10 | Standard feature | JEP 286 |
| Java 11 | var on lambda parameters | JEP 323 |
Use Java 10+. There was no preview. var is a reserved type name, not a keyword — a method or variable named var still compiles; a class named var does not.
Catch parameters stay explicit. Java 11 later allows var on lambda formals (all or none) so you can annotate them. Java 22 later lets you write _ for a local you will not read — that is unnamed variables, not type inference.
Mental model: infer from the initializer
The compiler does not guess. It takes the type of the initializer and copies it onto the variable.
var name = initializer;
// ↑ this expression's type becomes the variable's type
That is why there must be an initializer, why null is rejected, and why a lambda with no target type fails. Nothing on the left is available yet to aim at.
| You write | Compiler sees |
|---|---|
var list = new ArrayList<String>(); | ArrayList<String> |
var stream = list.stream(); | Stream<String> |
var path = Path.of("config.yml"); | Path |
List<String> names = new ArrayList<>(); | List<String> (you named the interface) |
var names = new ArrayList<String>(); | ArrayList<String> (concrete class) |
The last two rows matter. With var, you are not “programming to the interface.” You get the exact runtime class of the initializer. For a short-lived local that is usually fine. For a local you might later swap to LinkedList through a List binding, write List.
Where var is legal
JEP 286 allows var only for initialized local variables, including enhanced-for indexes and the locals declared in a traditional for. Try-with-resources counts: those resources are initialized locals.
var path = Path.of("orders.csv");
try (var in = Files.newBufferedReader(path)) {
for (var line = in.readLine(); line != null; line = in.readLine()) {
// ...
}
}
Path.of is a location in the JVM; the Files operations that use it are java.nio.file. This snippet is only why var is legal on that local.
for (var item : items) {
process(item);
}
It is not legal on fields, method or constructor parameters, return types, or catch parameters. Those are the published contract of a type or a method; they stay explicit.
class OrderService {
// var cache = new HashMap<String, Order>(); // not a field
// Order find(var id) { ... } // not a parameter
// var find(String id) { return repo.get(id); } // not a return type
}
Note: No initializer, no var. Split declarations (var a = 1, b = 2) and extra array brackets on the name (var slots[] = ...) are also rejected.
When var clarifies
Use it when the right-hand side already tells the reader the type.
Constructors and factories that name the class:
var orders = new ArrayList<Order>();
var ids = new HashSet<OrderId>();
var path = Path.of("config.yml");
var now = Instant.now();
Long generic instantiations, where repeating the type arguments is the noise:
var index = new HashMap<CustomerId, List<Order>>();
var grouped = orders.stream()
.collect(Collectors.groupingBy(Order::customerId));
groupingBy still infers Map<CustomerId, List<Order>>. The stream pipeline is the documentation; spelling the map type on the left does not add a fact.
Tiny-scope locals next to the expression that creates them:
var reader = Files.newBufferedReader(path);
var first = reader.readLine();
newBufferedReader returns BufferedReader; readLine returns String. A reader who knows the NIO API does not need those names written twice.
When var hides
Skip it when the initializer does not name the type, or names the wrong amount of it.
A method call with a vague name
var result = service.load(id);
var data = repo.findAll();
var value = parse(line);
load might return Order, Optional<Order>, or OrderView. The reviewer has to jump to the method. Write the type:
Optional<Order> result = service.load(id);
List<Order> data = repo.findAll();
BigDecimal value = parse(line);
If the RHS does not name the type, write the type on the left.
Diamond with no information
Diamond infers from the left. var infers from the right. Using both leaves nothing to infer from:
var list = new ArrayList<>(); // ArrayList<Object> — compiled, almost never what you meant
Put the type arguments on the constructor, or drop var:
var list = new ArrayList<String>();
List<String> list = new ArrayList<>();
null without a type
var x = null; // error: variable initializer is 'null'
null has no useful type. Give the variable a type, then assign null if you must:
Order pending = null;
Literals that look interchangeable
var count = 1; // int
var total = 1.0; // double
var id = 1L; // long
var flag = true; // boolean
Those are fine when the primitive is obvious. They hide a problem when you needed BigDecimal, Integer, or an enum. If the literal does not make the domain type obvious, write the domain type.
Public-facing locals that are the API
var never appears in a method signature, so the published contract stays typed. The trap is locals that carry that contract across a long method:
public OrderResponse toResponse(Order order) {
var body = mapper.toBody(order); // what is body?
var links = linker.forOrder(order); // what is links?
return new OrderResponse(body, links);
}
A reader of a public mapper should see OrderBody and List<Link> without opening two other types. Use var in private helpers with tiny scope; keep the types visible where the method is the documentation.
Poly expressions need a target type
Lambdas, method references, and array initializers are poly expressions: they need a target type on the left. var provides none, so these fail:
var fn = s -> s.toUpperCase(); // error: lambda needs an explicit target-type
var size = this::size; // error: method reference needs an explicit target-type
var ints = { 1, 2, 3 }; // error: array initializer needs an explicit target-type
Give them a type, or use var only after the target is already a typed expression:
Function<String, String> fn = s -> s.toUpperCase();
IntSupplier size = this::size;
int[] ints = { 1, 2, 3 };
var fn2 = Function.<String, String>identity().andThen(String::toUpperCase);
Java 11 lets you write var on lambda parameters ((var x, var y) -> ...) so you can annotate them. That is still not “infer the lambda’s type from var on the left.” The functional interface remains the target.
Anonymous classes and intersection types
Most initializers have a type you could have written by hand. Two that do not:
Anonymous class. The inferred type is the synthetic class, so you can call methods declared only in the body:
var handler = new Object() {
void onTimeout() {
log.warn("timed out");
}
};
handler.onTimeout(); // legal — type is the anonymous class, not Object
Useful as a one-off bundle of methods. You cannot name that type elsewhere, so do not pass handler out of the method.
Intersection types. A cast or a ternary can infer A & B, which you cannot write as a variable type in source. That is legal and occasionally handy; it is also a reason a later assignment looks mysterious. If the intersection is the point of the local, a small interface or a record is clearer than var.
Note: Capture variables (the ? that wildcards turn into) are not kept as the variable’s type. The compiler projects them to a denotable supertype so var does not leak wildcard captures into the next statement.
Style rule
Read the right-hand side the way a reviewer would.
| RHS | Left-hand side |
|---|---|
| Constructor or factory already names the type | var |
| Long generics duplicated on both sides | var |
Method / field access whose type is obvious from the name (path.getFileName()) | var is fine |
Method whose return type is the fact you need (service.load) | Write the type |
Diamond, null, lambda, method reference, array initializer | Cannot, or should not, use var |
Field, parameter, return type, catch | Illegal |
Keep the local’s scope small. var at the top of a 80-line method is how types disappear. var two lines above its only use is how they stay obvious.
Do not use var to dodge a type you have not decided yet. If you cannot name the type, the design is unfinished — not the declaration.
Cheat sheet
Java 10 / JEP 286: final, no preview
Compiler copies the initializer's type onto the local — nothing is "dynamic"
Legal: initialized locals, for-loop indexes, enhanced-for, try-with-resources
Illegal: fields, method/ctor params, return types, catch, no initializer
Java 11: var on lambda parameters (all or none) so you can annotate them
var list = new ArrayList<String>(); // ArrayList<String>
var list = new ArrayList<>(); // ArrayList<Object> — don't
var x = null; // error
var fn = s -> s.trim(); // error (no target type)
List<String> xs = new ArrayList<>(); // interface type you chose
var xs = new ArrayList<String>(); // concrete ArrayList<String>
Style: if the RHS does not name the type, write the type on the left
Do:
- Use
varwhen the constructor, factory, or obvious API already names the type. - Put type arguments on the
newside (new ArrayList<String>()), not on an empty diamond next tovar. - Keep
varlocals in a tight scope so the initializer is still on screen. - Write explicit types on public mappers and long methods where the type is the documentation.
Don’t:
- Pair
varwith a bare diamond — you will getArrayList<Object>. - Declare
var x = nullorvar fn = () -> ...and expect the compiler to invent a type. - Put
varon fields, parameters, or return types — it will not compile, and those slots should stay explicit anyway. - Hide
service.load(id)behindvarwhen the return type is what the next ten lines depend on. - Treat
varas optional typing. Everyvarlocal still has a static type; you just asked the compiler to copy it from the right.
Wrap-up
JEP 286 is a small syntax change with a sharp scope: initialized locals only, type taken from the initializer, no new dynamism. Java 10 made it final; Java 11 only added lambda-parameter syntax on top.
The feature pays off when the right-hand side already says ArrayList<String> or Path. It costs when the right-hand side says load() and the left-hand side used to say Optional<Order>. Use that as the whole style guide: if the RHS does not name the type, write the type on the left.
Once locals are quieter, the next ceremony worth deleting is the switch that assigns a result through break. Switch expressions return a value directly.