Trying the improved instanceof operator in Java 14

Not far off is the new, 14th version of Java, which means it's time to see what new syntax features this version of Java will contain. One of these syntactic possibilities is pattern matching of the type that will be implemented through the improved (extended) instanceof



operator.



Today I would like to play around with this new operator and consider the features of its work in more detail. Since pattern matching by type has not yet entered the main JDK repository, I had to download the repository of the Amber project , which is developing new Java syntax constructs, and collect the JDK from this repository.



So, the first thing we will do is check the Java version to make sure that we really use JDK 14:



 > java -version openjdk version "14-internal" 2020-03-17 OpenJDK Runtime Environment (build 14-internal+0-adhoc.osboxes.amber-amber) OpenJDK 64-Bit Server VM (build 14-internal+0-adhoc.osboxes.amber-amber, mixed mode, sharing)
      
      





That's right.



Now we will write a small piece of code with the “old” instanceof



operator and run it:



 public class A { public static void main(String[] args) { new A().f("Hello, world!"); } public void f(Object obj) { if (obj instanceof String) { String str = (String) obj; System.out.println(str.toLowerCase()); } } }
      
      





 > java A.java hello, world!
      
      





Working. This is a standard type check followed by a cast. We write similar constructions every day, no matter what version of Java we use, at least 1.0, at least 13.

But now we have Java 14 in our hands, and let's rewrite the code using the improved instanceof



operator (I will omit repeating lines of code in the future):



 if (obj instanceof String str) { System.out.println(str.toLowerCase()); }
      
      





 > java --enable-preview --source 14 A.java hello, world!
      
      





Perfectly. The code is cleaner, shorter, safer and more readable. There were three repetitions of the word String, one became. Note that we did not forget to specify the arguments --enable-preview --source 14



, as The new operator is a preview feature . In addition, an attentive reader probably noticed that we ran the A.java source file directly, without compilation. This feature appeared in Java 11.



Let's try to write something more sophisticated and add a second condition that uses the just declared variable:



 if (obj instanceof String str && str.length() > 5) { System.out.println(str.toLowerCase()); }
      
      





