Using Pattern Matching

 

Introducing Pattern Matching

Pattern matching is a feature that is still being worked on. Some elements of this feature have been released as final features in the Java language, some have been released as preview features, and some are still being discussed.

If you want to learn more about pattern matching and provide feedback, then you need to visit the Amber project page. The Amber project page is the one-stop page for everything related to pattern matching in the Java language.

 

Understanding Pattern Matching

If you are new to pattern matching, the first thing you may have in mind is pattern matching in regular expressions. If this is the case, then you may be wondering what does it have to do with "Pattern Matching for instanceof"?

Regular expressions are a form of pattern matching that has been created to analyze strings of characters. It is a good and easy to understand starting point.

Let us write the following code.

String sonnet = "From fairest creatures we desire increase,\n" +
        "That thereby beauty's rose might never die,\n" +
        "But as the riper should by time decease\n" +
        "His tender heir might bear his memory:\n" +
        "But thou, contracted to thine own bright eyes,\n" +
        "Feed'st thy light's flame with self-substantial fuel,\n" +
        "Making a famine where abundance lies,\n" +
        "Thyself thy foe, to thy sweet self too cruel.\n" +
        "Thou that art now the world's fresh ornament,\n" +
        "And only herald to the gaudy spring,\n" +
        "Within thine own bud buriest thy content,\n" +
        "And, tender churl, mak'st waste in niggardly.\n" +
        "Pity the world, or else this glutton be,\n" +
        "To eat the world's due, by the grave and thee.";

Pattern pattern = Pattern.compile("\\bflame\\b");
Matcher matcher = pattern.matcher(sonnet);
while (matcher.find()) {
    String group = matcher.group();
    int start = matcher.start();
    int end = matcher.end();
    System.out.println(group + " " + start + " " + end);
}

This code takes the first sonnet of Shakespeare as a text. This text is analyzed with the regular expression \bflame\b. This regular expression starts and ends with \b. This escaped character has a special meaning in regular expressions: it denotes the start or the end of a word. In this example it means that this pattern matches the word flame.

You can do much more things with regular expression. It is outside the scope of this tutorial.

If you run this code, it will print the following:

flame 233 238

This result tells you that there is a single occurrence of flame between the index 233 and the index 238 in the sonnet.

Pattern matching with regular expression works in this way:

  1. it matches a given pattern; flame is this example and matches it to a text
  2. then it gives you information on the place where the pattern has been matched.

There are three notions that you need to keep in mind for the rest of this tutorial:

  1. What you need to match; this called the matched target. Here it is the sonnet.
  2. What you match against; this is called the pattern. Here the regular expression flame.
  3. The result of the matching; here the start index and the end index.

These three elements are the fundamental elements of pattern matching.

 

Pattern Matching for Instanceof

Matching Any Object to a Type with Instanceof

There are several ways of extending pattern matching. The first one that we cover is called Pattern matching for instanceof; which has been released as a final feature in Java SE 16.

Let us extend the example of the previous section to the instanceof use case. For that, let us describe the three elements we presented there.

The matched target is any object of any type. It is the left-hand side operand of the instanceof operator.

The pattern is a type followed by a variable declaration. It is the right hand-side of the instanceof. The type can be a class, an abstract class or an interface.

The result of the matching is a new reference to the matched target. This reference is put in the variable that is declared as a part of the pattern. It is created if the matched target matches the pattern. This variable has the type you have matched.

Let us examine the following example.

Object object = ...; // any object
if (object instanceof String s) {
    int length = s.length();
    System.out.println("This object is a string of length " + length);
} else {
    System.out.println("This object is not a string.");
}

The variable object is the element you need to match; it is your matched target. The pattern is the String s declaration. The result of the matching is the variable s declared along with the type String. This variable is created only if object is of type String.

This special syntax where you can define a variable along with the type declared with the instanceof is a new syntax added to Java SE 16.

The compiler allows you to use the variable s wherever it makes sense to use it. The if branch is the first scope that comes to mind. It turns out that you can also use this variable in some parts of the if statement.

The following code checks if object is an instance of the String class, and if it is a non-empty string. You can see that it uses the variable s in the boolean expression after the &&. It makes perfect sense because you evaluate this part of the boolean expression only if the first part is true. In that case the variable s is created.

Object object = ...; // any object
if (object instanceof String s && !s.isEmpty()) {
    int length = s.length();
    System.out.println("This object is a non-empty string of length " + length);
} else {
    System.out.println("This object is not a string.");
}

