javapattern-matchingrecord-patterns

Why null value is matching the record pattern?


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?


Solution

  • 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 pattern R(Super s), including the value resulting from evaluating the expression new R(null). (Even though the null value does not match the pattern Super s.) However, we would not expect this value to match the pattern R(Sub s) as the null value for the record component does not match the pattern Sub 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 pattern R(Sub s) is resolved to the pattern R(Sub s), whereas the pattern R(Super s) is resolved to a record pattern with type R 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.