Search code examples
javapattern-matchingjava-21switch-expression

Can a Java 21 switch expression have multiple guards?


I have the following type for a command-line argument lexer (inspired by lexopt)

private sealed interface Opt permits ShortOpt, LongOpt, MiscOpt {}
record ShortOpt(char opt) implements Opt {}  // the "a", "b", or "c" in "-abc"
record LongOpt(String opt) implements Opt {} // the "option" in "--option"
record MiscOpt(String etc) implements Opt {} // something argument with no hyphens

I'd like to pattern match over it, but to handle a short and long option I need to have two copies of each case expression.

switch(opt) {
    case ShortOpt(char c) when c == 'h' ->
        printHelp();
    case LongOpt(String s) when s.equals("help") ->
        printHelp();

    case ShortOpt(char c) when c == 'V' ->
        printVersion();
    case LongOpt(String s) when s.equals("version") ->
        printVersion();

    default ->
        System.out.println("Unrecognized option: " + opt);
}

When I use the Java 21 switch expression over an enum or integer I can combine multiple cases with a comma. But it doesn't seem to work when a guard is involved:

switch(opt) {
    //                                 👇 syntax error
    case ShortOpt(char c) when c == 'h', LongOpt(String s) when s.equals("help") ->
        printHelp();
    case ShortOpt(char c) when c == 'V', LongOpt(String s) when s.equals("version") ->
        printVersion();
    default ->
        System.out.println("Unrecognized option: " + opt);
}

Is there some syntactic form I'm missing? Or is it just not possible?

Tried a comma, a pipe (like in Rust), double-pipe. Looked at the JEP but it doesn't seem to mention anything about multiple case blocks


Solution

  • No, you can't. And that's not an oversight - it is unlikely it'll ever be part of java. The problem is, in a hypothetical:

    switch (opt) {
      case ShortOpt(char c) when c == 'h', LongOpt(String s) when s.equals("help") -> ....
    

    At the ..., the idea is that the c variable is still available. Sure, in this specific case you aren't using it - you use it in the guard clause and don't need it in the body. But you can if you want to.

    With comma - how does that work? Some rule that if you access c and we got here via the LongOpt(String s) path, some runtime exception is thrown? That's not how any of java works right now. That c takes its default value (here, given that it's a primitive, that'd be '\0' - kinda surprising. That, in such a clause, you can't access c in any way?

    That last one seems the most plausible, except, what if I wrote:

    case Foo(x) when x instanceof Integer, case Bar(x) when x instanceof Double
    

    Here I would expect x to be accessible, and of type Number, I guess (it's guaranteed to be one; Number is the most specific type such that no matter how we get there, it's guaranteed to be one) - and that logic matches what happens in e.g. catch (FooEx | BarEx e) - e is then the most specific type that is guaranteed.

    This is all possible but incredibly complicated for a rather exotic use case. Hence why the current implementation certainly doesn't allow it (because the new 'modus operandi' of OpenJDK is to not make such extremely tricky decisions lightly, and to try to not make them at all, instead picking a conservative option (in the sense of: Any questionable stuff? Just make it not compile) and gather data. After all, what I propose could, in theory, be added later. But if it already worked like that, it can never be removed without breaking backwards compatibility. Which OpenJDK sometimes does, but tends to adamantly argue they'd never do in cases like this.

    Thus, odds this will ever happen are highly unlikely and you shouldn't wait around for it. Find another way; 2 case statements, or unify these. For what its worth, I'd unify them. I fail to see a point in attempting to differentiate between a longopt that is a single letter long, and a shortopt. It'd be confusing as heck in the docs of a tool that did that, and it limits you in how one could configure which opts are legitimate.