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<>()) infers ArrayList<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.

Last updated: October 2026

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:

  1. Local variables with an immediate initializer:
    var message = "Hello, Java 17!"; // Inferred as java.lang.String
    var count = 42;                  // Inferred as int
    
  2. Enhanced for loops (for-each):
    List<String> names = List.of("Alice", "Bob");
    for (var name : names) {         // Inferred as String
        System.out.println(name.toUpperCase());
    }
    
  3. Traditional index-based for loops:
    for (var i = 0; i < 10; i++) {   // Inferred as int
        System.out.print(i);
    }
    
  4. Try-with-resources variable declarations:
    try (var fis = new FileInputStream("app.log")) { // Inferred as FileInputStream
        int data = fis.read();
    }
    
  5. Lambda formal parameters (Java 11+): When using var in lambda parameter lists, all parameters must use var. 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 ContextCode ExampleReason for Rejection
Instance / Static Fieldsclass Car { var speed = 50; }Class fields define public or internal API state; inference would complicate compilation units and circular dependencies.
Method Parametersvoid drive(var miles) {}Method parameter types define class contracts and overload signatures.
Method Return Typesvar calculate() { return 10; }Return types define public contracts; inference would cause circular call graph dependency issues.
Constructor Parameterspublic Car(var model) {}Constructor signatures must be explicitly typed.
Catch Block Parameterscatch (var ex) {}Exception parameter types must explicitly list the catchable hierarchy or union types (IOException | SQLException).
Uninitialized Variablesvar total; total = 5;The compiler cannot infer a type without an immediate initializer expression on the declaration line.
Explicit null Initializervar item = null;The literal null is compatible with any reference type; the compiler cannot resolve which specific type to infer.
Compound Declarationsvar x = 1, y = 2;var cannot be used in multi-variable declarations, even if all initializers share the same type.
Array Shorthandvar nums = { 1, 2, 3 };Array initializer { ... } requires an explicit target array type (new int[]{ 1, 2, 3 } is required).
Bare Lambdas / Method Referencesvar 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)
    }
}
Test Your Knowledge

Which of the following local variable declarations compiles without any compiler errors?

A

var action = (String s) -> s.toUpperCase();

B

var values = new int[] { 10, 20, 30 };

C

var numbers = { 10, 20, 30 };

D

var a = 1, b = 2;

Test Your Knowledge

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?

A

Compilation fails because var cannot be assigned an anonymous class instance

B

Compiles successfully and prints 20

C

Compilation fails because members of anonymous classes cannot be invoked outside their declaration

D

Throws a NoSuchMethodError at runtime

Test Your Knowledge

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?

A

Compilation fails at items.add(100); because items was inferred as ArrayList<String> from the first add call

B

Compiles, but throws a ClassCastException at runtime on items.add(100);

C

Compilation fails at String first = items.get(0); because items is inferred as ArrayList<Object>

D

Compiles and prints Java 17

Sections you finish are checked off in the contents.