Magnus' blog


Never throw in default switch expressions

A long-running problem in Java-land is that switch-expressions need to be exhaustive, and this is commonly solved incorrectly with a default branch that throws. This hides any problems until they are encountered as runtime errors, which are usually not discovered by the original developer. Instead, these types of errors can be moved to a compile-time error. This helps drastically, as it pushes the errors to the developer who is actually making the changes, instead of them being discovered in production.

Specifically, this post looks at switch-expressions that evaluate to a single value. For example, the following switch:

final var display = switch(enumValue) {
    case TYPE_1 -> "Type 1";
    case TYPE_2 -> "Type 2";
};

In this case, the code will only compile if the enumValue enum type only contains TYPE_1 and TYPE_2. If you later add a TYPE_3, it will give a compilation error. This is the result that we want, since any change will force the developer to update this too.

It is important to notice that this breaks the Open-closed principle, as a change to an enum owned by one module will force changes to another. This might still be worth it to ensure full coverage of all the options, but use it with consideration.

Object cast

If you have a base interface that other classes inherit from:

interface Base {}
record Derived1(String value) implements Base {}
record Derived2(int value) implements Base {}

You can use a switch-expression to extract the value:

final var display = switch (base) {
case Derived1(String value) -> value;
case Derived2(int value) -> "" + value;
};

However, this will fail because it is not exhaustive. Instead, you can make the interface sealed, and it will work:

sealed interface Base permits Derived1, Derived2 {}
record Derived1(String value) implements Base {}
record Derived2(int value) implements Base {}

The code will now compile.

The sealed interface can only permit classes that it can directly access. This is also a way of enforcing the Open-closed principle, as it will not allow the switch without complete ownership of the class.

Conclusion

Making the switch-expression exhaustive by using the type system will push errors further towards the development phase, which will help ensure fewer issues arise in any environment. If exhaustiveness is not possible, then a switch is probably not the correct pattern to use. In that case, a strategy pattern can be used instead.

View next or previous post: