How to ensure completeness in an enum switch at compile time?
Asked Answered
H

14

57

I have several switch statements which test an enum. All enum values must be handled in the switch statements by a case statement. During code refactoring it can happen that the enum shrinks and grows. When the enum shrinks the compiler throws an error. But no error is thrown, if the the enum grows. The matching state gets forgotten and produces a run time error. I would like to move this error from run time to compile time. Theoretically it should be possible to detect the missing enum cases at compile time. Is there any way to achieve this?

The question exists already "How to detect a new value was added to an enum and is not handled in a switch" but it does not contain an answer only an Eclipse related work around.

Haas answered 28/5, 2013 at 16:57 Comment(9)
Presumably you don't have a default case?Freedman
@OliCharlesworth How do you think I got the run time error? ;-)Haas
Well, if you have a default case, then it's unlikely that any compile-time tool would warn you about "missing" cases (because they're not actually missing in that scenario ;) )Freedman
@OliCharlesworth I want to get rid of the default because this kind of default is always a run time error. Because if it would be a valid state it should have been an enum value.Haas
Write a test that fails if one case is missing. Run the tests regularly.Paestum
Could you throw a RTE on the default: case and write a test which inputs all possible enum values?Heparin
I would not want this in all cases. Sometimes some values are left out on purpose, or handled in a default. So it is difficult to come up with a useful compile time check without some language extension where the programmer can indicate if a "complete switch" is desired or not.Nester
@Paestum that's what compilers are forPrototherian
@Heparin that would require writing tests for something the compiler should catch. Even if someone wanted/tried to do this, what would catch when a test is missing? That is why they are asking this question, and why 7 years later people like me are searching for this kind of thing and finding this question.Prototherian
P
1

Since Java 14 (with JEP 361) there are now Switch Expressions which lets you do that. For example with the following example:

public enum ExampleEnum {A, B}

public static String exampleMethod(ExampleEnum exampleEnum) {
    return switch (exampleEnum) {
        case A -> "A";
        case B -> "B";
    };
}

If you add a new enum value C then you get a compile error: java: the switch expression does not cover all possible input values.

But be warned, this does not magically work with "normal" (old) switch statements like:

public static String badExampleMethod(ExampleEnum exampleEnum) {
    switch (exampleEnum) {
        case A:
            return "A";
        case B:
            return "B";
    }
}

This would already throw a compile error: java: missing return statement and so force you to add a default branch (which then does not bring a compile error when adding a new enum value).

Pohai answered 10/9, 2023 at 18:15 Comment(1)
Also note that this doesn't work for new style (->) switch statements that don't yield a value, e.g. where different code is called for each branch but there is no value to be returned.Convalescent
S
29

In Effective Java, Joshua Bloch recommends creating an abstract method which would be implemented for each constant. For example:

enum Color {
    RED   { public String getName() {return "Red";} },
    GREEN { public String getName() {return "Green";} },
    BLUE  { public String getName() {return "Blue";} };
    public abstract String getName();
}

This would function as a safer switch, forcing you to implement the method if you add a new constant.

EDIT: To clear up some confusion, here's the equivalent using a regular switch:

enum Color {
    RED, GREEN, BLUE;
    public String getName() {
        switch(this) {
            case RED:   return "Red";
            case GREEN: return "Green";
            case BLUE:  return "Blue";
            default: return null;
        }
    }
}
Saba answered 28/5, 2013 at 17:56 Comment(3)
It would be useful if you would have shown the corresponding switch. From what you say I got the idea that I have to sub class the original enum in every place where a switch of the enum is used. Is this correct? Sounds like much overhead.Haas
@Haas No, you can't subclass an enum. My assumption was that you have access to the code, in which case it may be preferable to build the switch into the enum itself. See above for an equivalent version implemented as a standard switch.Saba
Ok this is possible (I tried it and it works) but if I have more than one switch, I move unrelated code from different places in a central enum class. It works but the code does not belong there.Haas
S
14

Another solution uses the functional approach. You just need to declare the enum class according with next template:

public enum Direction {

    UNKNOWN,
    FORWARD,
    BACKWARD;

    public interface SwitchResult {
        public void UNKNOWN();
        public void FORWARD();
        public void BACKWARD();
    }

    public void switchValue(SwitchResult result) {
        switch (this) {
            case UNKNOWN:
                result.UNKNOWN();
                break;
            case FORWARD:
                result.FORWARD();
                break;
            case BACKWARD:
                result.BACKWARD();
                break;
        }
    }
}

If you try to use this without one enumeration constant at least, you will get the compilation error:

getDirection().switchValue(new Direction.SwitchResult() {
    public void UNKNOWN() { /* */ }
    public void FORWARD() { /* */ }
    // public void BACKWARD() { /* */ } // <- Compilation error if missing
});
Soddy answered 12/12, 2016 at 17:16 Comment(1)
This reduces n switch statements to 1 switch statement. It is not a perfect solution but a big improvement. But I think it might be better to call SwitchResult Directable and switchValue direct, because I don't think this solution can be abstracted into a template.Haas
F
11

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.

Freedman answered 28/5, 2013 at 17:2 Comment(4)
@ceving: Ah ok. You should probably update your question to indicate that you're explicitly interested in javac (or whichever compiler) then...Freedman
Yes I had the same idea and added the link the the other question.Haas
@ceving: Indeed, I saw that. But you haven't actually said which compiler you're actually interested in (without this information, your question is just a duplicate of that other one ;) )Freedman
@ceving: Well, Eclipse comes with its own compiler, which may be invoked standalone from the command-line if required. (Google for "Eclipse ECJ")Freedman
G
3

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

  • compile time error [in most cases anyway]
  • works even if you don't own the enum code
  • no dead code (the default statement of switch on all enum values)
  • sonar/pmd/... not complaining that you have a switch statement without default statement
