When Java 8 introduced Streams and Lambdas it was a big change, enabling functional programming style to be expressed with much less boilerplate. While recent versions did not add such impactful features, lots of smaller improvements were made to the language.

This post summarizes language enhancements included in Java versions released after Java 8. For an overview of all the JEPs shaping the new platform check this ​​post​​.

Local-Variable Type Inference

Probably the most significant language improvement since Java 8 is the addition of the ​​var​​​keyword. It was initially introduced in ​​Java 10​​​, and was further improved in ​​Java 11​​.

This feature allows us to reduce the ceremony of a local variable declaration by omitting the explicit type specification:

var greetingMessage = "Hello!";

While it looks similar to Javascript’s ​​var​​ keyword, this is not about dynamic typing.

Take this quote from the JEP:

We seek to improve the developer experience by reducing the ceremony associated with writing Java code, while maintaining Java’s commitment to static type safety.

The type of the declared variables is inferred at compile time. In the example above the inferred type is String. Using ​​var​​ instead of an explicit type makes this piece of code less redundant, thus, easier to read.

Here’s another good candidate for type inference:

MyAwesomeClass awesome = new MyAwesomeClass();

It’s clear that in many cases this feature can improve code quality. However, sometimes it’s better to stick with the explicit type declaration. Let’s see a few examples where replacing a type declaration with ​​var​​ can backfire.

Keep readability in mind

The first case is when removing explicit type information from the source code makes it less readable.

Of course, IDEs can help in this regard, but during code-reviews or when you just quickly scanning the code it might hurt readability. For example, consider factories or builders: you have to find the code responsible for object initialization to determine the type.

Here’s a little puzzle. The following piece of code is using Java 8’s Date/Time API. Guess the types of the variables in the following snippet:

var date = LocalDate.parse("2019-08-13");
var dayOfWeek = date.getDayOfWeek();
var dayOfMonth = date.getDayOfMonth();

Done? Here’s the solution:

The first one is pretty intuitive, the ​​parse​​​ method returns a ​​LocalDate​​​ object. However, for the next two, you should be a little bit more familiar with the API: ​​dayOfWeek​​​ returns a ​​java.time.DayOfWeek​​​, while ​​dayOfMonth​​​ simply returns an ​​int​​.

Another potential problem is that with ​​var​​ the reader has to rely more on the context. Consider the following:

private void horriblyLongMethod() {
// ...
// ...
// ...

var dayOfWeek = date.getDayOfWeek();

// ...
// ...
// ...
}

Based on the previous example, I bet you’d guess it’s a ​​java.time.DayOfWeek​​​. But this time, it’s an integer, because the ​​date​​​ in this example is from Joda time. It’s a different API, behaving slightly differently, but you can’t see it because it’s a longer method, and you did not read all the lines. (JavaDoc: ​​Joda time​​​ / ​​Java 8 Date/Time API​​)

If the explicit type declaration was present, figuring out what type ​​dayOfWeek​​​ has would be trivial. Now, with ​​var​​​, the reader first has to find out the type of the ​​date​​​ variable and check what ​​getDayOfWeek​​ does. This is simple with an IDE, not so simple when just scanning the code.

Pay attention to preserve important type information

The second case is when using ​​var​​​ removes all available type information, so it can not be even inferred. In most cases, these situations are caught by the Java compiler. For example, ​​var​​ cannot infer type for lambdas or method references, because for these features the compiler relies on the left-hand side expression to figure out the types.

However, there are a few exceptions. For example, ​​var​​ does not play nicely with the Diamond Operator. The Diamond operator is a nice feature to remove some verbosity from the right-hand side of an expression when creating a generic instance:

Map<String, String> myMap = new HashMap<String, String>(); // Pre Java 7
Map<String, String> myMap = new HashMap<>(); // Using Diamond operator

Because it only deals with the generic types, there is still redundancy to be removed. Let’s try to make it terser with ​​var​​:

var myMap = new HashMap<>();

This example is valid, and Java 11 it does not even emit in compiler warnings about it. However, with all these type inference we ended up not specifying the generic types at all, and the type will be ​​Map<Object, Object>​​.

Of course, this can be solved easily by removing the Diamond Operator:

var myMap = new HashMap<String, String>();

Another set of problems can arise when ​​var​​ is used with primitive data types:

byte   b = 1;
short s = 1;
int i = 1;
long l = 1;
float f = 1;
double d = 1;

Without explicit type declaration, the type of all these variables would be inferred to ​​int​​​. Use type literals (e.g. ​​1L​​​) when working with primitive data types, or don’t use ​​var​​ in this case at all.

Make sure to read the official style guides

It’s ultimately up to you to decide when to use type inference and make sure that it does not hurt readability and correctness. As a rule of thumb, sticking to good programming practices, such as good naming and minimizing the scope of local variables certainly helps a lot. Make sure to read the official ​​style guide​​​ and ​​FAQ​​​ about ​​var​​.

