Java var Keyword: Local Type Inference Explained
Learn how the java var keyword works, where it can and cannot be used, and how local type inference affects readability and maintainability.
The java var keyword, introduced in Java 10, lets you declare local variables without writing an explicit type. Instead, the compiler infers the type from the initializer on the right side of the assignment. For example:
var count = 42; // inferred as int var name = "Ada"; // inferred as String var list = new ArrayList<String>(); // inferred as ArrayList<String>
This is not dynamic typing. The variable still has a fixed, compile-time type; you just don't write it out. The compiler determines the type from the initializer expression and enforces it for the rest of the variable's scope.
How var Performs Type Inference
Type inference with var follows the same rules as generic method type inference. The compiler looks at the initializer expression and computes the most specific type that is compatible with the expression's static type. For instance, if you assign a lambda to a var, the inferred type is the functional interface type that the lambda targets, which requires the lambda to be cast or used in a context that provides a target type.
var runnable = (Runnable) () -> System.out.println("run");
Without the cast, the lambda has no target type, and the compiler would reject the declaration. This is an important distinction: var does not change how the expression is typed; it only hides the explicit type name.
The inferred type is always the compile-time type of the initializer. If you assign an int literal, you get int, not long or float. If you assign a method call that returns a List<String>, you get List<String>, not ArrayList<String> even if the actual object is an ArrayList. The static type is what matters.
Where var Is Allowed
var can only be used for local variables. That includes variables declared inside methods, constructors, and initializer blocks. It also works in for-loop initializers and enhanced for-loop variables:
for (var i = 0; i < 10; i++) { // i is int } for (var item : items) { // item type is inferred from items' element type }
In a try-with-resources statement, var can replace the resource type:
try (var input = new FileInputStream("data.txt")) { // input is FileInputStream }
In all these cases, the variable must have an initializer, because there is no explicit type to fall back on. The initializer must be present and must not be null, because null has no type to infer.
Where var Cannot Be Used
var is restricted to local variables. It cannot be used for fields, method parameters, return types, or catch clause parameters. Those positions require an explicit type because they are part of the method signature or class contract, and the compiler cannot infer them from a single local initializer.
For example, this is invalid:
public class Example { var name = "invalid"; // compile error }
A method parameter cannot be var either:
void print(var value) { // compile error }
You also cannot use var in a lambda parameter list, even if the target type is known:
list.stream().map((var s) -> s.length()); // error: lambda parameters need explicit types
The Java language specification explicitly forbids var in lambda parameters because it would conflict with the existing type inference for lambda expressions.
var and the Diamond Operator
One common mistake is combining var with the diamond operator on anonymous classes or when the initializer already has a type parameter. For example:
var list = new ArrayList<>(); // inferred as ArrayList<Object>
The diamond operator uses the target type to infer the generic type arguments. When you use var, there is no target type from the left side, so the compiler falls back to the upper bound of the type parameter, which is Object. This is often not what you want.
To get a specific generic type, you must specify it explicitly in the initializer:
var list = new ArrayList<String>(); // inferred as ArrayList<String>
This behavior is a key difference from using an explicit type on the left, where the diamond operator can infer from the assignment context.
Impact on Readability and Maintainability
var can make code more concise, especially when the type is long and repetitive. For example:
Map<String, List<Map<String, Integer>>> complex = new HashMap<>();
becomes:
var complex = new HashMap<String, List<Map<String, Integer>>>();
The type still appears on the right, so the declaration is not hiding information; it is moving it to a more natural position. However, when the initializer is a method call, the inferred type may not be obvious:
var result = service.fetchData();
Here, the reader must know what fetchData returns. If the method name is clear, this is fine. If not, it hurts readability. The Java style guide suggests using var only when the type is evident from the initializer or when the type is not important to the reader.
Maintainability also improves when the initializer's type changes. If you change a method to return a different type, var declarations that use it will automatically adapt, as long as the new type is compatible with the subsequent code. This can reduce the number of changes needed during a refactor, but it can also hide breaking changes if the new type changes the behavior of overloaded method calls.
Common Pitfalls and Misconceptions
A common misconception is that var makes a variable dynamic or changes its type at runtime. It does not. The type is fixed at compile time, and there is no runtime overhead. The bytecode is identical to using an explicit type; var is purely a source-level feature.
Another pitfall is using var with primitive arrays. The inferred type is the array type, but you must still use the correct initializer:
var numbers = new int[] {1, 2, 3}; // inferred as int[]
You cannot use var to declare an array without an initializer, because the array size is part of the type, and the null initializer is not allowed.
A subtle issue arises with var and method overloading. If the initializer is a method call that is overloaded, the compiler picks the most specific overload based on the argument types. The result type is then fixed. Changing the method's return type later can change the inferred type of the var, which may cause compilation errors in code that depends on the old type. This is a maintainability consideration, not a bug in var itself.
Compatibility and Migration Considerations
var is available in Java 10 and later. If your project targets an earlier Java version, you cannot use it. For projects on Java 11 or newer, var is a safe addition because it does not affect the compiled bytecode or the public API. It is purely a local variable feature, so it does not change method signatures or class contracts.
When migrating existing code, you can introduce var gradually. The compiler will infer the same type that was explicitly written, so behavior does not change. However, you should review each usage for readability. A good rule is to use var when the type is obvious from the initializer, such as with constructors or simple literals, and avoid it when the type is not clear from the expression or when the variable name alone does not convey the type.
One specific compatibility concern is with the diamond operator. As shown earlier, var list = new ArrayList<>() infers ArrayList<Object>, which is different from ArrayList<String> if you had an explicit type on the left. When migrating code that relies on diamond inference, you must add explicit type arguments to the initializer to preserve the original type. This is a common source of subtle bugs during migration.
Another edge case is using var in a for-each loop over an array of primitives. The loop variable will be the primitive type, not the wrapper type. For example:
int[] values = {1, 2, 3}; for (var v : values) { // v is int, not Integer }
This matches the behavior of an explicit int declaration, so there is no surprise, but it is worth remembering when you later change the array type.
Finally, note that var cannot be used in a lambda parameter list, even with the @Nonnull annotation style that Java 11 introduced for var in lambdas. The syntax (var s) -> ... is allowed only if the lambda parameter type is inferred from the functional interface target, but the var is not allowed there. This restriction exists to avoid confusion with the existing type inference for lambdas and is unlikely to change in future Java versions.