javaswitch-statementjep

Why did JEP 441 weaken the dominance checking of guarded patterns compared to JEP 420


This question addresses the difference between the different JEPs for "Pattern Matching for switch"

In JEP 420 (second preview, Java 18-20), the conditions in guarded patterns are lightly checked for correct dominance order:

Dominance checking now forces a constant case label to appear before a guarded pattern of the same type.

The compiler checks all pattern labels. It is a compile-time error if a pattern label in a switch block is dominated by an earlier pattern label in that switch block.

In JEP 441 (release, Java 21), this check is removed.

So while this raises a (valid) compiler error in Java (18 using &&), 19 and 20:

static void dominanceExampleWithConstant(Object obj) {
   switch (obj.toString()) {
      case String str when str.length() > 5 -> System.out.println(str);
      case "Sophie" -> System.out.println("My lovely daughter");
      //   ^^^^^^^ Dominated by the preceding case label //
      default -> System.out.println("FALLBACK");
   }
}

it no longer does in Java 21.

My question

Why was this compiler check removed?

I'm unable to find an explanation in the JEP. Was there an official comment?


Solution

  • We had some conversation in the comments but ignore those, there's a much more fundamental problem at work. Your premise is wrong. Java17 did not and never has worked that way. Let's go back to your example:

    static void dominanceExampleWithConstant(Object obj) {
       switch (obj.toString()) {
          case String str when str.length() > 5 -> System.out.println(str);
          case "Sophie" -> System.out.println("My lovely daughter");
          default -> System.out.println("FALLBACK");
       }
    }
    

    As you say, this doesn't compile in java17. But it has absolutely nothing to do with the domination issue you raise at all. No, in java17, the syntax was different. Instead of when, the syntax was &&. The above does not compile in java17 even if you remove the domination issue (say, you make that case "Soph" instead to address it). Instead, you write it like so:

    > echo Test.java
    class Test {
      public static void main(String[] args) throws Exception {
        String y = "Hello";
        switch (y) {
          case String str && str.length() > 5 -> System.out.println(str);
          case "Sophie" -> System.out.println("My lovely daughter");
          default -> System.out.println("FALLBACK");
        }
      }
    }
    
    > `/usr/libexec/java_home -v 17`/bin/javac --enable-preview --release 17 Test.java
    
    Note: Test.java uses preview features of Java SE 17.
    Note: Recompile with -Xlint:preview for details.
    
    > `/usr/libexec/java_home -v 17`/bin/java --enable-preview --source 17 Test
    
    FALLBACK
    
    
    

    See? Compiles just fine. Note that you have to run this with java 17, because java18+ cannot run these even if you pass --enable-preview --source 17. 'preview' is serious: Don't use that stuff unless you are toying about.

    That domination business does not take into consideration the evaluation of expressions in any way. Here is an example of domination:

    > cat DominationTest.java
    
    class DominationTest {
      public static void main(String[] args) throws Exception {
        String y = "Hello";
        switch (y) {
          case String str -> System.out.println(str);
          case "Sophie" -> System.out.println("My lovely daughter");
          default -> System.out.println("FALLBACK");
        }
      }
    }
    

    This has 2 issues and both are decidable compile-time without needing to conflate the evaluation of expressions with '... and it will evaluate to that always even if we change the body content of methods such as String's length':

    Indeed, javac 17 dutifully reports both problems:

    > `/usr/libexec/java_home -v 17`/bin/javac --enable-preview --release 17 DominationTest.java
    DominationTest.java:6: error: this case label is dominated by a preceding case label
          case "Sophie" -> System.out.println("My lovely daughter");
               ^
    DominationTest.java:7: error: switch has both a total pattern and a default label
          default -> System.out.println("FALLBACK");
                  ^
    Note: DominationTest.java uses preview features of Java SE 17.
    Note: Recompile with -Xlint:preview for details.
    

    However... so does java 23; because we no longer have a when guard clause, the syntax no longer needs to be modified; it compiles on both, as long as preview is enabled on 17:

    `/usr/libexec/java_home -v 23`/bin/javac --release 23 DominationTest.java
    DominationTest.java:7: error: switch has both an unconditional pattern and a default label
          default -> System.out.println("FALLBACK");
          ^
    DominationTest.java:6: error: this case label is dominated by a preceding case label
          case "Sophie" -> System.out.println("My lovely daughter");
               ^
    2 errors
    

    I have done some toying about, attempting to introduce a guard clause (when/&&) that triggers such errors. Here's the simplest, most obvious take on this: Add when true to the first case. java23 still reports the same errors - the compiler has correctly determined that when true cannot fail.

    That's because this is piggybacking on determinability/reachability rules that have been part of java since forever. This does not compile:

    void foo() {
      while (true) { System.out.println("HI"); }
      System.out.println("BYE");
    }
    

    It doesn't compile because the BYE print statement is deemed 'unreachable'. javac knows this because it knows true, if evaluated, will never be false. 1 But the list of 'can, at compile time, realize this' is incredibly tiny. It is nowhere near being able to conclude that someStr.length() >= 0 can never be false. Here, even this exceedingly simple thing is beyond javac's abilities:

    > cat DominationTest3.java
    class DominationTest3 {
      public static void main(String[] args) throws Exception {
        String y = "Hello";
        switch (y) {
          case String str when Boolean.TRUE -> System.out.println(str);
          case "Sophie" -> System.out.println("My lovely daughter");
          default -> System.out.println("FALLBACK");
        }
      }
    }
    
    > `/usr/libexec/java_home -v 23`/bin/javac --release 23 DominationTest3.java
    ## (no error or warning at all; compilation worked just fine)
    

    Yup. javac failed to determine that evaluating Boolean.TRUE cannot ever be false.

    This is intentional; javac is kept 'dumb' on purpose. It'd be backwards incompatible to change this. Hence, the only thing that the domination system uses is this compile-time-constant facility that is intentionally exceedingly limited. Pretty much the literal true and that's about as far as that'll ever go. That and constructs like 1 > 0. Or a > b where a and b are final local vars or final fields that were definitively assigned as they were declared. But not if they weren't, which is bizarre:

    class WhatThe {
      void test() {
        final int a = 1;
        final int b = 0;
        String c = "Hello";
        switch (c) {
          case String s when a > b -> System.out.println("A");
          case "sophie" -> System.out.println("B");
          default -> "F";
        }
      }
    }
    
    // the above fails to compile; second case is dominated.
    // ... but.. this next one will compile!
    
    class WhatThe2 {
      void test() {
        final int a;
        final int b;
        a = 1;
        b = 0;
        String c = "Hello";
        switch (c) {
          case String s when a > b -> System.out.println("A");
          case "sophie" -> System.out.println("B");
          default -> "F";
        }
      }
    }
    

    Yeah. Now you know. If someone asks you, is there a difference between:

    final int a = 0;
    

    and

    final int a;
    a = 0;
    

    In the context of a method declaration, you'd think the answer is 'obviously there is absolutely no difference', but that'd be wrong in this rather silly way.


    [1] When toying about with this, be aware that if (true) is a special snowflake; the Java Lang Spec explicitly does allow that, specifically as a wonky way to have #IFDEF in java. The idea is that you can write e.g. private static final boolean ADD_SPECIAL_STUFF = true; and edit the source file to make that 'false' if you want to add conditional compilation. This is insane, java does not have a precompiler and this is a weird way to address it... but that really is how it works. So, use while, not if, when playing about with compiler-determinable expression values.