Java var with Generics: Type Inference Explained
java var with generics: Understand how Java's var keyword infers generic types, including diamond operator, method calls, and anonymous classes, with practical examples.
When you use var in Java, the compiler infers the type from the initializer. This works with generics, but the inferred type is not always what you might expect. For example, var list = new ArrayList<String>(); infers ArrayList<String>, but var list = new ArrayList<>(); infers ArrayList<Object> because the diamond operator cannot infer the type argument without a target type. This article explains how java var with generics behaves, where it works well, and where it can cause subtle issues.
How var Infers Generic Types
The var keyword, introduced in Java 10, performs local variable type inference. The compiler looks at the initializer expression and derives the variable's type from it. When the initializer is a generic type, the inferred type includes the full generic signature.
var strings = new ArrayList<String>(); var map = new HashMap<String, Integer>();
Here, strings is ArrayList<String> and map is HashMap<String, Integer>. This is straightforward because the explicit type arguments are present in the constructor call. The compiler uses the exact generic type from the expression, preserving type safety.
However, var does not change how generics are resolved. It only hides the explicit type declaration. The underlying type is exactly what the initializer produces. This means you cannot use var to bypass generic type checking—the compiler still enforces the inferred type at compile time.
var with the Diamond Operator
The diamond operator (<>) relies on target type inference to determine the type arguments. When you write new ArrayList<>(), the compiler looks at the assignment context to infer the type. With var, there is no assignment context—the initializer is the only source of information. As a result, the compiler falls back to the most general type: Object.
var list = new ArrayList<>(); // Inferred as ArrayList<Object>
This is a common pitfall. If you intend to create a List<String> but use the diamond operator with var, you end up with ArrayList<Object>, and any attempt to add a String will compile, but retrieving it requires a cast. The type safety you expect from generics is lost.
To avoid this, always provide explicit type arguments when using var with a generic constructor:
var list = new ArrayList<String>(); // Correct
The diamond operator works with var only when the type can be inferred from another part of the expression, such as a method call that returns a generic type. For example, var list = List.of("a", "b"); infers List<String> because the List.of method has a generic return type that is inferred from its arguments.
var with Generic Method Calls
When you call a generic method, the compiler infers the type arguments from the method arguments and the assignment context. With var, the assignment context is absent, so the compiler relies solely on the method arguments. This usually works well because the arguments provide enough information.
var list = Collections.emptyList(); // Inferred as List<Object>
Here, Collections.emptyList() is generic, but without a target type, the compiler infers Object. The result is List<Object>, which may not be what you want. To get List<String>, you need to provide a type witness or assign to a typed variable:
var list = Collections.<String>emptyList(); // Explicit type witness
This is less common because most generic methods infer from their arguments. For example, Arrays.asList("a", "b") infers List<String> because the arguments are strings. The problem arises only when the method has no arguments or when the type cannot be determined from the arguments alone.
Another example is Optional.empty(). With var, it infers Optional<Object>, which is rarely useful. You must specify the type explicitly:
var empty = Optional.<String>empty(); // Optional<String>
When using var with generic method calls, consider whether the inferred type is the one you need. If not, either provide a type witness or use an explicit type declaration.
var with Anonymous Classes and Generics
Anonymous classes can implement or extend generic types. When you use var with an anonymous class, the inferred type is the anonymous class itself, not the generic supertype. This can lead to unexpected behavior if you try to assign the result to a variable of the generic type.
var comparator = new Comparator<String>() { @Override public int compare(String a, String b) { return a.compareTo(b); } };
Here, comparator is not Comparator<String>; it is the anonymous class type. This is usually fine because you can call methods on it, but you cannot assign it to a Comparator<String> variable without a cast. More importantly, if the anonymous class adds methods that are not part of the generic interface, those methods are accessible through var, which can be useful but also couples your code to the implementation.
A more subtle issue arises when the anonymous class uses a generic type parameter that depends on the target type. For example:
var list = new ArrayList<String>() {}; // Anonymous subclass of ArrayList<String>
The inferred type is the anonymous subclass, not ArrayList<String>. This is generally safe, but it means the variable's type is not the generic base type. If you later need to pass it to a method expecting List<String>, it will work because the anonymous subclass is a subtype, but the static type is not exactly ArrayList<String>.
In practice, using var with anonymous classes is rare and often unnecessary. It can obscure the actual type and make the code harder to read. Prefer explicit types when dealing with anonymous classes that have complex generic relationships.
Limitations and Edge Cases of var with Generics
The most significant limitation is the loss of target type inference. Because var does not provide a target type, any generic expression that relies on the assignment context will infer Object or the most general type. This includes the diamond operator, generic methods with no arguments, and certain builder patterns.
Another limitation is that var cannot be used with method parameters, return types, or fields. It is only for local variables. This means you cannot use var to simplify generic method signatures or class fields.
Additionally, var does not support array initializers. You cannot write var arr = {1, 2, 3}; because the array initializer requires a target type. For generic arrays, this is even more restrictive.
Finally, var can cause issues with generic inference in lambda expressions. If a lambda's parameter types are inferred from a target type, using var in the lambda itself (as allowed in Java 11) does not affect the outer inference. But if you use var to declare a variable that holds a lambda, the inferred type is the functional interface type, which is usually correct.
When to Use var vs Explicit Types with Generics
The decision to use var with generics should be based on readability and maintainability. Use var when the generic type is obvious from the initializer and the explicit type would add noise. For example:
var list = new ArrayList<String>(); // Clear from the right side var map = Map.of("key", 1); // Map<String, Integer> is clear
Avoid var when the inferred type is not the one you want, or when the initializer is complex enough that the explicit type helps the reader. This includes cases where the diamond operator is used, where generic methods have no arguments, or where the type is not immediately obvious from the right side.
A good rule of thumb: if the explicit type would be longer than the initializer and the initializer clearly shows the generic type, var improves readability. If the type is ambiguous or the initializer is a method call with unclear return type, stick with explicit types.
Maintainability and Readability Considerations
Using var with generics can improve or harm maintainability depending on context. When the generic type is long, such as Map<String, List<Map<Integer, String>>>, var reduces clutter and makes the code easier to scan. However, it also hides the type from the reader, forcing them to look at the initializer to understand the variable's type.
In code reviews, var can make diffs harder to read because the type is not immediately visible. This is especially true when the initializer is a complex expression. For generic types, the explicit type often conveys more information than the expression itself.
Consider using var only when the initializer is a constructor call with explicit type arguments or a simple factory method like List.of. For anything else, weigh the benefit of brevity against the cost of reduced clarity. If your team values explicit types for maintainability, use var sparingly with generics.
var with Wildcard Types
Wildcard types, such as List<? extends Number>, interact with var in a specific way. When you use var with an expression that has a wildcard type, the inferred type preserves the wildcard. For example:
var numbers = getNumbers(); // Suppose getNumbers() returns List<? extends Number>
Here, numbers is List<? extends Number>. This is correct and maintains the wildcard constraint. However, you cannot add elements to numbers because the wildcard prevents modification. This is the same behavior as with an explicit type, so var does not change the semantics.
One subtlety is that var can hide the wildcard, making it less obvious that the list is read-only. If you later try to add an element, the compiler error might be confusing because the variable's type is not visible. In such cases, an explicit type can serve as a reminder of the wildcard restriction.
var with Generic Arrays and Varargs
Generic arrays are not allowed in Java, but you can have arrays of generic types through casting or via varargs. When using var with a method that returns a generic array, the inferred type is the array type, which may involve unchecked warnings.
var array = createArray(); // Suppose createArray() returns T[]
The inferred type depends on the method's return type. If the method returns String[], var infers String[]. If it returns T[] with a type parameter, the inference may be Object[] if the type cannot be determined. This is rare but can cause issues.
Varargs methods that return generic arrays are even more complex. For example, Arrays.asList(T... a) returns List<T>, and with var the type is inferred from the arguments. This works well because the arguments provide the type. But if you pass no arguments, you get List<Object>.
In general, var does not add new capabilities for generic arrays; it only hides the type. Use explicit types when dealing with arrays and generics to avoid ambiguity.
Compatibility and Compiler Behavior
var is available from Java 10 onward. If your codebase targets an older version, you cannot use it. When using var with generics, the compiler performs the same type inference as it would for an explicit type. There is no runtime cost; var is purely a compile-time feature.
One compiler behavior to note is that var cannot be used with lambda expressions without explicit target types. For example, var f = (String s) -> s.length(); is allowed because the lambda has an explicit parameter type. But var f = s -> s.length(); is not allowed because the lambda's parameter type cannot be inferred without a target type. This is a limitation of lambda inference, not var itself.
When using var with generic method references, the same rules apply. The method reference must have a compatible functional interface type, and var infers that type from the expression.
Practical Example: Building a Generic Collection
To see how var and generics work together in practice, consider a method that builds a map of lists:
public Map<String, List<Integer>> buildMap() { var result = new HashMap<String, List<Integer>>(); for (int i = 0; i < 10; i++) { result.computeIfAbsent("key" + i, k -> new ArrayList<>()); } return result; }
Here, var is used for the local variable result, and the type is HashMap<String, List<Integer>>. The diamond operator inside the lambda (new ArrayList<>()) is allowed because the target type is inferred from the computeIfAbsent method signature. This works because the lambda has a target type from the method call, not from var. The var declaration only affects the variable, not the lambda.
This example shows that var can be used safely with generics when the initializer provides explicit type arguments. The lambda's diamond operator works because the target type comes from the method's generic signature, not from var.
If you had written var list = new ArrayList<>(); in the lambda, it would fail because the lambda's return type is inferred from the target type, but the diamond operator inside the lambda still needs a target type. In this case, the target type is List<Integer> from the method's return type, so it works. But if you used var to declare the list outside a lambda, you would get ArrayList<Object>.
This distinction is important: var does not provide a target type for nested diamond operators. The target type must come from an explicit type, a method signature, or an assignment context. When using var, always ensure that any diamond operators in the initializer have a target type from another source.
var with Generic Builder Patterns
Builder patterns often use generics to enforce type safety. When you use var with a builder, the inferred type is the builder's type, which may include generic parameters. For example:
var builder = new GenericBuilder<String>().withValue("test");
If GenericBuilder<T> has a method withValue(T value) that returns GenericBuilder<T>, the inferred type is GenericBuilder<String>. This is correct and preserves the generic type. However, if the builder uses a fluent interface with self-referential generics, var can sometimes infer a type that is too narrow or too wide, depending on the method signatures.
Consider a builder that uses the CRTP pattern:
class Builder<T extends Builder<T>> { T self() { return (T) this; } }
Using var with such a builder can lead to complex inferred types. In most cases, the inference works, but it may be difficult to read. Explicit types are often clearer for builders with intricate generic hierarchies.
A safer approach is to use var only when the builder's generic type is obvious from the initializer, such as when the builder is created with an explicit type argument. Otherwise, stick with explicit types to avoid confusion.
Final Technical Consideration: Type Erasure and var
Type erasure removes generic type information at runtime. var does not affect erasure; it is a compile-time construct. The bytecode produced by var is identical to what you would get with an explicit type. This means there is no performance impact or runtime overhead.
However, because var hides the type, it can sometimes mask the effects of erasure. For example, if you use var to hold a generic list and later check list instanceof ArrayList<String>, the compiler will reject the check because generic types cannot be used with instanceof. This is a limitation of generics, not var. But with var, the error message might be less clear because the variable's type is not visible.
To avoid confusion, use explicit types when you need to perform runtime type checks or when the code relies on the exact generic type. var is best used when the type is obvious and no runtime type reflection is needed.
In summary, java var with generics works well when you provide explicit type arguments or use methods that infer types from their arguments. It breaks down when you rely on target type inference, such as with the diamond operator or no-argument generic methods. Understanding these boundaries helps you use var effectively without sacrificing type safety or readability.