I have been trying out examples of pattern matching with record patterns in Java 21. The official documentation asserts that a null value does not match any record pattern. However, I try this example:
record Point(Integer x, Integer y) {}
public class MainPatternMatchingRecord {
public static void main(String[] args) {
printSum(new Point(null, 2));
}
private static void printSum(Object obj) {
if (obj instanceof Point(var x, var y)) {
System.out.println(x + 1);
}
}
}
Here, in my understanding of JEP, new Point(null, 2)
shouldn't match in instanceof Point(var x, var y)
, but when run program, throws this exception:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "x" is null
Why is this behaviour happen? How should correctly interpret that null value not match any record pattern?
JEP 440 indeed says:
The null value does not match any record pattern.
But look carefully at what a record pattern is:
A record pattern consists of a record class type and a (possibly empty) pattern list [...]
The JEP also specified the syntax of a record pattern like this:
Pattern:
TypePattern
RecordPattern
TypePattern:
LocalVariableDeclaration
RecordPattern:
ReferenceType ( [ PatternList ] )
PatternList :
Pattern { , Pattern }
By these definitions, we can see that var x
and var y
are not record patterns. They are type patterns. The rule that "The null value does not match any record pattern" does not apply to var x
and var y
.
This rule does apply to the entire Point(var x, var y)
pattern, which is a record pattern, so if obj
is null, it will not match Point(var x, var y)
.
I can't find exactly where in the JEP says that type patterns in record patterns behave like this, but the preview spec does state this explicitly:
Consider, for example:
class Super {} class Sub extends Super {} record R(Super s) {}
We expect all non-null values of type
R
to match the patternR(Super s)
, including the value resulting from evaluating the expressionnew R(null)
. (Even though the null value does not match the patternSuper s
.) However, we would not expect this value to match the patternR(Sub s)
as thenull
value for the record component does not match the patternSub s
.The meaning of a pattern occurring in a nested pattern list is then determined with respect to the record declaration. Resolution replaces any type patterns appearing in a nested pattern list that should match all values including
null
with instances of the special any pattern. In our example above, the patternR(Sub s)
is resolved to the patternR(Sub s)
, whereas the patternR(Super s)
is resolved to a record pattern with typeR
and a nested pattern list containing an any pattern.
In other words, Point(var x, var y)
is resolved to Point(<any>, <any>)
. The <any>
pattern here can match everything, including null.
Here's another example:
record Foo(Integer x, Integer y) {}
record Bar(Foo a, Foo b) {}
private static void printSum(Object obj) {
if (obj instanceof Bar(Foo(Integer x1, Integer y1), Foo(var x2, var y2))) {
System.out.println("matched!");
}
}
Now, a new Bar(null, null)
would not match the pattern Bar(Foo(Integer x1, Integer y1), Foo(var x2, var y2))
, because Foo(Integer x1, Integer y1)
is a record pattern, and null
does not match it.