Recommended resources
The freely available resources this lesson will direct you to.
Good to see you again. In the previous lesson, you saw that Java distinguishes sharply between primitives and wrapper references, even when autoboxing makes them look similar. Local-variable type inference continues that theme: var can remove an explicit type from the source, but it never removes the type from Java’s compile-time model.
For a developer coming from JavaScript and Node.js, the most important correction is immediate: Java’s var is not JavaScript’s historical var. Java var was introduced in Java 10 and means “infer this local variable’s static type from its initializer.” It does not make the variable dynamically typed.
This lesson explains how that inference works, the contexts where it is legal, common generic and lambda pitfalls, and how to give a concise interview answer.
var is inferred static typing, not dynamic typing
Consider these two declarations:
String environment = "production";
var inferredEnvironment = "production";
They are equivalent in the sense that the compiler treats inferredEnvironment as a String. It is not a variable that can later become an int, List, or some arbitrary runtime value.
var environment = "production";
// environment = 42; // Compile-time error: int is not a String
environment = "staging"; // Valid
The compiler examines the initializer on the right-hand side, determines its compile-time type, and fixes that as the variable’s type for the remainder of its scope.
var retries = 3; // int
var timeout = 2_000L; // long
var enabled = true; // boolean
var endpoint = URI.create(url); // URI
This is why var preserves static typing:
- The compiler infers a type when it compiles the declaration.
- Every later assignment, method call, member access, and overload resolution is checked against that fixed type.
- The resulting bytecode and runtime behavior are not “dynamically typed Java.”
A useful interview phrasing is:
varis local-variable type inference, not dynamic typing. The compiler infers a static type from the initializer, and all later uses are checked exactly as if that inferred type had been written explicitly.
The following short reading establishes the core rule and the basic restrictions.
Using the Var Type Identifier - Dev.java
Read Oracle's Dev.java overview to establish the precise meaning of var: it reduces declaration boilerplate, while the compiler still chooses one fixed type.
Start in “The Var Keyword.” Read the central definition and notice the phrase “local variable.” Then move to “Examples with Var” and read the examples discussion. Finally, in “Restrictions on Using Var,” read the complete restriction list, focusing on why an initializer is required.
The initializer’s type, not its runtime object, controls inference
A subtle but important point: var infers the declared type of the expression, not necessarily the concrete class of the object created at runtime.
List<String> loadUsers() {
return new ArrayList<>();
}
var users = loadUsers(); // inferred type: List<String>
Although loadUsers() happens to create an ArrayList, its method signature promises only List<String>. Therefore users has the static type List<String>.
In contrast:
var users = new ArrayList<String>(); // inferred type: ArrayList<String>
Here the initializer expression itself has the more specific type ArrayList<String>.
That difference can matter during refactoring:
var users = new ArrayList<String>();
// users = List.of("Ada"); // Compile-time error: List<String> is not ArrayList<String>
If the variable’s role is “a list,” rather than specifically “an ArrayList,” an explicit abstraction may communicate the design better:
List<String> users = new ArrayList<>();
users = List.of("Ada"); // Valid: both values are List<String>
So var is not automatically preferable. It can accidentally expose an implementation type that you would otherwise hide behind an interface.
Where var is allowed
The word local is the boundary. var is permitted where a variable exists only inside executable code rather than being part of a class’s API.
| Context | Example | Valid? |
|---|---|---|
| Local variable in a method | var port = 8080; |
Yes |
| Local variable in a constructor | var normalized = input.trim(); |
Yes |
| Local variable in an initializer block | var config = loadConfig(); |
Yes |
Traditional for loop variable |
for (var i = 0; i < 10; i++) |
Yes |
Enhanced for loop variable |
for (var user : users) |
Yes |
| Try-with-resources variable | try (var stream = Files.newInputStream(path)) |
Yes |
| Field | private var cache = ...; |
No |
| Method parameter | void save(var user) |
No |
| Constructor parameter | User(var id) |
No |
| Method return type | public var find() |
No |
| Catch parameter | catch (var ex) |
No |
A practical backend example is try-with-resources:
try (var connection = dataSource.getConnection();
var statement = connection.prepareStatement(sql)) {
// Use connection and statement
}
This is valid because both resource variables are local to the try statement. The compiler still knows their types, such as Connection and PreparedStatement, and can verify that they are appropriate resources to close.
Enhanced for loops are also common:
for (var header : requestHeaders) {
log(header);
}
If requestHeaders is an Iterable<String>, then header is inferred as String.
The supplied image shows a similar use of var for a local ArrayList and the stream derived from it. The comments illustrate that the compiler has inferred specific types, not an untyped “var” value.

