Java is often criticized as being too verbose. One aspect contributing to this characterization is the requirement to specify every type explicitly, which leads to a lot of additional noise.

A new way of declaring local variables with less clutter was given to us with JDK 10 and JEP 286: local variable type inference.


General Concept

The name itself perfectly describes the feature’s core and its limitations:

Local variable

  • Only local variables are supported
  • No methods parameters
  • No return types
  • No fields
  • No lambdas

Type inference

The Java compiler will automatically detect the correct type for us.

Actually, this isn’t a completely new feature for the JDK. Inferred types were already supported by the diamond operator <> (JDK 7). Lambdas can infer their argument types (JDK 8), too:

1
2
3
List<String> data = new ArrayList<>();

BinaryOperator<Integer> sumFn = (a, b) -> a + b;

This doesn’t mean Java suddenly became dynamically typed. All types will be inferred at compile time, not runtime, providing us with the same safety as before.

How to use var

The basic idea is simple: When we initialize a local variable, we can replace the explicitly stated left-hand type with the newly introduced reversed-type name var instead.

All the information for the inferred type must be provided by the initializer (e.g., constructor, literal, method return value):

1
2
3
4
5
6
7
// WITHOUT INFERENCE
// {type} {variableName} = {initializer};
Customer customer = new Customer();

// WITH INFERENCE
// var {variableName} = {initializer};
var customer = new Customer();

With var not being a keyword, we can also use final to make the variable not reassignable.

Some restrictions may apply

The type must be inferable at the initialization of a variable, so null isn’t allowed:

1
2
3
var customer = null; // ERROR

customer = new Customer();

Lambdas can also not be represented with var, at least not without explicit casting. Due to lambdas being represented by concrete functional interfaces behind the scenes, the type can’t be inferred without additional context:

1
2
3
4
5
// NO EXPLICIT TYPE CAN BE INFERRED
var sumFn = (Integer a, Integer b) -> a + b; // ERROR

// COMPILES, BUT ISN'T AN IMPROVEMENT
var sumFn = (BinaryOperator<Integer>) (a, b) -> a + b;

By casting it to an explicit interface, we might be able to make the compiler happy. But this abomination isn’t in the intended spirit of var.


Reading Vs. Writing Code

Our code will be read way more often than it’ll be written.

While writing code, all the context is still right there. Whenever someone reads our code, even ourselves, we need to be able to reason with it.

“Programs are meant to be read by humans and only incidentally for computers to execute.”
 — Donald Knuth

The verbosity of Java can be a mental burden by confronting us with more code than is actually needed to comprehend it. var can help to reduce this additional, often redundant information. But not all code removed by it might be redundant. It might remove any indicators of the bigger context.

To reason with code means understanding its context and impact. If we remove the explicit type information, we need to make sure the context can be deducted in other ways. Just because the compiler might infer the correct types doesn’t automatically mean we, as humans, can do it.

We should always be able to comprehend code in its local scope without knowing the complete bigger picture surrounding it. This is why not every local variable declaration should be using var.


Explicit Context

Excellent use cases for var are constructs already containing explicit context: constructors, literals, and static factory methods.

Constructors

Constructors are made up of their type so not much more information is possible:

1
2
3
4
5
var customer = new Customer("John Doe");

var vatTax = new BigDecimal("0.19");

var joiner = new StringJoiner(" ");

No additional context needed.

Literals

Literals can provide all the context needed to infer the correct type if we comply with their special notations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// String => double-quoted
var message = "Hello, World!";

// char => single-quoted
var bullet = '\u2022';

// int => whole number
var dozen = 12;

// long => ends with "L/l"
var kb = 1_024L;

// double => contains decimal point, optional "d/D"
var tax = 0.19; // double
var reducedTax = 0.07d; // double

// float => contains decimal point and ends with "f/F"
var threeQuarter = 0.75f

If we don’t comply, literals could be inferred as another type. In practice, they still might work due to implicit casting. But the actual type would be wrong:

1
2
3
4
5
6
7
long kb = 1_024;
// vs.
var kb = 1_024; // Inferred as int

float threeQuarter = 0.75;
// vs.
var threeQuarter = 0.75; // Inferred as double

The literals byte and short don’t have special indicators, so they always will be inferred to int instead.

Static factory methods

Many types contain static factory methods, providing just as much information as a constructor, either by the class name or the factory-method name:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// List<String>
var names = List.of("Albattani",
                    "Bell",
                    "Boyd",
                    "Gauss");

// Long
var kb = Long.valueOf("1024");

// BufferedReader
var reader = Files.newBufferedReader(...);

Implicit Context

