Pattern matching and flow scoping in Java

Marcio Endo
January 21, 2024

JEP 394 brought pattern matching to the Java language as a final, non-preview, feature.

To be more precise, it brought a specific type of pattern matching, the type pattern, to a specific expression, the instanceof.

Along with pattern matching, the JEP brought another new concept to the language: flow scoping.

We'll discuss them in this article.

Pattern matching for instanceof (short introduction)

A common use of the instanceof operator is in equals method implementations.

Suppose a class Article representing a blog article. Here's one possible implementation of the equals method:

@Override
public boolean equals(Object obj) {
  if (obj instanceof Article) {
    Article that = (Article) obj;

    return Objects.equals(title, that.title)
        && Objects.equals(date, that.date);
  } else {
    return false;
  }
}

Yes, there are many other ways to implement the method, bear with me a little. Note the following:

Since Java 16 (without the need of --enable-preview) it is possible to do:

@Override
public boolean equals(Object obj) {
  if (obj instanceof Article that) {
    return Objects.equals(title, that.title)
        && Objects.equals(date, that.date);
  } else {
    return false;
  }
}

Note that the instanceof expression introduces the local variable that. The cast occurs implicitly.

This is an example of the pattern matching for instanceof.

We could also do it without the if statement:

@Override
public boolean equals(Object obj) {
  return obj instanceof Article that
      && Objects.equals(title, that.title)
      && Objects.equals(date, that.date);
}

Note that the variable that is accessible on the right side of the && operator.

In other words, the variable that is in the scope of that operator.

This is an example of the flow scoping introduced by pattern matching for instanceof.

The "traditional" scope

Before going into more detail about flow scoping, let's look at the "traditional" scope.

The following for statement:

var result = new ArrayList<Future<Foo>>();

for (var element : elements) {
  var future = submit(element);

  result.add(future);
}

Introduces two local variables:

The two variables can only be accessed within the for loop. In other words, the scope of variables is restricted to the for statement.

Therefore, the following code does not compile:

var result = new ArrayList<Future<Foo>>();

for (var element : elements) {
  var future = submit(element);

  result.add(future);
}

// compilation error
System.out.println(element);

The element variable cannot be accessed outside the for statement block.

This is the scope for local variables that we are used to as Java developers.

It is somewhat natural to see that the scope is restricted within the curly brackets.

Flow scoping

Flow scoping differs from the previous one in a few important ways. It is not the curly brackets that delimit the scope. It is the program's execution flow that delimits the scope instead.

Let's look at the following example:

static void printIfString(Object o) {
  if (!(o instanceof String s)) {
    return;
  }

  System.out.println(s);
}

public static void main(String[] args) {
  printIfString("Hello");
  printIfString(LocalDate.now());
  printIfString("World!");
}

When executed the program prints:

Hello
World!

Let's focus on the printIfString method.

As it says, it only prints the object when it is a String instance.

The interesting thing is that the local variable s can be accessed at the end of the method:

if (!(o instanceof String s)) {
  return;
}

// 's' is in scope!
System.out.println(s);

Note that if the object o is not a String, then we exit the method via an early return.

In other words, if the flow of execution continues after the if statement block, then the object is certainly a String instance.

Let’s look at a variation of the previous example:

static void printIfString(Object o) {
  if (!(o instanceof String s)) {
    // 's' is NOT in scope
    return;
  } else {
    // 's' is in scope
    System.out.println(s);
  }
}

The else block is executed if, and only if, the object is a String instance.

In this case, the variable s can be accessed in the else block.

Conclusion

Pattern matching brought a new kind of scope to the Java language: flow scoping.

In the article we saw that it is defined by the program's execution flow.

It is, therefore, different from the "traditional" scope in which it is restricted by a method body or a statement block.