Why fields, parameters, and return types must stay explicit
Fields, parameters, and return types form part of a class’s declared interface. Other code may compile against them, subclasses may depend on them, and frameworks may inspect them through reflection. Java intentionally requires those types to be visible in source.
class UserService {
// private var repository = new UserRepository(); // Invalid
// public var findById(long id) { ... } // Invalid
// public User findById(var id) { ... } // Invalid
}
Allowing inference in those positions could make a small implementation change silently alter an API type. Java confines var to local code precisely to avoid that risk.
Watch the following focused segments from Stuart Marks’s Devoxx talk. They are particularly useful for interview language because they distinguish inference from dynamic typing and explain the API-boundary rationale for Java’s restrictions.
Var with Style: Local Variable Type Inference in Java 10 by Stuart Marks
Watch “Var with Style: Local Variable Type Inference in Java 10” by Stuart Marks on the Devoxx channel. It explains the feature’s design intent, legal locations, and why a var declaration remains statically typed.
Start with static typing for the distinction between Java var and dynamic languages. Then watch legal locations, especially the explanation of why fields, parameters, and return types are excluded. Continue with initializer rules for null and lambda failures, and finish with fixed inferred types to see reassignment and final var in action.
Inference requires enough information at the declaration
The compiler must be able to determine one useful type at the point of declaration. Java does not wait for future statements and infer a type from later assignments.
No initializer
This does not compile:
// var response;
There is no expression from which to infer a type. Write an explicit type if initialization must happen later:
HttpResponse<String> response;
// ... later
response = client.send(request, BodyHandlers.ofString());
null is not enough
This also does not compile:
// var response = null;
Although null is compatible with any reference type, it does not tell the compiler whether response should be a String, User, HttpResponse<String>, or another reference type.
Use an explicit declaration when null is genuinely the intended initial state:
User currentUser = null;
Or, preferably, restructure code so a variable is initialized with a meaningful value before use.
One variable per declaration
Java permits this with explicit types:
int start = 0, end = 10;
But it does not permit var in a multi-variable declaration:
// var start = 0, end = 10; // Invalid
Write separate declarations:
var start = 0;
var end = 10;
No bare array initializers
An array initializer needs a target type:
// var ports = {8080, 8081}; // Invalid
The compiler does not know whether you intend int[], Integer[], long[], or another array type. Supply an array-creation expression:
var ports = new int[] {8080, 8081}; // int[]
Lambdas and method references need a target functional interface
A lambda does not carry a single standalone type. It needs a target type, normally a functional interface, to establish its parameter types and return type.
// var task = () -> System.out.println("Refreshing cache"); // Invalid
The compiler cannot choose among all possible compatible functional interfaces. State the intended abstraction:
Runnable task = () -> System.out.println("Refreshing cache");
The same applies to method references:
// var logger = System.out::println; // Invalid
Consumer<String> logger = System.out::println;
This is a useful contrast with ordinary method calls. A method call generally has a declared return type, so it can provide inference information. A lambda and method reference rely on the surrounding context to obtain their type, and var removes that required context.
var, generics, and the diamond operator
Generic code is where var produces some of its most interview-worthy surprises. The key principle remains the same: infer from the initializer expression alone.
This is correct and clear:
var names = new ArrayList<String>(); // ArrayList<String>
names.add("Ada");
// names.add(42); // Compile-time error
But this is usually a mistake:
var names = new ArrayList<>(); // ArrayList<Object>
With no target type on the left and no constructor argument that reveals an element type, the diamond operator has insufficient information. It settles on Object.
names.add("Ada"); // Valid
names.add(42); // Also valid, probably unintended
Compare that with an explicit interface declaration:
List<String> names = new ArrayList<>(); // Diamond infers String from the left side
When var is present, the left side offers no generic constraint. Ensure the right side supplies one:
var names = new ArrayList<String>();
var initialNames = List.of("Ada", "Lin");
var copiedNames = new ArrayList<>(initialNames); // ArrayList<String>
var immutableNames = List.of("Ada", "Lin"); // List<String>
The final line works well because the arguments to List.of reveal the element type, while the method’s declared return type is List<String>.
A type can still be primitive
The previous lesson’s distinction between primitives and wrappers remains fully relevant:
var retryCount = 3; // int, not Integer
var optionalCount = 3L; // long, not Long
var threshold = 0.75; // double, not Double
Using var does not cause automatic boxing. Thus, var retryCount = 3 cannot be assigned null, whereas var retryCount = Integer.valueOf(3) infers Integer and can be null.
Use var for readability, not merely fewer characters
A good rule is: use var when the variable’s type is obvious from the initializer or unimportant to understanding the local logic. Keep an explicit type when the abstraction itself communicates an important design decision.
Usually clear
var request = HttpRequest.newBuilder(uri).GET().build();
var users = userRepository.findActiveUsers();
var response = client.send(request, BodyHandlers.ofString());
These declarations are readable because the factory method, variable name, and subsequent usage provide strong context.
Often clearer with an explicit abstraction
Map<String, List<Order>> ordersByCustomer = service.groupOrders();
If the fact that this value is a Map of customer IDs to lists of orders matters to the reader, the explicit type can be valuable documentation.
Likewise, an explicit interface can prevent accidental coupling to an implementation:
List<String> tags = new ArrayList<>();
rather than:
var tags = new ArrayList<String>(); // Specifically ArrayList<String>
Neither is universally right. The question is whether readers need to know the static type and whether you want the local variable tied to the concrete implementation.
var does not imply mutability or immutability
In JavaScript, developers often mentally connect declaration syntax to reassignment rules such as let and const. Java separates those concerns:
var status = "PENDING";
status = "APPROVED"; // Valid: still a String
Use final when the reference must not be reassigned:
final var requestId = UUID.randomUUID();
// requestId = UUID.randomUUID(); // Compile-time error
As with any final reference, this prevents reassignment; it does not automatically make the referenced object immutable.
Interview-ready questions and answers
What is var in Java?
var is a reserved type name introduced in Java 10 for local-variable type inference. The compiler infers the variable’s static type from its initializer expression. It is not a dynamically typed variable.
Does var make Java similar to JavaScript’s var?
Only superficially in spelling. Java’s var does not weaken compile-time type checking. After inference, the variable has one fixed static type and cannot later hold an incompatible value.
Is var a type?
No. It is syntax that asks the compiler to infer the real type. For example, in var port = 8080, the variable’s type is int, not var.
Where can var be used?
It can be used for local variables with initializers, including local variables in methods, constructors, and initializer blocks; traditional and enhanced for loop variables; and try-with-resources variables.
Where can var not be used?
It cannot be used for fields, method or constructor parameters, method return types, catch parameters, declarations without initializers, or declarations initialized directly with null. It also cannot infer the target type required by a bare lambda, method reference, or bare array initializer.
Why are fields and method signatures excluded?
They are part of a class’s visible contract and can affect source compatibility, binary compatibility, reflection, and API clarity. Java deliberately keeps those types explicit.
Can a var variable be reassigned?
Yes, as long as the new value is compatible with the inferred type.
var mode = "read";
mode = "write"; // Valid
// mode = 1; // Invalid
Use final var to prevent reassignment.
What is a common var and generics pitfall?
Using var with an empty diamond expression can infer Object:
var values = new ArrayList<>(); // ArrayList<Object>
If you need strings, provide the generic type or infer it from meaningful constructor arguments:
var values = new ArrayList<String>();
A reliable prediction routine
When you see a var declaration in an interview snippet:
- Identify the compile-time type of the initializer expression.
- Treat that as the variable’s permanently inferred static type.
- Check whether the initializer needs a target type, as lambdas and method references do.
- For generic factories and diamond syntax, verify where the type argument comes from.
- Ask whether the inferred concrete type is more specific than the intended abstraction.
Key takeaways
- Java
varperforms local-variable type inference and was introduced in Java 10. - It preserves Java’s static type system: the compiler determines one fixed type at declaration time.
varis valid for initialized local variables, loop variables, and try-with-resources variables.- It is invalid for fields, method and constructor parameters, return types, catch parameters, uninitialized variables, and direct
nullinitializers. - Lambdas, method references, and bare array initializers fail with
varbecause they need a target type. - With generics,
varinfers the initializer’s type exactly;new ArrayList<>()can therefore becomeArrayList<Object>. - Use
varwhen it improves local readability, but write explicit interface types when they express an important design decision.
This completes the Java runtime and language-semantics module. Next, the course moves into object-oriented type behavior, beginning with initialization order across inheritance hierarchies: static fields and blocks, instance fields and blocks, and constructors.