In this article, we would take a deep dive at the new feature of Local-Variable Type Inference introduced in Java 10. We will go through the scope and limitations of using the local variable type inference.
This feature was proposed as part of JEP (JDK Enhancement Proposal): 286. The proposal was for enhancing the language to support the type inference to local variable declaration and initialization.
1. Java 10: Local Variable Type Inference
With Java 10, you can use var
for local variables instead of a typed name (Manifest Type). This is done by a new feature which is called Local Variable Type Inference.
But first, What is Type Inference?
Type inference is Java compiler’s ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable. Type Inference is not to Java programming.
For local variable declarations with initializer, we can now use a reserved type name “var” instead of a manifest type. Let’s look through a few examples.
var list = new ArrayList<String>(); // infers ArrayList<String>
var stream = list.stream(); // infers Stream<String>
Manifest Type: Explicit identification of type for each variable being declared is called as Manifest Typing. For example, If a variable “actors” is going to store a List of Actors, then its type List<Actor> is the manifest type and its must be declared (as mentioned below) prior to Java 10:
List<Actor> actors = List.of(new Actor()); // Pre Java 10
var actors = List.of(new Actor()); // Java 10 onwards
2. How does Local Variable Type Inference work?
Parsing a var statement, the compiler looks at the right-hand side of the declaration, aka initializer, and it infers the type from the right-hand side (RHS) expression.
Ok fine enough, does this mean that now Java is a dynamically typed language? Not really, it’s still a statically typed language. Let’s take a code snippet for reading a file.
private static void readFile() throws IOException {
var fileName = "Sample.txt";
var line = "";
var fileReader = new FileReader(fileName);
var bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
}
Now, let’s look at the decompiled code taken from IntelliJ IDEA decompiler.
private static void readFile() throws IOException {
String fileName = "Sample.txt";
String line = "";
FileReader fileReader = new FileReader(fileName);
BufferedReader bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
}
Here the compiler properly infers the type of the variable from the right-hand side expression and adds that to the bytecode.
3. var is a reserved type name
var is not a keyword, It’s a reserved type name. What does it mean?
- We can create a variable named “var”.
var var = 5; // syntactically correct // var is the name of the variable
- “var” as a method name is allowed.
public static void var() { // syntactically correct }
- “var” as a package name is allowed.
package var; // syntactically correct
- “var” cannot be used as the name of a class or interface.
class var{ } // Compile Error LocalTypeInference.java:45: error: 'var' not allowed here class var{ ^ as of release 10, 'var' is a restricted local variable type and cannot be used for type declarations 1 error interface var{ } // Compile Error
4. Local Variable Type Inference Usage Scenarios
Local type inference can be used only in the following scenarios:
- Limited only to Local Variable with initializer
- Indexes of enhanced for loop or indexes
- Local declared in for loop
Let’s walk through the examples for these scenarios:
var numbers = List.of(1, 2, 3, 4, 5); // inferred value ArrayList<String>
// Index of Enhanced For Loop
for (var number : numbers) {
System.out.println(number);
}
// Local variable declared in a loop
for (var i = 0; i < numbers.size(); i++) {
System.out.println(numbers.get(i));
}
5. Local Variable Type Inference Limitations
There are certain limitations of using var, let’s take a look at some of them.
- Cannot use ‘var’ on variables without initializer
If there’s no initializer then the compiler will not be able to infer the type.
var x; LocalTypeInference.java:37: error: cannot infer type for local variable x var x; ^ (cannot use 'var' on variable without initializer) 1 error
- Cannot be used for multiple variable definition
var x = 5, y = 10; LocalTypeInference.java:41: error: 'var' is not allowed in a compound declaration var x = 5, y = 10; ^ 1 error
- Null cannot be used as an initializer for var
Null is not a type and hence the compiler cannot infer the type of the RHS expression.
var author = null; // Null cannot be inferred to a type LocalTypeInference.java:47: error: cannot infer type for local variable author var author = null; ^ (variable initializer is 'null') 1 error
- Cannot have extra array dimension brackets
var actorArr[] = new Actor[10]; LocalTypeInference.java:52: error: 'var' is not allowed as an element type of an array var actorArr[] = new Actor[10]; ^ 1 error
- Poly expressions that have lambdas, method references, and array initializers, will trigger an error
For the type inference of Lambda expressions, Method inference and the Array initializers, compiler relies on the left hand side expression or the argument definition of the method where the expression is passed while var uses RHS, this would form a cyclic inference and hence the compiler generates a compile time error.
var min = (a, b) -> a < b ? a : b; LocalTypeInference.java:59: error: cannot infer type for local variable min var min = (a, b) -> a < b ? a : b; ^ (lambda expression needs an explicit target-type) 1 error var minimum = Math::min; LocalTypeInference.java:65: error: cannot infer type for local variable minimum var minimum = Math::min; ^ (method reference needs an explicit target-type) 1 error var nums = {1,2,3,4,5}; LocalTypeInference.java:71: error: cannot infer type for local variable nums var nums = {1,2,3,4,5}; ^ (array initializer needs an explicit target-type) 1 error
6. Generics with Local Variable Type Inference
Java has type inference for Generics and to top of it, it also has to do Type Erasure for any generics statement. There are some edge cases which should be understood when using local type reference with Generics.
Type Erasure: To implement generics, the Java compiler applies type erasure to, replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded.
Let’s go through some use case for var using generics:
var map1 = new HashMap(); // Inferred as HashMap
var map2 = new HashMap<>(); // Inferred as HashMap<Object, Object>
map1
– Compiler infers the map as HashMap without any generic type.
map2
– The diamond operator relies on the LHS for the type inference, where the compiler cannot infer the LHS and hence it infers map2 to have upper bound or supertype to which the HashMap can be denoted to. This leads to map2 being inferred as HashMap
7. Anonymous Class Types
Anonymous class types cannot be named, but they’re easily understood – they’re just classes. Allowing variables to have anonymous class types introduces useful shorthand for declaring a singleton instance of a local class. Let’s look at an example:
var runnable = new Runnable() {
@Override
public void run() {
var numbers = List.of(5, 4, 3, 2, 1);
for (var number : numbers) {
System.out.println(number);
}
}
};
runThread(runnable);
8. Non Denotable Types
An expression that cannot be inferred to a specific type is known as Non Denotable Type. Such type can occur for a capture variable type, intersection type, or anonymous class type. Let’s understand how a Non Denotable Type can be used for local variable type inference:
var map3 = new HashMap<>() { // anonymous class
int someVar;
};
Here, when the diamond operator is used with anonymous class type, the compiler cannot infer the RHS expression to any specific type. This leads to a formation of non-denotable Type.
Firstly, the compiler will get denotable type by using the supertype for HashMap<>, which is HashMap<Object, Object>.
Secondly, the anonymous class extension is applied. Finally, this becomes a Non-denotable type which gets assigned to map3.
A special case of Non-Denotable type which was not possible to create earlier in Java can now be created. Anonymously extending an Object class and adding attributes within it creates a POJO like a class which can be assigned to a variable to hold context. This can be very useful in using a dynamically created object which can have a structure within a temporary context. Let’s see an example:
// Special Case Non-Denotable Type
var person = new Object() {
class Name {
String firstName;
String lastName;
public Name(String firstName, String lastName) {
super();
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
}
Name name;
Actor actor;
public String displayName() {
return name.getFirstName() + " " + name.lastName;
}
};
person.name = person.new Name("Rakesh", "Kumar");
System.out.println(person.displayName());
9. Some Fun Facts for choosing var for Local Variable Type Inference
There was a survey for the list of keywords to choose from, for the local type inference. Below is the list of syntactic options provided to community users:
- var x = expr only (like C#)
- var, plus val for immutable locals (like Scala, Kotlin)
- var, plus let for immutable locals (like Swift)
- auto x = expr (like C++)
- const x = expr (already a reserved word)
- final x = expr (already a reserved word)
- let x = expr
- def x = expr (like Groovy)
- x := expr (like Go)
Results of the Survey:
Response of choices by percentage:
Rationale for using the 2nd best Choice (var)
- Even though var was the 2nd best choice, people were fine with it and almost no one hated it outright. Whereas this was not the case for the other options.
- C# experience. C# community had found the keyword to be reasonable for a Java-like language.
- Some readers found that var/val were so similar that they could mostly ignore the difference and it would be annoying to use the different keywords for immutable and mutable variables.
- Majority of local variables are effectively final and punishing immutability with another ceremony is not what the intent of the JEP was.
10. Benefits of Local Variable Type Inference
- It improves the developer experience
- It reduces code ceremony
- It reduces boiler plate code
- Increases code clarity
11. Conclusion
In this article we went through Local Type Inference and why var was chosen as a syntactic option. As usual you can check the complete code at github here.
var numbers = List.of(1, 2, 3, 4, 5) will be inferred as ArrayList
Thanks for explaining.
I like Java up to version 8, that is how I start to learn Java. This is becoming too much confused with those improvements (not this one, I mean on all those what it cames from version 9 > ).
Great & well elaborated article. Many thanks!!
Well written with depth coverage. Liked the way you covered different scenarios.
Very well written with examples. Thank you.
Great article , very well explained Rakesh !!!
Comprehensive and effortlessly understandable article. Thank you.