Detailed explanation of JDK21 new feature Record Patterns record mode

1 abstract

Enhance the Java programming language by using record patterns to destructure record values. Record patterns and type patterns can be nested, enabling powerful, declarative, and composable forms of data navigation and processing.

2 Development history

The preview feature was proposed by JEP 405 and released in JDK 19, and then previewed again by JEP 432 and released in JDK 20. This feature has co-evolved with pattern matching for switch (JEP 441), and the two have considerable interaction. This JEP proposes improvements to this feature based on ongoing experience and feedback.

Aside from some minor editorial changes, the main change since the second preview is the removal of support for enhanced for statement header occurrence record mode. This feature may be revisited in a future JEP.

3 goals

  • Extend pattern matching to deconstruct instances of record classes to enable more complex data queries
  • Add nested patterns to achieve more composable data queries

4 motivations

In Java 16, JEP 394 extended the instanceof operator so that it can accept type patterns and perform pattern matching. This simple extension makes the familiar instanceof and cast idioms cleaner and less error-prone:

// <Java 16
if (obj instanceof String) {
    
    
    String s = (String)obj;
    ... 使用s ...
}
// ≥Java 16
if (obj instanceof String s) {
    
    
    ... 使用s ...
}

In the new code, if obj is an instance of String at runtime, then obj matches the type pattern String s. If the pattern match is successful, instanceof true, and the pattern variable s is initialized to the value cast by obj to a String, which can then be used in the containing code block.

Type patterns eliminate the need for many type conversions at once. However, they are only the first steps towards a more declarative, data-focused programming style. As Java supports new, more expressive data modeling, pattern matching can simplify working with this data by letting developers express the semantic intent of the model.

5 Pattern matching and records

Records ( JEP 395 ) are transparent carriers of data. Code that receives an instance of a record class will typically use built-in component accessor methods to extract data from the component.

5.1 Point examples

For example, use a type pattern to test whether a value is an instance of the record class Point, and extract the x and y components from the value if the match is successful.

Java8
class Point {
    
    
    private int x;
    private int y;
    
    public Point(int x, int y) {
    
    
        this.x = x;
        this.y = y;
    }
    
    public int getX() {
    
    
        return x;
    }
    
    public int getY() {
    
    
        return y;
    }
}

static void printSum(Object obj) {
    
    
    if (obj instanceof Point) {
    
    
        Point p = (Point) obj;
        int x = p.getX();
        int y = p.getY();
        System.out.println(x + y);
    }
}
≥Java 16
record Point(int x, int y) {
    
    }

static void printSum(Object obj) {
    
    
    if (obj instanceof Point p) {
    
    
        int x = p.x();
        int y = p.y();
        System.out.println(x+y);
    }
}

The access methods x(), y() are called using only the mode variable p, and these methods return the values ​​of the components x and y.

Within each record class, there is a one-to-one correspondence between its access methods and components.

It would be better if the pattern could not only test whether a value is an instance of Point, but also extract the x and y components directly from the value, thus representing our intent in calling the accessor method. in other words:

// Java 21及以后
static void printSum(Object obj) {
    
    
    if (obj instanceof Point(int x, int y)) {
    
    
        System.out.println(x+y);
    }
}

Point(int x, int y) is a record pattern. It hoists the declarations of local variables used to extract the component directly into the pattern itself, and initializes these variables by calling access methods when the values ​​match the pattern. In effect, the record pattern deconstructs an instance of a record into its components.

6 Nested record pattern

The real power of pattern matching lies in its elegant extension to matching more complex object graphs.

Consider the following statement:

// Java 16及以后
record Point(int x, int y) {
    
    }
enum Color {
    
     RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {
    
    }
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {
    
    }

Components of an object are known to be extracted using record mode. If you want to extract the color from the upper left corner point:

// Java 21及以后
static void printUpperLeftColoredPoint(Rectangle r) {
    
    
    if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
    
    
         System.out.println(ul.c());
    }
}

But the ColoredPoint value ul itself is a record value and I hope to decompose it further. Therefore, record patterns support nesting, allowing further matching and decomposition of record components. You can nest a record schema within another schema, decomposing both outer and inner records:

// Java 21及以后
static void printColorOfUpperLeftPoint(Rectangle r) {
    
    
    if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
                               ColoredPoint lr)) {
    
    
        System.out.println(c);
    }
}

Nested patterns allow you to disassemble aggregates in the same clear and concise way as the code that assembles the objects. To create a rectangle, the constructor is usually nested within an expression:

// Java 16及以后
Rectangle r = new Rectangle(new ColoredPoint(new Point(x1, y1), c1), 
                            new ColoredPoint(new Point(x2, y2), c2));

Using nested mode, we can deconstruct a rectangle like this using code similar to the nested constructor structure:

// Java 21及以后
static void printXCoordOfUpperLeftPointWithPatterns(Rectangle r) {
    
    
    if (r instanceof Rectangle(ColoredPoint(Point(var x, var y), var c),
                               var lr)) {
    
    
        System.out.println("Upper-left corner: " + x);
    }
}

Nested patterns may not match:

// Java 21及以后
record Pair(Object x, Object y) {
    
    }
Pair p = new Pair(42, 42);
if (p instanceof Pair(String s, String t)) {
    
    
    System.out.println(s + ", " + t);
} else {
    
    
    System.out.println("Not a pair of strings");
}

The record pattern Pair(String s, String t) here contains two nested type patterns, namely String s and String t. A value is a Pair if it matches the pattern Pair(String s, String t), and recursively, its component values ​​match the type patterns String s and String t. In our example code above, since neither of the recorded component values ​​are strings, these recursive pattern matches fail, so the else block is executed.