Giotto answered 2/10, 2015 at 12:47 Comment(0)
P
3

The Enum Mapper project provides an an annotation processor which will make sure at compile-time that all enum constants are handled.
Moreover it supports reverse lookup and paritial mappers.

Usage example:

@EnumMapper
public enum Seasons {
  SPRING, SUMMER, FALL, WINTER
}

The annotation processor will generate a java class Seasons_MapperFull, which can be used to map all enum constants to arbitrary values.

Here is an example use where we map each enum constant to a string.

EnumMapperFull<Seasons, String> germanSeasons = Seasons_MapperFull
     .setSPRING("Fruehling")
     .setSUMMER("Sommer")
     .setFALL("Herbst")
     .setWINTER("Winter");

You can now use the mapper to get the values, or do reverse lookup

String germanSummer = germanSeasons.getValue(Seasons.SUMMER); // returns "Sommer"
ExtremeSeasons.getEnumOrNull("Sommer");                 // returns the enum-constant SUMMER
ExtremeSeasons.getEnumOrRaise("Fruehling");             // throws an IllegalArgumentException 
Parkman answered 18/5, 2017 at 9:11 Comment(0)
J
2

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.

Jeannettejeannie answered 28/5, 2013 at 17:16 Comment(4)
I think in your possibility 1 it is still possible to miss a "category" at compile time.Haas
It's probably worth mentioning that p() would need to be marked abstract in the base class.Freedman
What does this help? If category() can return 1, 3, and 5 who checks that the switch contains for 1, 3 and 5 a matching case expression?Haas
(1) If an implementation is indeed needed, yes abstract - thanks @OliCharlesworth. It depends on whether normally a switch is added or an enum value is added. (2) If many cases are the same, having a switch with say just 3 categories is less error prone, and the number of categories will not grow as fast if ever. All this is merely a software engineering code style argumentation. Whether sensible depends.Jeannettejeannie
E
2

In my opinion and if the code that your are going to execute is outside of the domain of your enum, a way to do that is to build a unit test case that loops through your items in the enumeration and execute the piece of code that contains the switch.If something goes wrong or not as expected you can check the return value or the state of the object with an assertion.

You could execute the tests as part of some building process and you will see any anomalies at this point.

Anyway, unit testing is almost mandatory and beneficial in many projects.

If the code inside the switch belongs in the enum, include it within as proposed in other answers.

Effusion answered 28/5, 2013 at 18:23 Comment(2)
Using unit tests to fix language problems smells at bit like a hack.Haas
it is indeed, but there is hope with the new switch expressions.Perspire
H
1

If you're using Android Studio (at least version 3 and up) you can activate this exact check in the inspections setting. This might be available on other IntelliJ Java IDE's as well.

Go to Preferences/Inspections. In the Java/Control flow Issues section, check the item Enum 'switch' statement that misses case. Optionally you can change severity to Error to make it more obvious than a warning.

Hambrick answered 10/4, 2018 at 18:21 Comment(0)
S
1

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));
So answered 9/12, 2019 at 16:45 Comment(0)
B
1

Had the same issue. I throw an error on the default case and add a static initializer that iterates all enum values. Simple but fails fast. If you have some unit test coverage it does the trick.

public class HolidayCalculations {
    
    public static Date getDate(Holiday holiday, int year) {
        switch (holiday) {
        case AllSaintsDay:
        case AscensionDay:
            return new Date(1);
        default: 
            throw new IllegalStateException("getDate(..) for "+holiday.name() + " not implemented");
            
        }
    }
    
    static {
        for (Holiday value : Holiday.values()) getDate(value, 2000);
    }
    
}
Brillatsavarin answered 11/3, 2021 at 12:46 Comment(0)
P
1

Since Java 14 (with JEP 361) there are now Switch Expressions which lets you do that. For example with the following example:

public enum ExampleEnum {A, B}

public static String exampleMethod(ExampleEnum exampleEnum) {
    return switch (exampleEnum) {
        case A -> "A";
        case B -> "B";
    };
}

If you add a new enum value C then you get a compile error: java: the switch expression does not cover all possible input values.

But be warned, this does not magically work with "normal" (old) switch statements like:

public static String badExampleMethod(ExampleEnum exampleEnum) {
    switch (exampleEnum) {
        case A:
            return "A";
        case B:
            return "B";
    }
}

This would already throw a compile error: java: missing return statement and so force you to add a default branch (which then does not bring a compile error when adding a new enum value).

Pohai answered 10/9, 2023 at 18:15 Comment(1)
Also note that this doesn't work for new style (->) switch statements that don't yield a value, e.g. where different code is called for each branch but there is no value to be returned.Convalescent
E
0

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.

Electrosurgery answered 28/1, 2016 at 8:6 Comment(1)
I am not sure, if I understand this. Is this one enum with three values or three enums with one value?Haas
R
0

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);
    }

}

Rodenticide answered 7/9, 2020 at 0:25 Comment(0)
R
-1

In case there are several enums on different tiers of the project that must correspond to each other, this can be ensured by a test case:

private static <T extends Enum<T>> String[] names(T[] values) {
    return Arrays.stream(values).map(Enum::name).toArray(String[]::new);
}

@Test
public void testEnumCompleteness() throws Exception {
    Assert.assertArrayEquals(names(Enum1.values()), names(Enum2.values()));
}
Rhodic answered 19/1, 2017 at 18:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.