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

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

提问人:ceving 提问时间:5/29/2013 最后编辑:Namanceving 更新时间:9/11/2023 访问量:15495

问:

我有几个 switch 语句来测试 .所有值都必须由语句在语句中处理。在代码重构期间,可能会发生收缩和增长的情况。当收缩时,编译器会抛出错误。但是,如果 增长,则不会抛出错误。匹配状态被遗忘并产生运行时错误。我想将此错误从运行时移到编译时。从理论上讲,应该可以在编译时检测到缺失的情况。有什么方法可以做到这一点吗?enumenumswitchcaseenumenumenumenum

问题已经存在:“如何检测新值已添加到枚举中并且未在开关中处理”,但它不包含答案,仅包含与 Eclipse 相关的解决方法。

java switch-语句

评论

1赞 Oliver Charlesworth 5/29/2013
想必你没有案子?default
0赞 ceving 5/29/2013
@OliCharlesworth 您认为我是如何收到运行时错误的?;-)
2赞 Oliver Charlesworth 5/29/2013
好吧,如果你有一个案例,那么任何编译时工具都不太可能警告你“丢失”案例(因为它们在那种情况下实际上并没有丢失;)default
4赞 assylias 5/29/2013
编写一个测试,如果缺少一个案例,则该测试将失败。定期运行测试。
2赞 still_dreaming_1 6/13/2020
@vikingsteve这需要为编译器应该捕获的内容编写测试。即使有人想要/试图这样做,当缺少测试时,会发生什么?这就是为什么他们要问这个问题,为什么 7 年后像我这样的人会寻找这种东西并找到这个问题。

答:

11赞 Oliver Charlesworth 5/29/2013 #1

我不知道标准的 Java 编译器,但 Eclipse 编译器当然可以配置为警告这一点。转到 Window->Preferences->Java->Compiler->Errors/Warnings/Enum type constant not covered on switch。

评论

0赞 Oliver Charlesworth 5/29/2013
@ceving:啊,好吧。您可能应该更新您的问题,以表明您明确对(或任何编译器)感兴趣......javac
0赞 ceving 5/29/2013
是的,我有同样的想法,并在另一个问题中添加了链接。
0赞 Oliver Charlesworth 5/29/2013
@ceving:确实,我看到了。但是你实际上还没有说你真正对哪个编译器感兴趣(如果没有这些信息,你的问题只是另一个编译器的重复;))
0赞 Oliver Charlesworth 5/29/2013
@ceving:嗯,Eclipse 自带编译器,如果需要,可以从命令行独立调用。(谷歌为“Eclipse ECJ”)
2赞 Joop Eggen 5/29/2013 #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。同样,人们可以面向对象工作。

评论

0赞 ceving 5/29/2013
我认为在你的可能性 1 中,在编译时仍然有可能错过一个“类别”。
1赞 Oliver Charlesworth 5/29/2013
可能值得一提的是,需要在基类中标记。p()abstract
0赞 ceving 5/29/2013
这有什么帮助?如果可以返回 1、3 和 5,谁检查开关是否包含 1、3 和 5 的匹配大小写表达式?category()
0赞 Joop Eggen 5/29/2013
(1) 如果确实需要实现,是的,抽象的 - 谢谢@OliCharlesworth。这取决于通常是添加开关还是添加枚举值。(2) 如果许多情况相同,则仅使用 3 个类别进行切换不太容易出错,并且类别的数量不会增长得那么快。所有这些都只是一个软件工程代码风格的论证。是否明智取决于。
29赞 shmosel 5/29/2013 #3

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

评论

0赞 ceving 5/29/2013
如果您能显示相应的开关,那将很有用。从你说的话中,我得到了一个想法,即我必须在使用枚举开关的每个地方对原始枚举进行子类化。这是正确的吗?听起来像是开销很大。
1赞 shmosel 5/30/2013
@ceving 不可以,你不能对 .我的假设是您可以访问代码,在这种情况下,将开关构建到枚举本身中可能更可取。有关作为标准实现的等效版本,请参阅上文。enumswitch
9赞 ceving 6/3/2013
好的,这是可能的(我试过了并且它有效),但是如果我有多个开关,我会从中心类中的不同位置移动不相关的代码。它可以工作,但代码不属于那里。enum
2赞 Víctor Herraiz 5/29/2013 #4

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