Because ​​var​​ has so many gotchas, it’s great that it was introduced conservatively and can only be used on local variables, which scope is usually pretty limited.

Also, it has been introduced cautiously, ​var​​ is not a new keyword but a reserved type name. This means that it only has special meaning when it’s used as a type name, everywhere else ​​var​​ is continuing to be a valid identifier.

Currently, ​​var​​​ does not have an immutable counterpart (such as ​​val​​​ or ​​const​​​) to declare a final variable and infer its type with a single keyword. Hopefully, we’ll get it in a future release, until then, we can resort to ​​final var​​.

Resources:

Various improvements from Milling Project Coin

Project Coin (​​JSR 334​​) was part of JDK 7, and brought a couple of handy language improvements:

  • Diamond operator
  • Try-with-resources statement
  • Multi-catch and more precise rethrow
  • Strings in switch statements
  • Binary integral literals and underscores in numeric literals
  • Simplified Varargs Method Invocation

​Java 9​​ continued on this path, and added a couple of further small improvements.

Allow private methods in interfaces

Since Java 8 it is possible to add default methods to interfaces. With Java 9 these default methods can even call private methods to share code in case you are in a need for reuse, but do not want to expose functionality publicly.

Although it’s not a huge deal, it’s a logical addition that allows to tidy up code in default methods.

Diamond operator for anonymous inner classes

Java 7 introduced the Diamond Operator (​​<>​​) to reduce verbosity by letting the compiler infer the parameter types for constructors:

List<Integer> numbers = new ArrayList<>();

However, this feature did not work with anonymous inner classes before. According to the ​​discussion on the project’s mailing list​​ this was not added as part of the original Diamond Operator feature, because it required a substantial JVM change.

With Java 9, this small rough edge is removed, making the operator more universally applicable:

List<Integer> numbers = new ArrayList<>() {
// ...
}

Allow effectively-final variables to be used as resources in the try-with-resources statement

Another enhancement introduced by Java 7 is the ​​try-with-resources​​, which frees the developer from having to worry about releasing resources.

To illustrate its power, first consider the effort made to properly close a resource in this typical pre-Java 7 example:

BufferedReader br = new BufferedReader(...);
try {
return br.readLine();
} finally {
if (br != null) {
br.close();
}
}

With ​​try-with-resources​​ resources can be automatically released, with much less ceremony:

try (BufferedReader br = new BufferedReader(...)) {
return br.readLine();
}

Despite its power, ​​try-with-resources​​ had a few shortcomings that Java 9 addressed.

Although this construct can handle multiple resources, it can easily make the code harder to read. Declaring variables like this in a list after the ​​try​​ keyword is a bit unconventional compared to the usual Java code:

try (BufferedReader br1 = new BufferedReader(...);
BufferedReader br2 = new BufferedReader(...)) {
System.out.println(br1.readLine() + br2.readLine());
}

Also, in the Java 7 version, if you already have a variable that you want to handle with this construct, you had to introduce a dummy variable. (For an example, see ​​JDK-8068948​​.)

To mitigate these criticisms, ​​try-with-resources​​ was enhanced to handle final or effectively final local variables in addition to newly created ones:

BufferedReader br1 = new BufferedReader(...);
BufferedReader br2 = new BufferedReader(...);
try (br1; br2) {
System.out.println(br1.readLine() + br2.readLine());
}

In this example, the initialization of the variables is separated from their registration to the ​​try-with-resources​​ construct.

One caveat to keep in mind is that now it’s possible to reference variables that are already released by ​​try-with-resources​​, which, in most cases will fail:

BufferedReader br = new BufferedReader(...);
try (br) {
System.out.println(br.readLine());
}
br.readLine(); // Boom!

Underscore is no longer a valid identifier name.

​In Java 8​​, the compiler emits a warning when ‘_’ is used as an identifier. Java 9 took this a step further making the sole underscore character illegal as an identifier, reserving this name to have special semantics in the future:

int _ = 10; // Compile error

Improved Warnings

Finally, let’s say a word about the changes related to the compiler warnings in newer Java versions.

Now it’s possible to annotate a private method with ​​@SafeVarargs​​​ to mark a ​​Type safety: Potential heap pollution via varargs parameter​​​ warning false positive. (In fact, this change is part of the previously discussed ​​JEP 213: Milling Project Coin​​​). Read more about ​​Varargs​​​, ​​Generics​​​ and ​​the potential probems​​ that can arise by combining these features in the official documentation.

Also since ​​Java 9​​, the compiler does not issue a warning for import statements when a deprecated type is imported. These warnings were uninformative and redundant since a separate warning is always displayed at the actual usage of the deprecated members.

Summary

This post covered the improvements related to the Java language since Java 8. It’s important to keep an eye on the Java platform, as with the new rapid release cadence a new Java version is released every six months, delivering changes to the platform and to the language.

 

Dávid Csákvári 

Dávid is a full-stack developer with more than 10 years of experience working with Java and web technologies. He is a long-time advocate of open-source and test automation. He holds a Master’s degree and attended countless online courses over the years.