There are cases where the compiler can tell if the matching fails. Let us consider the following example:

Double pi = Math.PI;
if (pi instanceof String s) {
    // this will never be true!
}

The compiler knows that the String class is final. So there is no way that the variable pi can be of type String. The compiler will issue an error on this code.

Writing Cleaner Code with Pattern Matching for Instanceof

There are many places where using this feature will make your code much more readable.

Let us create the following Point class, with an equals() method. The hashCode() method is omitted here.

public class Point {
    private int x;
    private int y;

    public boolean equals(Object o) {
        if (!(o instanceof Point)) {
            return false;
        }
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    // constructor, hashCode method and accessors have been omitted
}

This is the classic way of writing an equals() method; it could have been generated by an IDE.

You can rewrite this equals() method with the following code that is leveraging the pattern matching for instanceof feature, leading to a much more readable code.

public boolean equals(Object o) {
    return o instanceof Point point &&
            x == point.x &&
            y == point.y;
}

 

Pattern Matching for Switch Expressions

Extending Switch Expressions to Use Type Patterns for Case Labels

Pattern Matching for Switch Expressions is not a final feature of the JDK. It is presented as a preview feature in Java SE 17 that we describe here.

Pattern Matching for Switch Expressions uses switch expressions. It allows you to match a matched target to several patterns at once. So far the patterns are type patterns, just as in the pattern matching for instanceof.

In this case the matched target is the selector expression of the switch. There are several patterns in such a feature; each case of the switch expression is itself a type pattern that follows the syntax described in the previous section.

Let us consider the following code.

Object o = ...; // any object
String formatter = null;
if (o instanceof Integer i) {
    formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
    formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
    formatted = String.format("double %f", d);
} else {
    formatted = String.format("Object %s", o.toString());
}

You can see that it contains three type patterns, one for each if statement. Pattern matching for switch expressions allows to write this code in the following way.

Object o = ...; // any object
String formatter = switch(o) {
    case Integer i -> String.format("int %d", i);
    case Long l    -> String.format("long %d", l);
    case Double d  -> String.format("double %f", d);
    case Object o  -> String.format("Object %s", o.toString());
}

Not only does pattern matching for switch expressions makes your code more readable; it also makes it more performant. Evaluating a if-else-if statement is proportional to the number of branches this statement has; doubling the number of branches doubles the evaluation time. Evaluating a switch does not depend on the number of cases. We say that the time complexity of the if statement is O(n) whereas the time complexity of the switch statement is O(1).

So far it is not an extension of pattern matching itself; it is a new feature of the switch, that accepts a type pattern as a case label.

In its current version, the switch expression accepts the following for the case labels:

  1. the following numeric types: byte, short, char, and int
  2. the corresponding wrapper types: Byte, Short, Character and Integer
  3. the type String
  4. enumerated types.

Pattern matching for switch expressions adds the possibility to use type patterns for the case labels.

Extending Type Patterns with Guarded Patterns

The variable created in case the matched target matches the pattern can be used in the boolean expression that contains the instanceof, as in the following example.

Object o = ...; // any object
if (object instanceof String s && !s.isEmpty()) {
    int length = s.length();
    System.out.println("This object is a non-empty string of length " + length);
}

This works well in a if statement, because the argument of the statement is a boolean type. In switch expressions, case labels cannot be boolean. So in theory the following code should not compile.

Object o = ...; // any object
String formatter = switch(o) {
    case String s && !s.isEmpty() -> String.format("Non-empty string %s", s);
    case Object o                 -> String.format("Object %s", o.toString());
}

It turns out that the type pattern has been extended in this preview feature, to allow for a boolean expression to be added to a type pattern. This extended pattern is called a guarded pattern. The expression String s && !s.isEmpty() is such a guarded pattern. It is formed by a type pattern and a boolean expression. This extension makes this syntax compile, with a subtle difference: the && operator does not combine two boolean expressions to make another boolean expression, but a type pattern and a boolean expression to make a guarded pattern.

 

More Patterns

Pattern matching has modified two syntactic elements of the Java language: the instanceof keyword and switch statements. They were both extended with a special kind of patterns called type patterns.

There is more to come in the near future. More elements of the Java language will be modified and mode kind of patterns will be added. This page will be updated to reflect these modifications.

Last update: September 14, 2021


返回教程列表