3.5 Type Inference
Type inference allows the compiler to deduce types from context, reducing verbosity while maintaining complete type safety. Java's type system remains statically typed—inference happens at compile time, and every variable has a concrete type that never changes at runtime.
Local Variable Type Inference with var
Introduced in Java 10, var allows the compiler to infer the type of local variables from their initializers, eliminating redundant type declarations without sacrificing type safety.
The Basics
// Before Java 10 (redundant type specification)
String message = "Hello, world";
List<String> names = new ArrayList<String>();
Map<String, Integer> scores = new HashMap<String, Integer>();
// Java 10+ with var (type inferred from right-hand side)
var message = "Hello, world"; // String
var names = new ArrayList<String>(); // ArrayList<String>
var scores = new HashMap<String, Integer>(); // HashMap<String, Integer>
Key principle: var is not dynamic typing. The type is determined at compile time and cannot change:
var count = 10; // int
count = "text"; // COMPILE ERROR: incompatible types
Where var Can Be Used
| Context | Allowed | Example |
|---|---|---|
| Local variables with initializer | ✅ | var x = 10; |
| Enhanced for loop | ✅ | for (var item : items) |
| Try-with-resources | ✅ | try (var r = open()) |
| Lambda parameters (Java 11+) | ✅ | (var x, var y) -> x + y |
| Fields | ❌ | class C { var x = 10; } |
| Method parameters | ❌ | void m(var x) {} |
| Method return types | ❌ | var compute() { return 10; } |
| Without initializer | ❌ | var x; |
| Initialized to null | ❌ | var x = null; |
Good Uses of var
1. Factory methods make type obvious:
var users = List.of("alice", "bob"); // List<String>
var config = Map.of("env", "prod", "db", "pg"); // Map<String, String>
var path = Path.of("/data/file.txt"); // Path
var now = Instant.now(); // Instant
var random = new Random(); // Random
2. Constructors with explicit type arguments:
var cache = new ConcurrentHashMap<String, User>();
var executor = new ThreadPoolExecutor(
4, 8, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>()
);
3. Builder patterns:
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com"))
.header("Authorization", "Bearer " + token)
.GET()
.build(); // Type: HttpRequest
4. Long generic types:
// Instead of this monstrosity:
Map<String, List<Map<String, Object>>> complexData =
new HashMap<String, List<Map<String, Object>>>();
// Use var:
var complexData = new HashMap<String, List<Map<String, Object>>>();
5. Stream operations:
var activeUsers = users.stream()
.filter(User::isActive)
.collect(Collectors.toList()); // List<User>
var userMap = users.stream()
.collect(Collectors.toMap(User::id, User::name)); // Map<Long, String>
Bad Uses of var (Avoid These)
1. Ambiguous return types:
// BAD: What does compute() return? String? Integer? Optional<T>?
var result = compute();
// GOOD: Explicit type when method name doesn't clarify
ComputeResult result = compute();
2. Primitive literals without context:
// BAD: Is it int? long? Intended range unclear
var timeout = 30;
// GOOD: Explicit type documents intent
int timeoutSeconds = 30;
// OR use type suffix for clarity
var timeoutMs = 30_000L; // Clearly long
var ratio = 0.5; // Clearly double
3. Complex expressions:
// BAD: Type depends on complex conditional logic
var value = condition ? processA() : processB();
// GOOD: Explicit common return type
Result value = condition ? processA() : processB();
4. Public APIs:
// NEVER use var in:
public class UserService {
// BAD: Field
var cache = new HashMap<String, User>();
// BAD: Method parameter
public void update(var user) { }
// BAD: Return type
public var getUser(String id) { }
}
Detailed Inference Rules
1. Infers concrete type, not interface:
var list = new ArrayList<String>(); // Type: ArrayList<String> (not List<String>)
// To declare as interface, be explicit:
List<String> list = new ArrayList<>();
2. Infers from diamond operator:
var map = new HashMap<>(); // ERROR: Cannot infer type arguments
// Need type arguments somewhere:
var map = new HashMap<String, Integer>(); // HashMap<String, Integer>
// Or infer from add operation:
var set = new HashSet<>(); // ERROR: raw type
set.add("text"); // Too late, already raw type
// Must specify:
var set = new HashSet<String>();
3. Null requires explicit type:
var x = null; // COMPILE ERROR
// Must use explicit type:
String x = null;
// Or use ternary with non-null alternative:
var x = condition ? "value" : null; // Infers String
4. Array initialization:
var array = new int[] {1, 2, 3}; // int[]
var strings = new String[] {"a", "b"}; // String[]
// Cannot use array initializer without new:
var bad = {1, 2, 3}; // COMPILE ERROR
5. Method references and lambdas require target type:
// ERROR: Cannot infer without context
var func = String::toUpperCase;
// Need explicit functional interface type:
Function<String, String> func = String::toUpperCase;
// Or provide in var with explicit cast:
var func = (Function<String, String>) String::toUpperCase;
var in Enhanced For Loops
// Works with any Iterable
var items = List.of("apple", "banana", "cherry");
for (var item : items) { // item inferred as String
System.out.println(item.toUpperCase());
}
// Works with arrays
var numbers = new int[] {1, 2, 3, 4, 5};
for (var num : numbers) { // num inferred as int
System.out.println(num * 2);
}
// Works with Map entries
var scores = Map.of("Alice", 90, "Bob", 85);
for (var entry : scores.entrySet()) { // Map.Entry<String, Integer>
System.out.println(entry.getKey() + ": " + entry.getValue());
}
var in Try-with-Resources
Path path = Path.of("/data/input.txt");
try (var reader = Files.newBufferedReader(path)) { // BufferedReader
var line = reader.readLine(); // String
while (line != null) {
System.out.println(line);
line = reader.readLine();
}
}
// Multiple resources
try (var in = new FileInputStream("input.txt"); // FileInputStream
var out = new FileOutputStream("output.txt")) { // FileOutputStream
in.transferTo(out);
}
var with Lambda Parameters (Java 11+)
// Allows annotations on lambda parameters
BiFunction<String, String, String> concat = (var a, var b) -> a + b;
// Useful for annotations
(var a, @NonNull var b) -> a + b
// All parameters must use var (cannot mix)
(var a, String b) -> a + b // COMPILE ERROR
(var a, var b) -> a + b // OK
Generic Type Inference
Java's compiler can infer generic type arguments in several contexts, reducing boilerplate.
Diamond Operator (Java 7+)
The diamond operator <> tells the compiler to infer type arguments from the left-hand side:
// Before Java 7
List<String> names = new ArrayList<String>();
Map<Integer, List<String>> data = new HashMap<Integer, List<String>>();
// Java 7+ with diamond
List<String> names = new ArrayList<>();
Map<Integer, List<String>> data = new HashMap<>();
// Combined with var (Java 10+)
var names = new ArrayList<String>(); // Type arguments on right
var data = new HashMap<Integer, List<String>>();
When diamond operator fails:
// Cannot infer from nothing
var map = new HashMap<>(); // ERROR: raw type
// Need type arguments:
var map = new HashMap<String, Integer>(); // OK
// Or explicit left-hand side:
Map<String, Integer> map = new HashMap<>(); // OK
Generic Method Inference
Generic methods can infer type arguments from the actual arguments:
public static <T> List<T> listOf(T... elements) {
return Arrays.asList(elements);
}
// Explicit type argument
List<String> names = UserUtils.<String>listOf("Alice", "Bob");
// Inferred from arguments
List<String> names = UserUtils.listOf("Alice", "Bob"); // Infers <String>
// Using var
var names = UserUtils.listOf("Alice", "Bob"); // List<String>
Complex inference:
public static <T, R> List<R> map(List<T> input, Function<T, R> mapper) {
return input.stream().map(mapper).toList();
}
List<Integer> lengths = map(
List.of("a", "bb", "ccc"), // T inferred as String
String::length // R inferred as Integer
);
Target-Type Inference (Java 8+)
Type arguments can be inferred from the target type (left-hand side or parameter type):
// Lambda inference from interface
Comparator<String> byLength = (a, b) -> a.length() - b.length();
// Parameters a and b inferred as String
// Method reference inference
Function<String, Integer> toLength = String::length;
// Infers String → Integer
// Stream collectors
Map<String, Long> counts = users.stream()
.collect(Collectors.groupingBy(
User::role, // Key type inferred: String
Collectors.counting() // Value type inferred: Long
));
Generic Constructor Inference (Java 9+)
// Diamond with anonymous classes (Java 9+)
var comparator = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
}; // Infers Comparator<String>
Common Pitfalls and Edge Cases
Pitfall 1: Inference Widens Type
var list = new ArrayList<String>(); // ArrayList<String>, not List<String>
// Problem: exposes implementation
public var getUsers() { // ILLEGAL, but illustrates the point
return new ArrayList<String>(); // Returns ArrayList, not List
}
// Solution: Explicit interface type when needed
List<String> list = new ArrayList<>(); // List<String>
Pitfall 2: Loss of Compile-Time Checking
// With explicit type
List<String> items = getItems(); // Compile error if getItems() changes return type
// With var
var items = getItems(); // Silent change if getItems() return type changes
Pitfall 3: Raw Types with var
// Accidental raw type
var list = new ArrayList(); // Raw type ArrayList (no generics)
list.add("text"); // Unchecked warning
list.add(123); // No compile error! Type safety lost
// Correct:
var list = new ArrayList<String>();
Pitfall 4: var Doesn't Change Type
var count = 10; // int
count = 10L; // COMPILE ERROR: incompatible types (int vs long)
// If you need flexible numeric type, be explicit
Number count = 10; // OK
count = 10L; // OK (Long is a Number)
Pitfall 5: Confusing var with val
var config = Map.of("key", "value"); // var is mutable reference
config = Map.of("new", "map"); // OK: reassignment allowed
// config itself is mutable, but Map.of() returns immutable map
config.put("key2", "value2"); // RUNTIME ERROR: UnsupportedOperationException
Java doesn't have val (immutable reference) like Kotlin/Scala. Use final for that:
final var config = Map.of("key", "value");
config = Map.of("new", "map"); // COMPILE ERROR: cannot assign to final
Best Practices for Type Inference
- Use
varwhen type is obvious from right-hand side (factory methods, constructors with type args) - Avoid
varwhen return type is ambiguous (prefer explicit type) - Use descriptive variable names when using
var(name should clarify intent) - Combine
varwithfinalfor immutable references:final var config = ... - Never use in public APIs (fields, parameters, return types)
- Prefer interface types explicitly when you need polymorphism:
List<String> list = new ArrayList<>() - Use diamond operator with explicit left-hand side or var with type arguments on right
- Watch for raw types when using diamond with var
Performance Impact
Type inference has zero runtime cost:
- All inference happens at compile time
- Compiled bytecode is identical to explicit types
- No runtime type checks or reflection
// These produce identical bytecode:
String message = "Hello";
var message = "Hello";
Comparison with Other Languages
| Language | Local Inference | Example |
|---|---|---|
| Java | var (Java 10+) |
var x = 10; |
| C# | var (C# 3.0+) |
var x = 10; |
| C++ | auto (C++11+) |
auto x = 10; |
| Kotlin | val/var |
val x = 10 (immutable), var y = 10 (mutable) |
| Scala | val/var |
val x = 10, var y = 10 |
| TypeScript | let/const |
let x = 10, const y = 10 |
Java's var is most similar to C#'s var (both require initializers, both are mutable references).
Conclusion
Type inference in Java is a readability optimization, not a dynamic typing feature:
- Reduces boilerplate without sacrificing type safety
- Maintains complete compile-time type checking
- Has zero runtime performance cost
- Best used when type is obvious from context
Use var judiciously: when it improves readability by removing redundancy, not when it obscures the type. Let the compiler work for you, but don't hide intent from human readers.
Next section: Enhanced Switch Expressions—diving deep into exhaustiveness, pattern matching, and guarded cases.