Local type inference in Java 10, or If it quacks like a duck
Quite recently, Oracle adopted a new strategy of releasing a new language version every six months. The strategy assumes that only every 3rd version will have long-term support, or LTS. Quick notes about it:
- Current version that has LTS is Java 8;
- Java 9 is only supposed to have support until March 2018, so it is already OVER;
- Java 10 is supported through September 2018;
- Next version to have LTS is supposed to be Java 11, which is supposed to be released in September 2018 and will be supported at least through to September 2023.
More on that here in the Oracle Java SE support roadmap.
Our company still haven’t adopted Java 9, but now it looks that skipping it completely and jumping to the next LTS might be a valid option. There are many more concerns except for Java itself, for example Spring Boot Java versions etc, so we probably will tread carefully. Still, in view of the inevitable changes, I decided to look into what’s coming in Java 10. And it looks like the main thing that’s coming in that version is local type inference.
We all know this Java syntax:
List<User> list = new ArrayList<User>(); // or since Java 7 List<User> list = new ArrayList<>();
Basically, local type inference is the possibility to replace it with:
// left side type is inferred from the right side and will be ArrayList var userList = new ArrayList();
This means that the code has a little less boilerplate, but you need to pay more attention to variable and method names, because the var keyword isn’t self-explanatory.
Local type inference has been there for a long time in a lot of programming languages, such as Scala, C#, Go, and of course Kotlin. In that respect Java has been trailing behind, and now has decided to fix that. However, there was some controversy regarding the concrete implementation of this. For example, there was a possibility of:
- having val for local constants and var for variables as in Kotlin or Scala;
- having const or just final for local constants and var for variables since const and final are already reserved in Java;
- having final var for constants and var for variables;
- utilizing let, def or := ;
- and more on this here.
Finally, it was decided to keep the syntax closer to what’s already there and allow var for local variables and final var for local constants, so the example shown above will work in Java 10. However, the replacement isn’t always straightforward. For example:
// example 1 - list is a List<User> type List<User> list = new ArrayList<>(); // example 2 - userList is an ArrayList<Object> type, so you lose type information var userList = new ArrayList<>(); // example 3 - userListFixed is an ArrayList<User> type, so you keep type information var userListFixed = new ArrayList<User>();
Here in example two, with direct replacement of the left side the compiler isn’t able to infer the list type, so it’ll default to Object. Which will trip you when you try to process the items from the list.
One example where local type inference is helpful is data transformations. For example, you want to change the objects of one type into another, having different attributes, and use an anonymous class for that. In Java 8, you could only do it inside a stream scope, so for example the new properties would be accessible to you within a stream pipeline, but not outside.
List<User> users = Arrays.asList( new User("Elisabeth", "Bennett", 20), new User("Jane", "Bennett", 22), new User("Mary", "Bennett", 18), new User("Kitty", "Bennett", 17), new User("Lydia", "Bennett", 15) ); users.stream() .map(u -> new Object() { String fullName = u.firstName + " " + u.lastName; boolean canDrink = u.age >= 18; }) .forEach(u -> { if (u.canDrink) { System.out.println("+ " + u.fullName + " is of age and can drink"); } else { System.out.println("- " + u.fullName + " is not of age and cannot drink"); } }); /* Output will be * + Elisabeth Bennett is of age and can drink * + Jane Bennett is of age and can drink * + Mary Bennett is of age and can drink * - Kitty Bennett is not of age and cannot drink * - Lydia Bennett is not of age and cannot drink */
In Java 10, you could use the transformed list of new object outside the stream pipeline.
List<User> users = Arrays.asList( new User("Elisabeth", "Bennett", 20), new User("Jane", "Bennett", 22), new User("Mary", "Bennett", 18), new User("Kitty", "Bennett", 17), new User("Lydia", "Bennett", 15) ); final var objects = users.stream() .map(u -> new Object() { String fullName = u.firstName + " " + u.lastName; boolean canDrink = u.age >= 18; }) .collect(Collectors.toUnmodifiableList()); // do something with the users... System.out.println(); for (var o : objects) { if (o.canDrink) { System.out.println("+ " + o.fullName + " is of age and can drink"); } else { System.out.println("- " + o.fullName + " is not of age and cannot drink"); } }
So, after the stream closure is over, the mapped object properties are still accessible. However, as it is a local type inference, you won’t be able to use them outside the local scope of the current method. Var keyword won’t work in a method parameters declaration, and so you can’t pass in a var into another method. So, having var doesn’t mean that Java magically becomes dynamically typed. It is still typed statically, just with the added bit of syntactic sugar, and only in places where the compiler can infer the type.
To me, it is showing that Java strives to move forward, but is very much hindered by trying to keep to its historical roots, making backwards compatibility and not innovation its main priority.
Published on Java Code Geeks with permission by Maryna Cherniavska, partner at our JCG program. See the original article here: Local type inference in Java 10, or If it quacks like a duckOpinions expressed by Java Code Geeks contributors are their own. |