In summary, the nested pattern eliminates the unexpected complexity of navigation objects, allowing us to focus on the data these objects represent. They also give us the ability to centralize error handling, because if a value cannot match the nested pattern P(Q), then neither or both of the subpatterns P and Q will match. We don't need to check and handle each individual subpattern match failure - either the entire pattern matches, or it doesn't.

7 description

Use nestable logging mode.

The pattern syntax becomes:

Pattern:
  TypePattern
  RecordPattern

TypePattern:
  LocalVariableDeclaration

RecordPattern:
  ReferenceType ( [ PatternList ] )

PatternList: 
  Pattern {
    
     , Pattern }

8 recording mode

Consists of a record class type and a (possibly empty) list of patterns used to match the corresponding record component values.

as stated

record Point(int i, int j) {
    
    }

If the value v matches the record pattern Point(int i, int j), then it is an instance of the record type Point; as such, the pattern variable i will be initialized to the result of calling the accessor method corresponding to i on the value v, The mode variable j will be initialized to the result of calling the accessor method corresponding to j on the value v. (The name of the pattern variable need not be the same as the name of the recording component; that is, the recording pattern Point(int x, int y) behaves the same, except that the pattern variables x and y are initialized.)

A null value does not match any record pattern.

Record mode can use var to match record components without declaring the component type. In this case, the compiler infers the type of the pattern variable introduced by the var pattern. For example, the pattern Point(var a, var b) is the abbreviation of the pattern Point(int a, int b).

The set of pattern variables declared by a record pattern includes all pattern variables declared in the pattern list.

An expression is compatible with a record schema if it can be converted to a record type in the schema without requiring an unchecked conversion.

If the record mode names a generic record class but no type parameters are given (that is, the record mode uses primitive types), the type parameters are always inferred. For example:

// Java 21及以后
record MyPair<S,T>(S fst, T snd){
    
    };
static void recordInference(MyPair<String, Integer> pair){
    
    
    switch (pair) {
    
    
        case MyPair(var f, var s) -> 
            ... // 推断的记录模式 MyPair<String,Integer>(var f, var s)
        ...
    }
}

Type parameter inference for record mode is supported in all structures that support record mode, namely instanceof expressions and switch statements and expressions.

Inference works for nested record patterns; for example:

// Java 21及以后
record Box<T>(T t) {
    
    }
static void test1(Box<Box<String>> bbs) {
    
    
    if (bbs instanceof Box<Box<String>>(Box(var s))) {
    
    
        System.out.println("String " + s);
    }
}

Here, the type parameter of the nested pattern Box(var s) is inferred as String, so the pattern itself is inferred as Box(var s).

You can even omit the type parameter in the external record mode, resulting in concise code:

// Java 21及以后
static void test2(Box<Box<String>> bbs) {
    
    
    if (bbs instanceof Box(Box(var s))) {
    
    
        System.out.println("String " + s);
    }
}

Here the compiler will infer that the entire instanceof pattern is Box<Box<String>>(Box<String>(var s)).

To maintain compatibility, type patterns do not support implicit inference of type parameters; for example, the type pattern List l is always treated as a primitive type pattern.

9 record mode and full switch

JEP 441 enhances switch expressions and switch statements to support schema tags. Both a switch expression and a pattern switch statement must be complete: the switch block must have clauses that handle all possible values ​​of the selector expression. For pattern tags, this is determined by analyzing the type of the pattern; for example, the case tag case Bar b matches a value of type Bar and all its possible subtypes.

For schema tags involving record schemas, the analysis is more complex because we have to consider the type of component schema and make adjustments to the sealing hierarchy. For example, consider the following statement:

class A {
    
    }
class B extends A {
    
    }
sealed interface I permits C, D {
    
    }
final class C implements I {
    
    }
final class D implements I {
    
    }
record Pair<T>(T x, T y) {
    
    }
Pair<A> p1;
Pair<I> p2;

The following switch is not complete because there is no match for a pair containing two values ​​of type A:

// Java 21及以后
switch (p1) {
    
                     // 错误!
    case Pair<A>(A a, B b) -> ...
    case Pair<A>(B b, A a) -> ...
}

These two switches are complete because interface I is sealed, so types C and D cover all possible instances:

// Java 21及以后
switch (p2) {
    
    
    case Pair<I>(I i, C c) -> ...
    case Pair<I>(I i, D d) -> ...
}

switch (p2) {
    
    
    case Pair<I>(C c, I i) -> ...
    case Pair<I>(D d, C c) -> ...
    case Pair<I>(D d1, D d2) -> ...
}

In contrast, this switch is incomplete because no pair containing two values ​​of type D is matched:

// Java 21及以后
switch (p2) {
    
                            // 错误!
    case Pair<I>(C fst, D snd) -> ...
    case Pair<I>(D fst, C snd) -> ...
    case Pair<I>(I fst, C snd) -> ...
}

10 future

The description of the recording mode mentions a number of directions in which the recording mode described here can be extended:

  • variadic mode for variable number of records
  • Anonymous patterns, which can appear in the pattern list of recorded patterns, match any value, but do not declare pattern variables
  • Patterns that apply to any class of values, not just record classes.

We can consider some of these directions in future JEPs.

11 Dependencies

This JEP builds on Pattern Matching for instanceof (JEP 394), a feature released in JDK 16. It co-evolved with Pattern Matching for switch (JEP 441).

This article is published by OpenWrite, a blog that publishes multiple articles !

おすすめ

転載: blog.csdn.net/qq_33589510/article/details/133275268