It compiles and works. But what if you swap conditions?



 if (str.length() > 5 && obj instanceof String str) { System.out.println(str.toLowerCase()); }
      
      





 A.java:7: error: cannot find symbol if (str.length() > 5 && obj instanceof String str) { ^
      
      





Compilation error. Which is to be expected: the str



variable has not yet been declared, which means it cannot be used.



By the way, what about mutability? Is the variable final or not? We try:



 if (obj instanceof String str) { str = "World, hello!"; System.out.println(str.toLowerCase()); }
      
      





 A.java:8: error: pattern binding str may not be assigned str = "World, hello!"; ^
      
      





Yeah, the final variable. This means that the word "variable" is not entirely correct here. And the compiler uses the special term “pattern binding”. Therefore, I propose from now on to say not “variable”, but “pattern binding” (unfortunately, the word “binding” is not very well translated into Russian).



With mutability and terminology sorted out. Let's go experiment further. What if we manage to “break” the compiler?



What if we name the variable and the binding of the pattern with the same name?



 if (obj instanceof String obj) { System.out.println(obj.toLowerCase()); }
      
      





 A.java:7: error: variable obj is already defined in method f(Object) if (obj instanceof String obj) { ^
      
      





Is logical. Overlapping a variable from the outer scope does not work. This is equivalent to as if we just wound up the variable obj



second time in the same scope.



And if so:



 if (obj instanceof String str && obj instanceof String str) { System.out.println(str.toLowerCase()); }
      
      





 A.java:7: error: illegal attempt to redefine an existing match binding if (obj instanceof String str && obj instanceof String str) { ^
      
      





The compiler is as solid as concrete.



What else can you try? Let's play around with scopes. If binding is defined in the if



branch, will it be defined in the else



branch if the condition is inverted?



 if (!(obj instanceof String str)) { System.out.println("not a string"); } else { System.out.println(str.toLowerCase()); }
      
      





It worked. The compiler is not only reliable, but also smart.



And if so?



 if (obj instanceof String str && true) { System.out.println(str.toLowerCase()); }
      
      





It worked again. The compiler correctly understands that the condition boils down to a simple obj instanceof String str



.



Is it really not possible to “break” the compiler?



Maybe so?



 if (obj instanceof String str || false) { System.out.println(str.toLowerCase()); }
      
      





 A.java:8: error: cannot find symbol System.out.println(str.toLowerCase()); ^
      
      





Yeah! This already looks like a bug. After all, all three conditions are absolutely equivalent:





Flow scoping rules, on the other hand, are rather nontrivial , and perhaps such a case really should not work. But if you look purely from a human point of view, then I think this is a bug.



But come on, let's try something else. Will this work:



 if (!(obj instanceof String str)) { throw new RuntimeException(); } System.out.println(str.toLowerCase());
      
      





Compiled. This is good, as this code is equivalent to the following:

 if (!(obj instanceof String str)) { throw new RuntimeException(); } else { System.out.println(str.toLowerCase()); }
      
      





And since both options are equivalent, the programmer expects them to work the same way.



What about overlapping fields?



 public class A { private String str; public void f(Object obj) { if (obj instanceof String str) { System.out.println(str.toLowerCase()); } else { System.out.println(str.toLowerCase()); } } }
      
      





The compiler did not swear. This is logical, because local variables could always overlap fields. Apparently, they also decided not to make exceptions for pattern bindings. On the other hand, such code is rather fragile. One careless move, and you may not notice how your if



branch broke:



 private boolean isOK() { return false; } public void f(Object obj) { if (obj instanceof String str || isOK()) { System.out.println(str.toLowerCase()); } else { System.out.println(str.toLowerCase()); } }
      
      





Both branches now use the str



field, which an inattentive programmer might not expect. To detect such errors as early as possible, use the inspections in the IDE and different syntax highlighting for fields and variables. I also recommend that you always use the this



qualifier for fields. This will add even more reliability.



What else is interesting? Like the "old" instanceof



, the new one never matches null



. This means that you can always rely on the fact that pattern binders can never be null



:



 if (obj instanceof String str) { System.out.println(str.toLowerCase()); //    NullPointerException }
      
      





By the way, using this property, you can shorten such chains:



 if (a != null) { B b = a.getB(); if (b != null) { C c = b.getC(); if (c != null) { System.out.println(c.getSize()); } } }
      
      





If you use instanceof



, then the code above can be rewritten like this:



 if (a != null && a.getB() instanceof B b && b.getC() instanceof C c) { System.out.println(c.getSize()); }
      
      





Write in the comments what you think about this style. Would you use this idiom?



What about generics?



 import java.util.List; public class A { public static void main(String[] args) { new A().f(List.of(1, 2, 3)); } public void f(Object obj) { if (obj instanceof List<Integer> list) { System.out.println(list.size()); } } }
      
      





 > java --enable-preview --source 14 A.java Note: A.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. 3
      
      





Very interesting. If the "old" instanceof



only supports instanceof List



or instanceof List<?>



, Then the new one works with any particular type. We are waiting for the first person to fall into such a trap:



 if (obj instanceof List<Integer> list) { System.out.println("Int list of size " + list.size()); } else if (obj instanceof List<String> list) { System.out.println("String list of size " + list.size()); }
      
      





Why is this not working?
Answer: lack of reified generics in Java.


IMHO, this is a pretty serious problem. On the other hand, I do not know how to fix it. It looks like you have to rely on inspections in the IDE again.



conclusions



In general, the new pattern-matching type works very cool. The improved instanceof



operator allows you to do not only type tests, but also declare ready-made binders of this type, eliminating the need for manual casts. This means that there will be less noise in the code, and it will be much easier for the reader to discern useful logic. For example, most equals()



implementations can be written in one line:



 public class Point { private final int x, y; … @Override public int hashCode() { return Objects.hash(x, y); } @Override public boolean equals(Object obj) { return obj instanceof Point p && px == this.x && py == this.y; } }
      
      





The code above can be written even shorter. How?
Using entries that will also be part of Java 14. We’ll talk about them next time.


On the other hand, several controversial issues raise small questions:





However, these are more petty nitpicking than serious claims. All in all, the huge benefits of the new instanceof



operator are definitely worth its add language. And if he still leaves the preview state and becomes a stable syntax, then this will be a great motivation to finally leave Java 8 to the new version of Java.



PS I have a channel in Telegram where I write about Java news. I urge you to subscribe to it.



All Articles