Another aspect of var is the ability to replace unnecessary information.

Intermediate values

Local variables are an easy and cheap way of storing intermediate values in a narrow scope. For understanding the context, the actual type these variables are might not be as crucial as its name and its surroundings:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Customer customer = new Customer("John Doe");

Map<String, List<Order>> result = loadOrdersByCategory(customer);

for (Map.Entry<String, List<Order>> entry : result.entrySet()) {

    List<Order> orders = entry.getValue();
    
    // ...
}

By using better variable names, we can rely on var and still grasp the context:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var customer = new Customer("John Doe");

var ordersByCategoryMap = loadOrdersByCategory(customer);

for (var entry : ordersByCategoryMap.entrySet()) {

    var orders = entry.getValue();

    // ...
}

Loops

As seen before, loops can be simplified with var. Usually the surrounding context provides enough information, so we don’t need explicit types anymore:

1
2
3
4
5
6
7
8
9
// List<String>
var names = List.of("Albattani",
                    "Bell",
                    "Boyd",
                    "Gauss");

for (var name : names) {
    // ...
}

try-with-resources

A try-with-resources block is a very verbose construct. But thanks to var, we can make them more concise:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// WITHOUT TYPE INFERENCE
try (FileReader fileReader = new FileReader(...);
     BufferedReader bufferedReader = new BufferedReader(fileReader)) {

    // ...
}

// WITH TYPE INFERENCE
try (var fileReader = new FileReader(...);
     var bufferedReader = new BufferedReader(fileReader)) {

    // ...
}

Generics

Generic type declarations, especially, can be a mouthful. A simple Iterator can be as complicated as this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void removeIfLonger(Map<? extends String, ? extends String> map, int maxLength) {

    for (Iterator<? extends Map.Entry<? extends String, ? extends String>> iter = map.entrySet().iterator(); iter.hasNext();) {
  
        Map.Entry<? extends String, ? extends String> entry = iter.next();
        if (entry.getValue().length() > maxLength) {
            iter.remove();
        }
    }
}

By knowing what type the Map is, we don’t need to bother with the explicit types of the iterator or the entry:

1
2
3
4
5
6
7
8
9
void removeIfLonger(Map<? extends String, ? extends String> map, int maxLength) {

    for (var iter = map.entrySet().iterator(); iter.hasNext();) {
        var entry = iter.next();
        if (entry.getValue().length() > maxLength) {
            iter.remove();
        }
    }
}

This is much easier on the eyes and still as understandable as before.


Caveats

Besides overusing var and destroying valuable bits of information, there are multiple caveats to be aware of.

Diamond operator

As mentioned before, the diamond operator <> already provides us with type inference. The compiler infers the right-hand type based on the left-hand declaration:

1
2
// No additional type required for ArrayList
List<String> values = new ArrayList<>();

With var, we no longer have this information and must provide it ourselves in the initializer:

1
2
// ArrayList<String>
var values = new ArrayList<String>();

That doesn’t mean we can’t use the diamond operator at all, though. If the initializer provides enough information due to other circumstances, the compiler can infer the correct type:

1
2
3
4
Comparator<String> comparator = (lhs, rhs) -> ...;

// PriorityQueue<String>
var queue = new PriorityQueue<>(comparator);

Interface vs. concrete types

Usually we code against interfaces and not concrete implementations:

1
List<String> values = new ArrayList<>();

This abstraction provides us with a lot of flexibility for future changes. But with type inference we only get the type of the initializer, not its interface:

1
2
// ArrayList<String>
var values = new ArrayList<String>();

By being only available for local variables, this shouldn’t impose much of a problem. We still should code against abstractions for public interfaces. But in a local scope, it doesn’t matter as much.

Literals

As mentioned before, literals need additional context via type-specific indicators to be correctly inferred:

 TYPE   | INDICATOR
------- | ------------------------------------
 String | double-quoted
 char   | single-quoted
 int    | whole number
 long   | whole number ending with "L/l"
 float  | decimal number ending with "f/F"
 double | decimal number, optional "d/D"
 byte   | no indicator, always inferred to int
 short  | no indicator, always inferred to int

Conclusion

Better type inference is a great addition to Java. Static compile-time type safety paired with less typing and more concise code is a win-win.

Omitting the explicit type reduces clutter, as long as we have other bits of information to deduce the context.

But there’s more to it than just replacing an existing type declaration with var indiscriminately. Code must be reasoned with thanks to its surrounding context.

We can improve this context by choosing better variable names and narrowing the scope of intermediate values. If understandability is still impaired by using type inference, it can be an indicator for deeper structural problems, and var might not be the best way to go.

Resources