您可以将测试作为某些构建过程的一部分来执行,此时您将看到任何异常。

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

如果开关内的代码属于枚举,则按照其他答案中的建议将其包含在枚举中。

评论

4赞 ceving 12/13/2016
使用单元测试来修复语言问题闻起来有点像黑客攻击。
0赞 Víctor Herraiz 10/19/2022
确实如此,但新的 Switch 表达式还是有希望的。
3赞 Thierry 10/2/2015 #5

您还可以将 Visitor 模式改编为枚举,从而避免将所有类型的不相关状态放入枚举类中。

如果修改枚举的人足够小心,则编译时会失败,但没有得到保证。

在默认语句中,您仍然会遇到比 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 类中,那么在你使用访问者的任何地方,你都会直接遇到你所期望的编译错误(这应该替换你在枚举上所做的每个开关):

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 类。它是 Visitor 层次结构的顶层类,实现 visit 方法,如果您忘记更新 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);
        }
    }
}

有几种方法可以实现/改进此胶水代码。我选择沿着类层次结构向上走,在超类是 EnumVisitor 时停止,然后从那里读取参数化类型。您也可以使用构造函数参数作为枚举类来做到这一点。

您可以使用更智能的命名策略来减少丑陋的名字,等等......

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

  • 编译时错误 [无论如何在大多数情况下]
  • 即使您不拥有枚举代码,也可以工作
  • 无死代码(所有枚举值的默认语句)
  • 声纳/PMD/...不抱怨你有一个没有默认语句的 switch 语句
0赞 Alexander Torstling 1/28/2016 #6

这是 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 架构解决方案”。

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

评论

0赞 ceving 12/13/2016
我不确定,我是否理解这一点。这是一个具有三个值的枚举还是具有一个值的三个枚举?
14赞 Ilia Zlobin 12/13/2016 #7

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

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

评论

2赞 ceving 12/13/2016
这会将语句减少到 1 个语句。这不是一个完美的解决方案,而是一个很大的改进。但我认为调用 和 可能会更好,因为我认为这个解决方案不能抽象成模板。nswitchswitchSwitchResultDirectableswitchValuedirect
-1赞 Vadzim 1/20/2017 #8

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

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()));
}
3赞 TmTron 5/18/2017 #9

枚举映射器项目提供了一个注释处理器,它将确保在编译时处理所有枚举常量。
此外,它还支持反向查找和平方映射器。

使用示例:

@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 
1赞 Magnus 4/11/2018 #10

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

转到。在该部分中,选中项目 。(可选)可以将严重性更改为使其比警告更明显。Preferences/InspectionsJava/Control flow IssuesEnum 'switch' statement that misses caseError

1赞 Jeff DQ 12/10/2019 #11

我知道这个问题是关于 Java 的,我认为纯 Java 的答案很明确:它不是一个内置功能,但有解决方法。对于那些来到这里并在 Android 或其他可以使用 Kotlin 的系统上工作的人来说,该语言提供了此功能及其 when 表达式,并且与 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));
0赞 JackHammer 9/7/2020 #12

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

}

1赞 Bruno Wenger 3/11/2021 #13

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

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);
    }
    
}
1赞 Itchy 9/11/2023 #14

从 Java 14(带有 JEP 361)开始,现在有 Switch 表达式可以让你做到这一点。例如,以下示例:

public enum ExampleEnum {A, B}

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

如果添加新的枚举值,则会出现编译错误:.Cjava: the switch expression does not cover all possible input values

但请注意,这并不能神奇地与“正常”(旧)switch 语句一起使用,例如:

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

这已经抛出了一个编译错误:因此强制你添加一个分支(这样在添加新的枚举值时不会带来编译错误)。java: missing return statementdefault

评论

0赞 M. Justin 12/20/2023
另请注意,这不适用于不产生值的新样式 () 开关语句,例如,为每个分支调用不同的代码但没有要返回的值。->