Compile-time-safe-enum-switch-case statements in Java

Michael Graf
Oct 15 · 5 min read
Photo by Markus Winkler on Unsplash

While most modern programming languages like Scala with its Sealed Traits have improved Switch-Case-Statements to check if it matches all possibilities of an enum type. We as Java developers only know the problem of incomplete switch-case statements in our applications. Especially when we extend a Java enum in bigger applications, which is often used in multiple switch-case statements distributed over the whole application.
When we remove a value that is still used in a switch-case statement from such an enum, the compiler gives us an error. But that’s not the case for adding one. If we miss extending one single switch-case statement this could lead to strange problems at runtime.
If you don’t think it’s a problem, look at this example:

package de.digitalfrontiers.javaswitchcase;

public class NormalEnumDemoMain {

public static void main(String[] args) {
for (NormalEnum normalEnum : NormalEnum.values()) {
switch (normalEnum) {
case BAR:
System.out.println("That is awesome");
break;
case BLA:
System.out.println("Bla, Bla, Bla");
break;
case FOO:
System.out.println("Foo, that hurts");
break;
default:
System.out.println("Should not happen!"); // But it does!
break;
}
}
}
}

This example compiles and looks straightforward. But at runtime the line “should not happen!” will be printed, which is clear when we look into the enum class definiton.

package de.digitalfrontiers.javaswitchcase;

public enum NormalEnum {
XYZ, // It's new
FOO,
BAR,
BLA;
}

We can see that the enum also defines the value XYZ, which is not part of our switch case statement. That’s the reason why “Should not happen!” was printed.
It is absolut normal that the enum definitions and the switch-case statements are not in the same place. And we always have to look at both places to check that we don’t miss something. We can use tools like Spotbugs to look for incomplete enum-switch-case statements, but sometimes that’s not what we want. What we want is that our compiler detects that some part of our application is not implemented properly.

We can achieve this by introducing an interface and make usage of the fact that java enum values are classes.

package de.digitalfrontiers.javaswitchcase;

public enum FancyEnum {

XYZ {
@Override
public void switchCase(FancyEnumSwitch enumSwitch) {
enumSwitch.xyz();
}
},
FOO {
@Override
public void switchCase(FancyEnumSwitch enumSwitch) {
enumSwitch.foo();
}
},
BAR {
@Override
public void switchCase(FancyEnumSwitch enumSwitch) {
enumSwitch.bar();
}
},
BLA {
@Override
public void switchCase(FancyEnumSwitch enumSwitch) {
enumSwitch.bla();
}
};

public abstract void switchCase(FancyEnumSwitch enumSwitch);

public interface FancyEnumSwitch {

void foo();

void bar();

void bla();

void xyz();
}
}

This looks more complicated as it is. The enum class defines an abstract method that we called switchCase and an inner interface. The switchCase method will be used as a replacement for our normal switch-case statements. It takes an implementation of the interface as its only parameter. The interface itself is pretty simple. It just defines a method for each value of our enum.

The trick in this pattern is that each enum value calls its corresponding method of the inner interface by overriding the abstract switchCase method. This works because each enum value in Java is a subclass of the enum class itself. So we can override and implement this method for each value separately.

For a complete, compile-safe switch-case statement we just need to call the switchCase method of our enum object. As the only parameter, we give it an anonymous implementation of our new defined interface. Every method contains the logic for its enum counterpart.

package de.digitalfrontiers.javaswitchcase;

public class FancyEnumDemoMain {

public static void main(String[] args) {
for (FancyEnum fancyEnum : FancyEnum.values()) {
fancyEnum.switchCase(
new FancyEnum.FancyEnumSwitch() {
@Override
public void foo() {
System.out.println("Foo, that works");
}

@Override
public void bar() {
System.out.println("That's awesome");
}

@Override
public void bla() {
System.out.println("Bla Bla Bla");
}

@Override
public void xyz() {
System.out.println("We found XYZ");
}
});
}
}
}

As we can see the switch-case statement is transformed into an anonymous inner class definition, which is checked by our compiler at compile time. We also don’t need the error-prone “break” keywords anymore.

Because of the inner class, variable assignments are now a little bit more complex. We could extend our interface with Java-Generics, like this:

package de.digitalfrontiers.javaswitchcase;

public enum FancyEnum {

XYZ {
@Override
public <T> T switchCase(FancyEnumSwitchReturn<T> enumSwitch) {
return enumSwitch.xyz();
}
},
FOO {
@Override
public <T> T switchCase(FancyEnumSwitchReturn<T> enumSwitch) {
return enumSwitch.foo();
}
},
BAR {
@Override
public <T> T switchCase(FancyEnumSwitchReturn<T> enumSwitch) {
return enumSwitch.bar();
}
},
BLA {
@Override
public <T> T switchCase(FancyEnumSwitchReturn<T> enumSwitch) {
return enumSwitch.bla();
}
};

public abstract <T> T switchCase(FancyEnumSwitchReturn<T> enumSwitch);

public interface FancyEnumSwitchReturn<T> {

T foo();

T bar();

T bla();

T xyz();
}
}

But there is a much simpler way. To access a variable in our inner class it has to be only effectively final. this is effectively final and we can assign a new value to every field of our class. So only local variables, that are declared in our function, are a problem. When we want to assign a value to such a variable, we need to convert it to a one-dimensional-final-Java-Array. This array is final and could be used in the anonymous inner class and because java arrays are mutable we can assign a new value to its element(s).

package de.digitalfrontiers.javaswitchcase;

public class FancyEnumDemoMain {

public static void main(String[] args) {
for (FancyEnum fancyEnum : FancyEnum.values()) {
final int[] i = new int[1];
fancyEnum.switchCase(
new FancyEnum.FancyEnumSwitch() {

@Override
public void xyz() {
i[0] = 1;
}

@Override
public void foo() {
i[0] = 2;
}

@Override
public void bar() {
i[0] = 3;
}

@Override
public void bla() {
i[0] = 4;
}
});
System.out.println(i[0]);
}
}
}

That’s not pretty, but it also works with primitives. The Generic version that looks better only works with classes and objects, and it introduces a higher complexity.

Summary

As we can see it is possible to replace the java switch case statement with a compile safe inner class construct. This is indeed not a silver bullet. Especially the variable assignment. But it is better than search for all switch-case statements that we need to extend in big applications. We can still make some copy-paste mistakes in the enum and interface definition. But these problems are easy to find as they are in the same class file, and the mapping of the enum values can easily be tested for all switch-case statements with a single unit test.

Thanks for reading! What’s your favorite Java workaround/pattern to make programming in Java less error-prone or better readable? If you have any questions, suggestions, or ideas, please feel free to contact me.

You can find the complete code of this blog post on GitHub.

You might be interested in the other posts published in the Digital Frontiers blog, announced on our Twitter account.

Digital Frontiers — Das Blog

Dies ist das Blog der Digital Frontiers GmbH & Co.