JEP draft: Pattern Matching for instanceof (Preview 2)

AuthorBrian Goetz
OwnerJan Lahoda
TypeFeature
ScopeSE
StatusDraft
Componentspecification / language
Discussionamber dash dev at openjdk dot java dot net
EffortM
DurationM
Relates toJEP 305: Pattern Matching for instanceof (Preview)
Reviewed byAlex Buckley
Created2019/12/02 15:00
Updated2020/02/18 14:20
Issue8235186

Summary

Enhance the Java programming language with pattern matching for the instanceof operator. Pattern matching allows common logic in a program, namely the conditional extraction of components from objects, to be expressed more concisely and safely.

History

Pattern matching in instanceof was proposed by JEP 305 in mid 2017, and targeted to JDK 14 in late 2019 as a preview feature. This JEP proposes to re-preview the feature in JDK 15, both to extend the notion of a pattern to support pattern matching of records, and to incorporate refinements based on feedback from the community.

Motivation

Nearly every program includes some sort of logic that combines testing if an expression has a certain type or structure, and then conditionally extracting components of its state for further processing. For example, all Java programmers are familiar with the instanceof-and-cast idiom:

if (obj instanceof String) {
    String s = (String) obj;
    // use s
}

There are three things going on here: a test (is obj a String?), a conversion (casting obj to String), and the declaration of a new local variable (s) so we can use the string value. This pattern is straightforward and understood by all Java programmers, but is suboptimal for several reasons. It is tedious; doing both the type test and cast should be unnecessary (what else would you do after an instanceof test?). This boilerplate -- in particular, the three occurrences of the type String --- obfuscates the more significant logic that follows. But most importantly, the repetition provides opportunities for errors to creep unnoticed into programs.

Rather than reach for ad-hoc solutions, we believe it is time for Java to embrace pattern matching. Pattern matching allows the desired 'shape' of an object to be expressed concisely (the pattern), and for various statements and expressions to test that 'shape' against their input (the matching). Many languages, from Haskell to C#, have embraced pattern matching for its brevity and safety.

Description

A pattern is a combination of (1) a predicate that can be applied to a target, and (2) a set of binding variables that are extracted from the target only if the predicate successfully applies to it.

We propose two forms of patterns:

  1. A type test pattern that consists of a predicate that specifies a type, along with a single binding variable.

  2. A deconstruction pattern that consists of a predicate that specifies a record type, along with binding variables for the record components.

Note: Type test patterns were present in JEP 305. Deconstruction patterns are new in this JEP.

The instanceof operator (JLS 15.20.2) is extended to take a pattern as an alternative to a plain type.

In the code below, the phrase String s is a type test pattern:

if (obj instanceof String s) {
    // can use s here
} else {
    // can't use s here
}

The instanceof operator "matches" the target obj to the type test pattern as follows: if obj is an instance of String, then it is cast to String and assigned to the binding variable s. The binding variable is in scope in the true block of the if statement, and not in the false block of the if statement.

The scope of a binding variable, unlike the scope of a local variable, is determined by the semantics of the containing expressions and statements. For example, in this code:

if (!(obj instanceof String s)) {
    .. s.contains(..) ..
} else {
    .. s.contains(..) ..
}

the s in the true block refers to a field in the enclosing class, and the s in the false block refers to the binding variable introduced by the instanceof operator.

When the conditional of the if statement grows more complicated than a single instanceof, the scope of the binding variable grows accordingly. For example, in this code:

if (obj instanceof String s && s.length() > 5) {.. s.contains(..) ..}

the binding variable s is in scope on the right hand side of the && operator, as well as in the true block. (The right hand side is only evaluated if instanceof succeeded and assigned to s.) On the other hand, in this code:

if (obj instanceof String s || s.length() > 5) {.. s.contains(..) ..}

the binding variable s is not in scope on the right hand side of the || operator, nor is it in scope in the true block. (s at these points refers to a field in the enclosing class.)

There are no changes to how instanceof works when the target is null. That is, the pattern will only match, and s will only be assigned, if obj is not null.

The use of type test patterns in instanceof should dramatically reduce the overall number of explicit casts in Java programs. Moreover, type test patterns are particularly useful when writing equality methods. Consider the following equality method taken from Item 10 of the Effective Java book:

@Override public boolean equals(Object o) { 
    return (o instanceof CaseInsensitiveString) && 
        ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); 
}

Using a type test pattern means it can be rewritten to the clearer:

@Override public boolean equals(Object o) { 
    return (o instanceof CaseInsensitiveString cis) && 
        cis.s.equalsIgnoreCase(s); 
}

A deconstruction pattern allows an object to be tested for a type and, if the object matches that type, the values of its components will be assigned to the binding variables in the pattern.

In the code below, the phrase Point(var a, var b) is a deconstruction pattern:

record Point(int x, int y) {}

if (obj instanceof Point(var a, var b)) {
    System.out.println(a+b);
}

The instanceof operator "matches" the target obj to the deconstruction pattern as follows: if obj is an instance of the record type Point, then the value of its x component is assigned to the binding variable a, and the value of its y component is assigned to the binding variable b. Just as with the earlier examples, the binding variables are in scope in the true block of the if statement, and not in the false block of the if statement.

Deconstruction patterns provide a succinct way to deconstruct an object and access its constituent elements. Deconstruction patterns can be freely nested. For example:

record Circle(Point origin, int radius) {}

if (obj instanceof Circle(Point(var a, var b), var r)) {
    System.out.println("Circle centered at ("+a+", "+b+")");
    System.out.println("Area is: " + Math.PI*r*r);
}

In this example, the pattern is Circle(Point(var a, var y), var r), which nests one pattern in another. The outer pattern is a deconstruction pattern that matches a Circle value. If this match succeeds, then the first component of the Circle value is matched against the (nested) deconstruction pattern Point(var a, var b).

Overall, if the entire pattern matches the value obj, then the binding variable a will be assigned to the first component of the nested Point value, b will be assigned to the second component of the nested Point value, and r will be assigned to the second component of the Circle value.

It can be seen that deconstruction patterns are a declarative, compact, and type-safe way of extracting values from complex object graphs.

In this preview version, deconstruction patterns may only be used to deconstruct record types.

The grammar for instanceof is extended to support patterns:

RelationalExpression:
     ...
     RelationalExpression instanceof ReferenceType
     RelationalExpression instanceof Pattern

The grammar for patterns is as follows:

Pattern:
     TypeTestPattern
     DeconstructionPattern

TypeTestPattern:
     ReferenceType Identifier

DeconstructionPattern:
     ReferenceType ( [ PatternArgumentList ] )

PatternArgumentList:
     PatternArgument { , PatternArgument }

PatternArgument:
     DeconstructionPattern
     var Identifier

(Note: This is a preliminary grammar. We expect to additionally support type test patterns as pattern arguments, e.g. patterns of the form Point(int a, int b).)

Future Work

Future JEPs will enhance the Java programming language with pattern matching for other language constructs, such as switch expressions and statements, as well as supporting deconstruction patterns over other types.

Alternatives

The benefits of type-test patterns could be obtained by flow typing in if statements, or by a type switch construct. Pattern matching generalizes both of these constructs.