Java Pattern Variable Scope

Viewed 129

I am going through Oracle's official docs to understand Pattern Variable scope in Java 17. In the following example, the method testScope1 works as explained in the docs, but the method testScope2 gives a compilation error. I am not able to figure out why void return type of the method is causing the issue?

interface Vehicle{}
class Car implements Vehicle{}

class Pattern{
    public int testScope1(Vehicle v){
        if(!(v instanceof Car c)){
            return 1;
        }
        System.out.println(c.toString());  //WORKS FINE
        return 2; 
    }
    public void testScope2(Vehicle v){
        if(!(v instanceof Car c)){
                 
        }
        System.out.println(c.toString());  //COMPILE TIME ERROR: Cannot resolve symbol c
    }
}
4 Answers

Think about what happens if v is not a Car instance:

  • In testScope1, the return 1; statement causes the method to be exited. No subsequent statements of the method are exceuted. All is well.
  • In testScope2 there’s no return, so the control flow reaches c.toString(). But v isn’t a Car, so … what is c?! It can’t exist, because it would have to be of type Car but that’s a counterfactual. That’s why you’re getting the error “Cannot resolve symbol c”.

Pattern variables (binding variables declared in patterns) use flow-sensitive scoping. Unlike ordinary locals, which are in scope for in contiguous regions, pattern variables are in scope where they would be definitely assigned by their declaring pattern.

If you have an if statement:

if (x instanceof Foo(var v)) { 
    A;
}
else {
    B;
}

then v is in scope for A, but not for B, because we are not guaranteed that v would be definitely assigned a value in the cases where we reach B. If we invert our test using the obvious refactoring:

if (!(x instanceof Foo(var v))) { 
    B;
}
else {
    A;
}

the same is true; v is in scope at A but not B. The rules are exactly the same as the definite assignment rules for locals -- "is this value guaranteed to have been assigned a value if I reach this point."

Other conditional constructs, such as short-circuiting && and ||, also participate in this scoping. For example, the following is valid:

if (x instanceof Foo(var v) && v != null) { 
    A;
}

but the following is not:

if (x instanceof Foo(var v) || v != null) { 
    A;
}

because in the latter, v is not guaranteed to have been assigned a value when we reach the v != null clause.

The rules even incorporate non-local control flow, such as exceptions. For example, if we had:

if (!(x instanceof Foo(var v)) { 
    System.out.println("Not a Foo");
}
B(v);

this would be an error, since v is not guaranteed to have been assigned a value when we reach B(v), but if the if block completes abruptly:

if (!(x instanceof Foo(var v)) { 
    throw new NotFooException();
}
B(v);

then v is in scope at B(v) because we are guaranteed that if we reach that point, v has been assigned a value.

This may look complicated, but it is actually quite simple: given what you know about the flow control of constructs like if, throw, etc, is a pattern variable guaranteed to have been assigned a value at a given point? If so, it is in scope at that point.

Pattern variable is just syntatic sugar added in java 14. When bytecode is generated, it is no different from simple instanceof and if-else statements. Forexample, I compiled your Pattern class using java 17, and decompiled using java 8. Here is the decompiled version of testScope1 method. Java 17 compiler has itself put an else:

public int testScope1(Vehicle var1) {
  if (var1 instanceof Car) {
    Car var2 = (Car)var1;
    System.out.println(var2.toString());
    return 2;
  } else {
    return 1;
  }
}

So, you can see, the java compiler has modified your if statement, so that Car variable is created only if v is instanceof Car. Simple concepts instanceof, TypeCasting, variable scopes, and if-else have been used, which are there since long.

Answer to your question: Put an else statement. Pattern variable will be created only if instanceof operator yields true. Otherwise there is no variable in the scope, and hence the compiler error.

  public void testScope2(Vehicle v){
    if(!(v instanceof Car c)){

    } else {
      System.out.println(c.toString());
    }
  }

Edit: testScope1 method does not need else statement because the return statement is already there. return statement is ensuring that control never reaches c.toString() if v is not an instance of Car.

Having spent more thoughts on this I have an explanation: The variable can only be accessed if the instanceof is clearly true for that piece of code.

In testScope1 you cannot access c within the if statement. But since the code after the if statement only gets executed when the instanceof is true, c is accessible.

In testScope1 you cannot access c within the if statement. But since the code after the if statement gets executed regardless of instanceof being true or false c is not accessible. After all we might have the false situation and the compiler acts accordingly.

Related