1.2 Local Variable Type Inference (`var`) Rules and Scoping
Key Takeaways
var is a contextual reserved type name (not a keyword) introduced in Java 10 that instructs the compiler to infer the exact static type of a local variable from its mandatory initializer expression.
var is strictly permitted for local variables with initializers, loop index/element variables, and try-with-resources resource declarations; it cannot be used for fields, method parameters, return types, or catch clauses.
Declarations using var cannot be initialized to null, cannot omit initializers, cannot use compound declaration lists (var a = 1, b = 2), and cannot use array shorthand initializers ({1, 2, 3}).
Using var with the diamond operator (
var list = new ArrayList<>()) infersArrayList<Object>; explicit type parameters are required on the constructor to infer specific generic types.Local variables declared with var can infer non-denotable types such as anonymous class instances and intersection types, permitting access to anonymous members that are inaccessible via standard supertypes.
Local Variable Type Inference (var) Rules and Scoping
Local variable type inference, introduced via JEP 286 in Java 10 and expanded in Java 11, allows developers to omit explicit type declarations by writing var. While var reduces boilerplate, it introduces nuanced compile-time mechanics, strict syntax restrictions, and scoping interactions that are primary targets for examination.
The Nature of var: Static Typing and Reserved Type Names
A critical misconception is that var behaves like JavaScript's dynamic variables or Python's duck typing. In Java, var maintains 100% static typing. The compiler inspects the compile-time type of the initializer expression and substitutes that exact, concrete type into the bytecode. Once compiled, the variable has a single, immutable static type.
Furthermore, var is not a keyword; it is a reserved type name. This design choice ensures backwards compatibility with legacy codebases:
int var = 100; // Legal! 'var' can be used as a variable or method identifier
void var() {} // Legal! 'var' can be used as a method name
// class var {} // COMPILE ERROR: 'var' cannot be used as a class or interface name
Where var Is Permitted
Java allows var in exactly five contexts, all tied to local execution scopes:
- Local variables with an immediate initializer:
var message = "Hello, Java 17!"; // Inferred as java.lang.String var count = 42; // Inferred as int - Enhanced
forloops (for-each):List<String> names = List.of("Alice", "Bob"); for (var name : names) { // Inferred as String System.out.println(name.toUpperCase()); } - Traditional index-based
forloops:for (var i = 0; i < 10; i++) { // Inferred as int System.out.print(i); } - Try-with-resources variable declarations:
try (var fis = new FileInputStream("app.log")) { // Inferred as FileInputStream int data = fis.read(); } - Lambda formal parameters (Java 11+):
When using
varin lambda parameter lists, all parameters must usevar. This syntax exists primarily to apply annotations to inferred lambda parameters:BiFunction<Integer, Integer, Integer> add = (var x, var y) -> x + y; // Legal // BiFunction<Integer, Integer, Integer> mix = (var x, int y) -> x + y; // COMPILE ERROR: cannot mix var with explicit types
Where var Is Strictly Forbidden
The compiler rejects var in any position where type information cannot be immediately resolved from a right-hand initializer, or where public API contracts require explicit typing:
| Disallowed Context | Code Example | Reason for Rejection |
|---|---|---|
| Instance / Static Fields | class Car { var speed = 50; } | Class fields define public or internal API state; inference would complicate compilation units and circular dependencies. |
| Method Parameters | void drive(var miles) {} | Method parameter types define class contracts and overload signatures. |
| Method Return Types | var calculate() { return 10; } | Return types define public contracts; inference would cause circular call graph dependency issues. |
| Constructor Parameters | public Car(var model) {} | Constructor signatures must be explicitly typed. |
| Catch Block Parameters | catch (var ex) {} | Exception parameter types must explicitly list the catchable hierarchy or union types (IOException | SQLException). |
| Uninitialized Variables | var total; total = 5; | The compiler cannot infer a type without an immediate initializer expression on the declaration line. |
Explicit null Initializer | var item = null; | The literal null is compatible with any reference type; the compiler cannot resolve which specific type to infer. |
| Compound Declarations | var x = 1, y = 2; | var cannot be used in multi-variable declarations, even if all initializers share the same type. |
| Array Shorthand | var nums = { 1, 2, 3 }; | Array initializer { ... } requires an explicit target array type (new int[]{ 1, 2, 3 } is required). |
| Bare Lambdas / Method References | var fn = s -> s.length(); | Lambdas and method references require an explicit target functional interface type. |
Warning
The Array Shorthand and Lambda Traps
You cannot write var arr = { 1, 2, 3 }; because the {} syntax relies on target typing provided by the left-hand variable declaration (int[] arr = { 1, 2, 3 };). With var, no target type exists! To use var with arrays, you must supply the explicit type on the right-hand side: var arr = new int[]{ 1, 2, 3 };.
Type Inference Mechanics and Advanced Nuances
The Diamond Operator (<>) Pitfall
When var is combined with the diamond operator new ArrayList<>(), there is no left-hand type to supply the generic type parameter. In this scenario, the compiler defaults the generic type argument to Object:
var list = new ArrayList<>(); // Inferred as ArrayList<Object>!
list.add("Java 17");
list.add(42);
// String s = list.get(0); // COMPILE ERROR: list.get(0) returns Object, cannot assign to String
Object obj = list.get(0); // Compiles successfully
// Correct approach to infer ArrayList<String>:
var stringList = new ArrayList<String>(); // Inferred as ArrayList<String>
String s = stringList.get(0); // Compiles successfully
Non-Denotable Types and Anonymous Classes
Because var infers the exact compile-time type, it can capture non-denotable types that cannot be named in standard Java source code. A prominent example is an anonymous class instance. With explicit typing, assigning an anonymous class instance to Object hides all fields and methods declared inside the anonymous body. With var, the compiler infers the anonymous subtype itself:
var point = new Object() {
int x = 10;
int y = 20;
int distance() { return x + y; }
};
System.out.println(point.x); // Compiles! Prints 10
System.out.println(point.distance()); // Compiles! Prints 30
If Object point had been used, point.x would fail compilation because x is not a member of Object.
Reassignment and Type Compatibility
Once a variable is declared with var, its inferred type is immutable. Assigning a value of an incompatible type causes a compile-time error:
var number = 10; // Inferred as int
number = 20; // Valid: 20 is an int
// number = "ten"; // COMPILE ERROR: incompatible types: String cannot be converted to int
// number = 3.14; // COMPILE ERROR: incompatible types: possible lossy conversion from double to int
Scope and Variable Shadowing
Local variables declared with var obey standard Java block-scoping rules:
- A local variable exists from the moment it is declared until the closing brace
}of its enclosing block. - Java forbids variable shadowing between local blocks in the same method. You cannot redeclare a variable name if it was already declared in an enclosing scope within that method:
public void testScope() {
var x = 10;
{
// var x = 20; // COMPILE ERROR: variable x is already defined in method testScope()
}
}
However, a local variable (declared with or without var) can shadow an instance or static field of the surrounding class:
public class ShadowDemo {
int count = 100; // Instance field
public void show() {
var count = 5; // Valid: local var shadows the instance field
System.out.println(count); // Prints 5 (local variable)
System.out.println(this.count); // Prints 100 (instance field accessed via this)
}
}
Which of the following local variable declarations compiles without any compiler errors?
var action = (String s) -> s.toUpperCase();
var values = new int[] { 10, 20, 30 };
var numbers = { 10, 20, 30 };
var a = 1, b = 2;
Consider the following code snippet:
var helper = new Object() {
int count = 5;
int multiply(int factor) { return count * factor; }
};
System.out.println(helper.multiply(4));
What is the outcome of compiling and running this code?
Compilation fails because var cannot be assigned an anonymous class instance
Compiles successfully and prints 20
Compilation fails because members of anonymous classes cannot be invoked outside their declaration
Throws a NoSuchMethodError at runtime
Consider the following code snippet:
var items = new ArrayList<>();
items.add("Java 17");
items.add(100);
String first = items.get(0);
System.out.println(first);
What is the result of attempting to compile and run this code?
Compilation fails at items.add(100); because items was inferred as ArrayList<String> from the first add call
Compiles, but throws a ClassCastException at runtime on items.add(100);
Compilation fails at String first = items.get(0); because items is inferred as ArrayList<Object>
Compiles and prints Java 17
Sections you finish are checked off in the contents.