如何在编译时确保枚举开关的完整性?

时间:2013-05-28 16:57:42

标签: java

我有几个switch语句测试enum。必须通过enum语句在switch语句中处理所有case值。在代码重构期间,enum会缩小并增长。当enum收缩时,编译器会抛出错误。但是如果enum增长,则不会抛出任何错误。匹配状态被遗忘并产生运行时错误。我想将此错误从运行时移动到编译时。从理论上讲,应该可以在编译时检测丢失的enum个案例。有没有办法实现这个目标?

问题已经存在“How to detect a new value was added to an enum and is not handled in a switch”,但它不包含仅与Eclipse相关的解决方案。

13 个答案:

答案 0 :(得分:20)

Effective Java 中,Joshua Bloch建议创建一个为每个常量实现的抽象方法。例如:

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

这将用作安全开关,如果添加新常量,则强制您实施该方法。

编辑:为了澄清一些困惑,这里是使用常规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;
        }
    }
}

答案 1 :(得分:10)

我不知道标准的Java编译器,但是Eclipse编译器当然可以配置为警告这个。转到Window-> Preferences-> Java-> Compiler->错误/警告/枚举类型常量未包含在开关上。

答案 2 :(得分:5)

另一种解决方案使用功能方法。您只需要根据下一个模板声明枚举类:

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

如果你尝试使用这个,至少没有一个枚举常量,你将得到编译错误:

getDirection().switchValue(new Direction.SwitchResult() {
    public void UNKNOWN() { /* */ }
    public void FORWARD() { /* */ }
    // public void BACKWARD() { /* */ } // <- Compilation error if missing
});

答案 3 :(得分:3)

在我看来,如果你要执行的代码在你的枚举域之外,那么一种方法是构建一个单元测试用例,循环遍历枚举中的项目并执行包含switch的代码。如果出现问题或未按预期发生,您可以通过断言检查返回值或对象的状态。

您可以在某些构建过程中执行测试,此时您将看到任何异常。

无论如何,在许多项目中,单元测试几乎是强制性的,也是有益的。

如果交换机内的代码属于枚举,请按照其他答案中的建议将其包含在内。

答案 4 :(得分:3)

您还可以使用访问者模式对枚举的调整,避免将所有类型的无关状态放入枚举类中。

如果修改enum的那个人足够小心,但是没有保证编译时失败。

在默认语句中,您仍然会比RTE更早失败:当加载其中一个访问者类时,它将失败,您可以在应用程序启动时进行此操作。

以下是一些代码:

你从一个看起来像这样的枚举开始:

public enum Status {
    PENDING, PROGRESSING, DONE
}

以下是如何将其转换为使用访问者模式:

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

当您向枚举添加新常量时,如果您不忘记将方法visitXXX添加到抽象StatusVisitor类中,您将直接遇到编辑错误,您希望在任何地方使用访问者(应该替换每个访问者)切换你在enum上做的事情:

switch(anObject.getStatus()) {
case PENDING :
    [code1]
    break;
case PROGRESSING :
    [code2]
    break;
case DONE :
    [code3]
    break;
}

应该成为:

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

现在丑陋的部分,即EnumVisitor类。它是访问者层次结构的顶级类,实现访问方法并在启动(测试或应用程序)时使代码失败,如果您忘记更新absract访问者:

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

有几种方法可以实现/改进此粘合代码。我选择走上类层次结构,当超类是EnumVisitor时停止,并从那里读取参数化类型。您也可以使用构造函数参数作为枚举类。

你可以使用更聪明的命名策略来减少丑陋的名字,等等......

缺点是它有点冗长。 好处是

  • 编译时错误[大多数情况下无论如何]
  • 即使您不拥有枚举代码
  • 也能正常工作
  • 没有死代码(所有枚举值的默认语句)
  • sonar / pmd / ...没抱怨你有一个没有默认声明的开关语句

答案 5 :(得分:2)

像FindBugs这样的工具可能会标记这样的开关。

难以回答的是重构:

可能性1:可以面向对象

如果可行,取决于案例中的代码。

而不是

switch (language) {
case EO: ... break;
case IL: ... break;
}

创建一个抽象方法:,例如p

language.p();

switch (p.category()) {
case 1: // Less cases.
...
}

可能性2:更高级别

当有许多开关时,在一个枚举中,如DocumentType,WORD,EXCEL,PDF,.... 然后创建一个WordDoc,ExcelDoc,PdfDoc扩展基类Doc。再次,人们可以面向对象。

答案 6 :(得分:2)

Enum Mapper project提供了一个注释处理器,可以在编译时确保处理所有枚举常量。
此外,它支持反向查找和paritial映射器。

用法示例:

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

注释处理器将生成一个java类Seasons_MapperFull,可用于将所有枚举常量映射到任意值。

以下是我们将每个枚举常量映射到字符串的示例用法。

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

您现在可以使用映射器获取值,或执行反向查找

String germanSummer = germanSeasons.getValue(Seasons.SUMMER); // returns "Sommer"
ExtremeSeasons.getEnumOrNull("Sommer");                 // returns the enum-constant SUMMER
ExtremeSeasons.getEnumOrRaise("Fruehling");             // throws an IllegalArgumentException 

答案 7 :(得分:1)

我知道问题是关于Java的,我认为纯Java的答案很明确:它不是内置功能,但是有解决方法。对于到达这里并在可使用Kotlin的Android或其他系统上工作的人来说,该语言的when expression提供了此功能,并且与Java的互操作使它相当无缝,即使这是唯一的Kotlin代码中的代码。

例如:

public enum HeaderSignalStrength {
  STRENGTH_0, STRENGTH_1, STRENGTH_2, STRENGTH_3, STRENGTH_4;
}

我的原始Java代码为:

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

可以用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));

答案 8 :(得分:0)

这是Visitor方法的一种变体,它在您添加常量时为您提供编译时帮助:

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

漂亮,嗯?我称之为“Rube Goldberg架构解决方案”。

我通常只使用抽象方法,但是如果你真的不想在你的枚举中添加方法(也许是因为你引入了循环依赖),这是一种方法。

答案 9 :(得分:0)

如果您正在使用Android Studio(至少版本3及更高版本),则可以在检查设置中激活此精确检查。这也可以在其他IntelliJ Java IDE上使用。

转到Preferences/Inspections。在Java/Control flow Issues部分中,检查项Enum 'switch' statement that misses case。您可以选择将严重性更改为Error,使其比警告更明显。

答案 10 :(得分:0)

使用lambda的功能性方法,更少的代码

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

}

答案 11 :(得分:0)

有同样的问题。我在默认情况下抛出一个错误并添加一个迭代所有枚举值的静态初始值设定项。简单但失败很快。如果您有一些单元测试覆盖率,它就可以解决问题。

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

答案 12 :(得分:-1)

如果项目的不同层上有多个枚举必须相互对应,可以通过测试用例来确保:

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