Tagir Valeev recently had a tweet about the preview feature of the soon coming JDK14 release of Java:
#Java14 pattern matching brings name shadowing to the next level of craziness. Here I add or remove the `final` modifier for the `FLAG` field, which is accessed only in unreachable `if` branch. This actually changes the program semantics! #ProgrammingIsFun. pic.twitter.com/UToRY3mpW9
The issue is that there is a planned and in the EA release already available new feature of Java that introduces pattern variables and the current version of the proposed new standard leaves room for some really spooky coding issue.
Following the tweet, the details were discussed in detail enough to understand the actual problem. In this article, however, I will summarize what all this is about so that you do not need to dig yourself through the tweets and the standards.
What is a pattern variable
Before getting into the deep detail of the issue outlines in the tweet above, let’s discuss a bit, what a pattern variable is. (Maybe a bit sloppy, more explanatory than precise and complete, but here it comes.)
Programming many times we need to check the type of some objects. The operator
instanceof does that for us. A typical sample code can be something like this:
In real life, the variable
z may come from somewhere else, in which case it is not so obvious that this is a string. When we want to print out the length of the string using
println we already know that the object referenced by
z is a string. The compiler, on the other hand, does not.We have to cast the variable to a
String and then we can use the
length() method. Other languages do it better. Ideally, I could write:
That is not the Java way and also that is not the way JDK14 simplifies this programming pattern. Instead, the proposed feature introduces a new syntax for the
instanceof operator that introduces a new variable: a pattern variable.
To make a long story short, the above example will look the following:
It introduces a new variable
s that is in scope only when the referenced object is a
String. A simpler version of the code without the exception throwing part would be
When the condition is true, the object is a string thus we have ‘s’. If the condition is false then we jump over the then_statement, and there we do not have ‘s’ as we do not have a string. ‘s’ is available in the code which only runs when the object is a string. That way the variable scope of a pattern variable is determined and constrained not only by the syntactical scope of the variable but also by the possible control flow. Only the control flow that can be analyzed with certainty is taken into account.
Such control-flow analysis is not unparalleled in the Java compiler. A Java program will not compile, for example, if there is an unreachable code that the compiler can detect.
So far it seems to be simple and we are all happy to get the new feature in Java 14.
The JSL14 standard
The precise scope calculation is defined in the JLS14 (Java Language Specification 14) standard. At the time of this article, the spec is only available as a preview.
As the execution flow of a Java program can be controlled by many different language-constructs the scope of a pattern variable is defined for each of these structures. There are separate sections for the different logical operators that evaluate short-circuit, ‘if’ statement, ‘while’ statement and so on. I do not want to discuss the different cases extensively. I will focus here only on the case of the ‘if’ statement without the ‘else’ part. The standard cited above says:
The following rules apply to a statement `if (e) S` (14.9.1):
* A pattern variable introduced by e when true is definitely matched at `S`.
It is a compile-time error if any pattern variable introduced by `e` when true is already in scope at `S`.
* `V` is introduced by `if (e) S` if and only if `V` is introduced by `e` when `false` and `S` cannot complete normally.
It is a compile-time error if any pattern variable introduced by the `if` statement is already in scope.
The interesting part is the “cannot complete normally”. A good example of this is our example above: we create a so-called guarding
if statement. When the variable
z is not a
String then we throw an exception, return or do something else that will always prevent the execution to reach the code after the
if statement when the variable is not a
In the case of a
return statement, it is usually very straightforward and easy to see that the code “cannot complete normally”. In case of an infinite loop, this is not always so evident.
Let’s have a look at the following code fragment:
In this case, we have a loop, which is infinite or not. It depends on the other part of the code that may alter the value of the class field
false. This part of the code “can complete normally”.
If we modify the above code just a little making the field
FLAG to be
then the compiler will see that the loop is infinite and cannot complete normally. The program will print out
Hello from field in the first case, and it will print
Hello from pattern matching. The pattern
variable in the second case hides the field
variable because of the scope of the pattern variable is extended to the commands following the
if statement because the then-part cannot complete normally.
This is really a problem with this preview feature as it is. The readability of the code, in this case, is very questionable. The scope of the pattern variable and if it is hiding a field or not depends on the
final modifier of the field, which is not there. When we look at some code the actual execution and the result of the code should be simple and should not really depend on some code that is far away and may skip our attention reading the code locally.
This is not the only situation in Java that has this anomaly. You can have a class named
String for example in your codebase. The code of the classes, which are in the same package will use that class when they refer to the type
String. If we delete the
String class from the user code then the meaning of the
String type becomes
java.lang.String. The actual meaning of the code depends on some other code that is “far”.
This second example, however, is a hack and it is not likely that a Java programmer who has not lost their mind names a class
String (seriously https://github.com/verhas/jScriptBasic/blob/master/src/main/java/com/scriptbasic/classification/String.java?) or some other name that also exists in the JDK in the
java.lang package. Maybe it is pure luck, maybe it was well considered during the decision making to avoid the mandatory import of the classes from the
java.lang package. This is history.
The variable name shadowing and the situation above is, on the other hand, does not seem to be so weird and something that surely will not accidentally happen in some Java code.
Fortunately, this is only a preview feature. It will be in the JDK14 as it is, but as a preview feature it is only available when the javac compiler and the java execution uses the
--enable-preview flag and the preview feature may change in the future in an incompatible way.
I cannot tell how it will change. I cannot even tell that it will change at all. It is only my personal opinion that it would be very sad if it remained like that. With this feature, Java would be a better language so long as long we count how brilliantly and readable a seasoned Java programmer can program. But it will worse if we look at how a non-seasoned, fresh junior can fuck the code up. In my humble opinion, this second is the more important and Java has a very strong point in this. Java is not a hacker language, and you should be very desperate to write a very unreadable code. I would not like it changing.
After having said that we can look at the technical possibilities. One is to abandon the feature, which would not really be a good solution. It would not actually be a solution.
Another possibility is to limit the scope of the pattern variables to the
then statement or to the
That way we do not rely on the “cannot complete normally” feature of the code. The
else guarantees that the
else branch is executed only when the condition of the
if statement is
false. This will make the solution less elegant.
Again another possibility is to forbid for the pattern variables to shadow any field variable. It would solve the problem outlined above but would introduce a different one. With this restriction, it could happen that an existing class with methods and pattern variable
V stops compiling when we introduce a new field variable named
V. At least this issue is compile-time and not some code that is buggy during run-time.
I rather have 100 compile time error than one run-time error.
Still another possibility is to abandon the pattern variable and just to use the original variable with extended type information where the current preview solution uses the pattern variable. Kotlin fans would love this solution. This would also elegantly eliminate the shadowing issue because the local variable already shadows (or does not) the field variable. The drawback of this solution is that the variable type re-scoped would have different types in different places in the code. Let’s have a look at the following code:
This code will print out
A because the call to
b.m() is the same as
B.m() based on the declared type of the variable
b and the same way
a.m() is the same as
A.m() based on the declared type of the variable
a. Omitting the pattern variable and using the original variable could make confusion:
a.m() call different methods on different lines?
As you can see there is no known good or best solution to this issue… except one. Call your representative in the JDK “parliament” and tell them that it is not good that way. (Psst: they already know it from the original tweet.)
This is a special article because this is not about some well established Java feature or some good programming tool or style, pattern, methodology. We discussed a preview feature. A preview feature that, perhaps, proves why we need preview features in Java.
Use the latest LTS version for long-running commercial projects that will need long term support from you.
Use the latest released Java version for your experiments and opensource projects and be prepared to support older Java versions if the users need it.
Do not use the preview features in your projects or be prepared to have a new release from your code in case they change in the next Java releases when they become non-preview but normal features.
Experiment with the pre-view features to embrace them and to have a kind of muscle memory when they become real features. And also to give feedback to the Java community in case you feel they are not really perfect.