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

后端 未结 12 793
悲&欢浪女
悲&欢浪女 2020-11-27 06:25

I have several switch statements which test an enum. All enum values must be handled in the switch statements by a case s

相关标签:
12条回答
  • 2020-11-27 06:49

    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.

    0 讨论(0)
  • 2020-11-27 06:49

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

    }

    0 讨论(0)
  • 2020-11-27 06:50

    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));
    
    0 讨论(0)
  • 2020-11-27 06:51

    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.

    0 讨论(0)
  • 2020-11-27 06:52

    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
    0 讨论(0)
  • 2020-11-27 06:54

    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.

    0 讨论(0)
提交回复
热议问题