Pattern Variable Scope (Flow Scoping)

Naveen AnamaneniUpdated · 7 min read

A pattern variable is the variable a pattern declares, like s in o instanceof String s. Unlike an ordinary local variable, its scope isn't simply "the block it's written in". The compiler gives it a scope based on where the match is known to have succeeded. This rule is called flow scoping. In this lesson, you will learn exactly where a pattern variable can be used, with if, else, &&, ||, the ternary operator, early returns and loops, and the errors you get when you step outside that scope.

"Java Instanceof and Pattern Matching" introduced the basic idea; this lesson goes through every case in detail.

The Rule in One Sentence

A pattern variable is in scope wherever the compiler can prove the pattern matched, and nowhere else.

The compiler works this out from the code's structure, without running it:

CodeWhere s is in scope
if (o instanceof String s) { A } else { B }In A only
if (!(o instanceof String s)) { A } else { B }In B only
o instanceof String s && XIn X
o instanceof String s || XNot in X; there the match failed
!(o instanceof String s) || XIn X; X runs only if the match succeeded
o instanceof String s ? A : BIn A only
if (!(o instanceof String s)) return; followed by more codeIn all the code after the if
while (!(o instanceof String s)) { ... } with no breakIn the code after the loop

Example: Every Case in One Program

Let's see an example that uses a pattern variable in each of these positions:

Flow.java▶ Try it
public class Flow {
    static String name = "field";
 
    static int lengthOrZero(Object o) {
        return o instanceof String s ? s.length() : 0;
    }
 
    static void elseBranch(Object o) {
        if (!(o instanceof String s)) {
            System.out.println("not a string");
        } else {
            System.out.println("else branch sees " + s);
        }
    }
 
    static void loop(Object[] items) {
        int i = 0;
        Object o = items[i];
        while (!(o instanceof String s)) {
            o = items[++i];
        }
        System.out.println("first string: " + s);
    }
 
    static void shadow(Object o) {
        if (o instanceof String name) {
            System.out.println("inside: " + name);
        }
        System.out.println("outside: " + name);
    }
 
    static void reassign(Object o) {
        if (o instanceof String s) {
            s = s.toUpperCase();
            System.out.println(s);
        }
    }
 
    public static void main(String[] args) {
        System.out.println(lengthOrZero("Navika"));
        System.out.println(lengthOrZero(7));
        elseBranch(5);
        elseBranch("Advitha");
        loop(new Object[] {1, 2.5, "Srihitha", "x"});
        shadow("pattern");
        reassign("srihitha");
    }
}
Output
6
0
not a string
else branch sees Advitha
first string: Srihitha
inside: pattern
outside: field
SRIHITHA

Explanation:

  • lengthOrZero: in the ternary, s exists only in the "true" part, s.length().
  • elseBranch: the test is negated, so the if block runs when the match failed and the else block when it succeeded. s is available in the else block.
  • loop: the loop keeps going while o is not a String. When the loop ends, the match must have succeeded, so s is usable after the loop.
  • shadow: the pattern variable name hides the static field name inside the if. After the if, name means the field again.
  • reassign: a pattern variable is an ordinary variable, so you can assign to it, unless you declare it final.

The Early-Return Style

The most useful case in real code is rejecting the wrong type at the top of a method:

EarlyReturn.java▶ Try it
public class EarlyReturn {
    record Student(String name, int marks) { }
 
    static String report(Object o) {
        if (!(o instanceof Student s)) {
            return "not a student";
        }
        // From here on, s is a Student.
        return s.name() + " scored " + s.marks();
    }
 
    public static void main(String[] args) {
        System.out.println(report(new Student("Navika", 91)));
        System.out.println(report("hello"));
    }
}
Output
Navika scored 91
not a student

Explanation: If o isn't a Student, the method returns. So the code after the if only runs when the match succeeded, and s stays in scope for the rest of the method. This keeps the main logic unindented.

When the Variable Is Not in Scope

Using the variable where the match might have failed is a compile error:

F3.java
static void m(Object o) {
    if (o instanceof String s) {
        System.out.println(s);
    }
    System.out.println(s.length());
}
Compiler output
F3.java:6: error: cannot find symbol
        System.out.println(s.length());
                           ^
  symbol:   variable s
  location: class F3
1 error

After the if, the match may have failed, so s doesn't exist. The same happens when a loop can be left with break, because then the loop can end without a match:

F4.java
while (!(o instanceof String s)) {
    if (o == null) break;
    o = o.toString();
}
System.out.println(s);   // error: cannot find symbol

Name Clashes and Final

A pattern variable may hide a field (as the shadow method showed), but it may not reuse the name of a local variable that is still in scope:

Compiler output
E1.java:5: error: variable s is already defined in method main(String[])
        if (o instanceof String s) { }
                                ^
1 error

To stop a pattern variable from being reassigned, declare it final:

Compiler output
F2.java:4: error: cannot assign a value to final variable s
            s = "x";
            ^
1 error

That error comes from if (o instanceof final String s) { s = "x"; }.

Scope in Switch Cases

In a switch, each case's pattern variable is in scope only in that case's guard and body:

Snippet.java
switch (o) {
    case String s -> System.out.println(s.length());   // s is a String here
    case Integer s -> System.out.println(s + 1);        // a different s
    default -> System.out.println("other");
}

Both cases can use the name s, because the scopes don't overlap. "Pattern Matching for Switch in Java" covers this in detail.

Common Mistakes

Using the variable after an if that doesn't return. Either use it inside the if, or turn the test around and return early.

Combining with ||. In o instanceof String s || s.isEmpty(), the right side runs only when the match failed, so s isn't available.

Reusing a local variable's name. Pick a different name for the pattern variable.

Adding a break to a loop that defines a pattern variable. The variable is then no longer in scope after the loop.

Interview Corner

What is flow scoping in Java pattern matching?

Flow scoping means a pattern variable is in scope exactly where the compiler can prove the pattern matched. In if (o instanceof String s) it is in the if block; with a negated test it is in the else block, or after the if when the if block always exits (return or throw). It is available on the right of && but not of ||, in the true branch of a ternary, and after a while (!(o instanceof T t)) loop that has no break. A pattern variable can hide a field but can't redeclare a local variable, and it can be reassigned unless declared final.

Practice

  1. Easy: Rewrite a method that uses if (o instanceof Student s) { ...long body... } into the early-return style.
  2. Harder: For each row of the table at the top of this lesson, write a two-line method and check with the compiler whether s is in scope where the table says.

Quick check: Flow scoping

4 questions
1In if (!(o instanceof String s)) { A } else { B }, where is s usable?
2Is s usable in X in: o instanceof String s || X?
3When is s usable after if (!(o instanceof String s)) { ... }?
4Can a pattern variable be reassigned?

Key Takeaways

Was this tutorial helpful?

Lesson 455 of 482