I have several switch statements which test an enum
. All enum
values must be handled in the switch
statements by a case
s
I don't know about the standard Java compiler, but the Eclipse compiler can certainly be configured to warn about this. Go to Window->Preferences->Java->Compiler->Errors/Warnings/Enum type constant not covered on switch.
Functional approach with lambdas, much less code
public enum MyEnum {
FIRST,
SECOND,
THIRD;
<T> T switchFunc(
Function<MyEnum, T> first,
Function<MyEnum, T> second,
Function<MyEnum, T> third
// when another enum constant is added, add another function here
) {
switch (this) {
case FIRST: return first.apply(this);
case SECOND: return second.apply(this);
case THIRD: return third.apply(this);
// and case here
default: throw new IllegalArgumentException("You forgot to add parameter");
}
}
public static void main(String[] args) {
MyEnum myEnum = MyEnum.FIRST;
// when another enum constant added method will break and trigger compile-time error
String r = myEnum.switchFunc(
me -> "first",
me -> "second",
me -> "third");
System.out.println(r);
}
}
I know the question is about Java, and I think the answer for pure Java is clear: it's not a built-in feature, but there are workarounds. For those who arrive here and are working on Android or other systems that can utilize Kotlin, that language provides this feature with its when expression, and the interop with Java allows it to be rather seamless, even if this is the only Kotlin code in your codebase.
For example:
public enum HeaderSignalStrength {
STRENGTH_0, STRENGTH_1, STRENGTH_2, STRENGTH_3, STRENGTH_4;
}
With my original Java code as:
// In HeaderUtil.java
@DrawableRes
private static int getSignalStrengthIcon(@NonNull HeaderSignalStrength strength) {
switch (strength) {
case STRENGTH_0: return R.drawable.connection_strength_0;
case STRENGTH_1: return R.drawable.connection_strength_1;
case STRENGTH_2: return R.drawable.connection_strength_2;
case STRENGTH_3: return R.drawable.connection_strength_3;
case STRENGTH_4: return R.drawable.connection_strength_4;
default:
Log.w("Unhandled HeaderSignalStrength: " + strength);
return R.drawable.cockpit_connection_strength_0;
}
}
// In Java code somewhere
mStrength.setImageResource(HeaderUtil.getSignalStrengthIcon(strength));
Can be rewritten with Kotlin:
// In HeaderExtensions.kt
@DrawableRes
fun HeaderSignalStrength.getIconRes(): Int {
return when (this) {
HeaderSignalStrength.STRENGTH_0 -> R.drawable.connection_strength_0
HeaderSignalStrength.STRENGTH_1 -> R.drawable.connection_strength_1
HeaderSignalStrength.STRENGTH_2 -> R.drawable.connection_strength_2
HeaderSignalStrength.STRENGTH_3 -> R.drawable.connection_strength_3
HeaderSignalStrength.STRENGTH_4 -> R.drawable.connection_strength_4
}
}
// In Java code somewhere
mStrength.setImageResource(HeaderExtensionsKt.getIconRes(strength));
Probably a tool like FindBugs will mark such switches.
The hard answer would be to refactor:
Possibility 1: can go Object Oriented
If feasible, depends on the code in the cases.
Instead of
switch (language) {
case EO: ... break;
case IL: ... break;
}
create an abstract method:, say p
language.p();
or
switch (p.category()) {
case 1: // Less cases.
...
}
Possibility 2: higher level
When having many switches, in an enum like DocumentType, WORD, EXCEL, PDF, ... . Then create a WordDoc, ExcelDoc, PdfDoc extending a base class Doc. And again one can work object oriented.
You could also use an adaptation of the Visitor pattern to enums, which avoid putting all kind of unrelated state in the enum class.
The compile time failure will happen if the one modifying the enum is careful enough, but it is not garanteed.
You'll still have a failure earlier than the RTE in a default statement : it will fail when one of the visitor class is loaded, which you can make happen at application startup.
Here is some code :
You start from an enum that look like that :
public enum Status {
PENDING, PROGRESSING, DONE
}
Here is how you transform it to use the visitor pattern :
public enum Status {
PENDING,
PROGRESSING,
DONE;
public static abstract class StatusVisitor<R> extends EnumVisitor<Status, R> {
public abstract R visitPENDING();
public abstract R visitPROGRESSING();
public abstract R visitDONE();
}
}
When you add a new constant to the enum, if you don't forget to add the method visitXXX to the abstract StatusVisitor class, you'll have directly the compilation error you expect everywhere you used a visitor (which should replace every switch you did on the enum) :
switch(anObject.getStatus()) {
case PENDING :
[code1]
break;
case PROGRESSING :
[code2]
break;
case DONE :
[code3]
break;
}
should become :
StatusVisitor<String> v = new StatusVisitor<String>() {
@Override
public String visitPENDING() {
[code1]
return null;
}
@Override
public String visitPROGRESSING() {
[code2]
return null;
}
@Override
public String visitDONE() {
[code3]
return null;
}
};
v.visit(anObject.getStatus());
And now the ugly part, the EnumVisitor class. It is the top class of the Visitor hierarchy, implementing the visit method and making the code fail at startup (of test or application) if you forgot to update the absract visitor :
public abstract class EnumVisitor<E extends Enum<E>, R> {
public EnumVisitor() {
Class<?> currentClass = getClass();
while(currentClass != null && !currentClass.getSuperclass().getName().equals("xxx.xxx.EnumVisitor")) {
currentClass = currentClass.getSuperclass();
}
Class<E> e = (Class<E>) ((ParameterizedType) currentClass.getGenericSuperclass()).getActualTypeArguments()[0];
Enum[] enumConstants = e.getEnumConstants();
if (enumConstants == null) {
throw new RuntimeException("Seems like " + e.getName() + " is not an enum.");
}
Class<? extends EnumVisitor> actualClass = this.getClass();
Set<String> missingMethods = new HashSet<>();
for(Enum c : enumConstants) {
try {
actualClass.getMethod("visit" + c.name(), null);
} catch (NoSuchMethodException e2) {
missingMethods.add("visit" + c.name());
} catch (Exception e1) {
throw new RuntimeException(e1);
}
}
if (!missingMethods.isEmpty()) {
throw new RuntimeException(currentClass.getName() + " visitor is missing the following methods : " + String.join(",", missingMethods));
}
}
public final R visit(E value) {
Class<? extends EnumVisitor> actualClass = this.getClass();
try {
Method method = actualClass.getMethod("visit" + value.name());
return (R) method.invoke(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
There are several ways you could implement / improve this glue code. I choose to walk up the class hierarchy, stop when the superclass is the EnumVisitor, and read the parameterized type from there. You could also do it with a constructor param being the enum class.
You could use a smarter naming strategy to have less ugly names, and so on...
The drawback is that it is a bit more verbose. The benefits are
This is a variant of the Visitor approach which gives you compile-time help when you add constants:
interface Status {
enum Pending implements Status {
INSTANCE;
@Override
public <T> T accept(Visitor<T> v) {
return v.visit(this);
}
}
enum Progressing implements Status {
INSTANCE;
@Override
public <T> T accept(Visitor<T> v) {
return v.visit(this);
}
}
enum Done implements Status {
INSTANCE;
@Override
public <T> T accept(Visitor<T> v) {
return v.visit(this);
}
}
<T> T accept(Visitor<T> v);
interface Visitor<T> {
T visit(Done done);
T visit(Progressing progressing);
T visit(Pending pending);
}
}
void usage() {
Status s = getRandomStatus();
String userMessage = s.accept(new Status.Visitor<String>() {
@Override
public String visit(Status.Done done) {
return "completed";
}
@Override
public String visit(Status.Progressing progressing) {
return "in progress";
}
@Override
public String visit(Status.Pending pending) {
return "in queue";
}
});
}
Beautiful, eh? I call it the "Rube Goldberg Architecture Solution".
I would normally just use an abstract method, but if you really don't want to add methods to your enum (maybe because you introduce cyclic dependencies), this is a way.