Nulls and pattern matching
The null value is treated somewhat inconsistently in Java; unfortunately
pattern matching places a fresh focus on these inconsistences. Consider a local
variable, String s=null;. Currently s instanceof String returns false; whereas
(String)ssucceeds; and further switch (s){ case "Hello": ... } throws a NPE.
Unfortunately, we need to maintain these behaviours whilst providing a
consistent story for patterns.
So far, we have essentially two choices on the table. One based on what might
be called a pragmatic navigation of existing choices; and another more
sophisticated one based on static type information. (In what follows, we assume
a declaration T t = null; is in scope.)
Option 1.
Matches t matches Object o. To keep it consistent with instanceof this must
return false.
Switch switch is retconned to not throw a NPE when given a null. However, all
switches with reference-typed selector expressions are considered to have an
implicit case null: throw new NullPointerException(); clause as the first
clause in the switch. If the user supplies a null clause (which means it must
be a pattern-matching switch) then this replaces the implicit clause. [An
alternative to this is to introduce non-null type tests, which we fear would
quickly become unwieldy.]
Note that this addresses a problem that has been brought up on the external
mailing list. Currently:
static void testSwitchInteger(Integer i) {
switch(i) {
case 1: System.out.println("One"); break;
default: System.out.println("Other"); break;
}
}
static void testSwitchNumber(Number i) {
switch(i) {
case 1: System.out.println("One"); break;
default: System.out.println("Other"); break;
}
}
testSwitchNumber(null); // prints "Other"
testSwitchInteger(null); // NPE
The Integer case is an old-style switch, so throws an NPE. The Number case is a
pattern matching case, so without the insertion of an implicit null clause, it
would actually match the default clause (this is the behaviour of the current
prototype).
ASIDE Adding a null clause has an impact on the dominance analysis in pattern
matching. A null pattern must appear before a type test pattern.
Nested/Destructuring patterns As discussed earlier, t matches Object o returns
false. But unfortunately new Box(t) matches Box(Object o) really ought to
return true. (Both because this is what we feel would be expected, but also to
be consistent with expected semantics of extractors.) In other words, the
semantics of matching against null is not compositional. Note also that the
null value never matches against a nested pattern.
We might expect a translation to proceed something like the following.
e matches Box(Object o)
-> e matches Box &&
(e.contents matches null as o || e.contents matches Object o)
-> e instance Box &&
(e.contents == null || e.contents instanceof Object)
(Note the rarely seen as pattern in the intermediate pattern.)
Option 2.
We can use the static type information to classify pattern matches, which
ultimately determines how the matching is translated.
For example:
if (t matches U u) { // where T <: U
...
}
Notice here that the pattern match is guaranteed to succeed as T is a subtype
of U. We can classify this as a type restatement pattern, and compile it
essentially to the following
if (true) {
U u = t;
...
}
In other words, the expression (o matches U u) succeeds depending on the static
type of o: if the static type of o is a subtype of U then it evaluates to true,
even for the value of null. If it is not statically a subtype of U then its
runtime type is tested as normally, and null would fail.
ASIDE The choice of null matching also impacts on our reachability analysis.
For example:
Integer i = ...;
switch (i) {
case Integer j: {
System.out.println(j);
break;
}
default: System.out.println("Something else");
}
Is the default case reachable? If the type test matches null then it is
unreachable, otherwise it is reachable.