0

I have a function that switches on a value of some enum type, and in all cases returns a value. However, the Java compiler is not happy with this function unless it also returns a default value. A minimal example is as follows:

class Main {
    enum MyEnum {
        A,
        B
    }
    
    static String toString(MyEnum e) {
        switch (e) {
            case A: return "A";
            case B: return "B";
        }
        // COMPILER COMPLAINS HERE: 'MISSING RETURN STATEMENT'
    }
    
    public static void main(String[] args) {
        System.out.println(toString(MyEnum.A) + toString(MyEnum.B));
    }
}

Adding either a default case in the switch, or simply returning a default value at the end of the function, now allows the code to compile:

// only 1 of the 2 fixes is required to get this to compile
static String toString(MyEnum e) {
    switch (e) {
        case A: return "A";
        case B: return "B";
        default: return null;  // fix 1
    }
    return null; // fix 2
}

Why is this the case? As I understand it, enums in Java are type-safe so cannot contain unexpected values (whereas in say, C or C++, you could force in a random integer into a function that takes an enum type and really cause problems). As a result the code without a default feels like it should work, but clearly the Java compiler isn't happy about it. Does anybody have information on what about the Java language causes this to be the case?

alex
  • 61
  • 5
  • 7
    What would your function return if `e` is null? – biziclop May 23 '23 at 14:22
  • 2
    If you expect only these two values and not even null, it becomes a validation thing. So validate the input before calling this function or throw `IllegalArgumentException` – Chetan Ahirrao May 23 '23 at 14:27
  • @biziclop in this case it'd emit an `NPE` since you cannot call `Enum#ordinal` on a `null`. For OP, validation and _robustness_ to change are valid here too: If you added another enum constant, then this method would no longer work. Admittedly, a compile-time check would be able to catch that, the only real "evil" thing I can think of here is someone instrumenting new enum constants at runtime. It may just be that the check for this case isn't overly involved. – Rogue May 23 '23 at 14:39
  • A better fix here is to put `throw new AssertionError();` in place of fix 2. That's saying that it's an error to reach that point, rather than silently returning null. – Andy Turner May 23 '23 at 14:42
  • Alternative approach: You could move the `toString` method into your enum class. All instances of the enum would be forced to implement `toString` and there would be no need for a switch statement. – byxor May 23 '23 at 14:42
  • Or: use single return statement with body as `{ return switch (e) { case A -> "A"; case B -> "B";};}` – DuncG May 23 '23 at 14:55
  • you are aware that the class and the enum can eventually be compiled separately - maybe not in this specific code, but in general That means, it is still possible to add a third value to the enum and *forget* to re-compile the switch containing source – user16320675 May 23 '23 at 15:04
  • @user16320675 That's a really good point. In general this sort of function was desired in order to make it a compile-time issue if the enum was extended, which would then prompt fixes elsewhere rather than silently masking issues (or even runtime AssertionErrors as seen elsewhere in the thread). But I did not consider this as a possible path for an unrecognized value to enter the function. – alex May 23 '23 at 15:08
  • @user16320675 Although do you know how this effects the switch expression, as demonstrated in one of the answers? It seems from a previous answer that we might be able to break type safety by separate compilation, as an answer below seems to indicate a switch expression with no default is valid. – alex May 23 '23 at 15:09
  • yes, compiling the files separately will allow *incomplete* switch expressions [`IncompatibleClassChangeError`](https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/IncompatibleClassChangeError.html): "*Thrown when an incompatible class change has occurred to some class definition. The definition of some class, on which the currently executing method depends, has since changed.*" – user16320675 May 23 '23 at 15:30

2 Answers2

3

Does anybody have information on what about the Java language causes this to be the case?

You can find this in JLS 14.22, Unreachable statements:

  • A switch statement whose switch block consists of switch labeled statement groups can complete normally iff at least one of the following is true:

    • The last statement in the switch block can complete normally.
    • There is at least one switch label after the last switch block statement group.
    • There is a reachable break statement that exits the switch statement.
    • The switch block does not contain a default label.

If your switch statement doesn't contain a default: label, it's clear that at least one of those conditions is true.

Thus, your switch statement can complete normally (meaning, it the compiler can't guarantee that it loops forever; returns; or throws an exception).

This means that the statement following the switch statement is reachable, so you have to provide something to do if execution reaches that point.

Remember: you sometimes know more than the compiler. In this case, you know that the switch statement actually can't complete normally, because all of the enum values are covered. In this case, it's appropriate to put throw new AssertionError(); after the switch statement, to indicate that your "model of the world" is invalidated if you reach that point in the program.


However, a better approach (at least in Java 17+) is to use a switch expression, which will not compile unless all of the enum values are explicitly matched (or you provide a default):

return switch (e) {
  case A -> "A";
  case B -> "B";
};

Or, assuming the returned name really is just the name of the enum value:

return e.name();
davidalayachew
  • 1,279
  • 1
  • 11
  • 22
Andy Turner
  • 137,514
  • 11
  • 162
  • 243
  • 2
    you meant `case A -> "A"; case B -> "B";` - `case` is required - see [SwitchRule](https://docs.oracle.com/javase/specs/jls/se20/html/jls-14.html#jls-SwitchRule) and [SwitchLabel](https://docs.oracle.com/javase/specs/jls/se20/html/jls-14.html#jls-SwitchLabel) – user16320675 May 23 '23 at 15:38
1

My understanding of this is that in java switch statements, a default control flow must be defined, even if the default realistically couldn't be reached. In your example, the obvious situation where default "might" happen is if null is passed into the function, though since you're using enums, the error would be thrown at the function call before it even makes it to the switch. Switch expressions can help with the weirdness for more modern versions and let you compile without the default case specifically with this example, since pattern matching will help the compiler know what actually could go into the switch, and if you later add a new enum for C, the compiler would then tell you that you need to cover all possible input values.

static String toStringExp(MyEnum e) {
  return switch(e) {
    case A -> "A";
    case B -> "B";
  };
}

Here is a link to an older question that was very similar with more details