我有几个开关语句测试一个enum
。所有的enum
值必须在switch
语句中由case
语句处理。在代码重构过程中,enum
可能会收缩和增长。当enum
收缩时,编译器抛出一个错误。但是,如果enum
增长,则不会抛出错误。匹配状态被遗忘并产生运行时错误。我想把这个错误从运行时移到编译时。理论上,在编译时应该可以检测到缺失的enum
情况。有什么办法可以做到这一点吗?
这个问题已经存在了,"如何检测一个新值被添加到一个枚举中,而不是在一个开关中处理",但是它不包含一个答案,只有一个Eclipse相关的工作。
在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;
}
}
}
另一种解决方案使用函数方法。你只需要根据下一个模板声明枚举类:
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
});
我不知道标准的Java编译器,但是Eclipse编译器肯定可以配置为对此发出警告。转到窗口->首选项->Java->编译器->错误/警告/枚举类型常量未覆盖的开关。
您还可以对枚举使用访问者模式的适配,从而避免在枚举类中放置各种不相关的状态。
如果修改enum的人足够小心,编译时就会发生失败,但不能保证会发生。
在默认语句中,你仍然会在RTE之前有一个失败:当一个访问者类被加载时,它会失败,这可以在应用程序启动时发生。
下面是一些代码:从一个看起来像这样的enum开始:
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();
}
}
当你在枚举中添加一个新的常量时,如果你没有忘记在抽象的StatusVisitor类中添加visitXXX方法,你将直接得到你所期望的在任何地方使用访问者(它应该取代你在枚举上做的每个开关)的编译错误:
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方法,如果忘记更新abstract 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时停止,并从那里读取参数化类型。也可以使用构造函数参数作为enum类。
您可以使用更聪明的命名策略来使用不那么难看的名称,等等…
缺点是它有点冗长。好处是
- 编译时错误[在大多数情况下]
- 工作,即使你不拥有枚举代码
- 无死代码(默认开关所有enum值的语句)
- 声纳/pmd/…不抱怨你有一个switch语句没有default语句
枚举映射器项目提供了一个注释处理器,它将确保在编译时处理所有枚举常量。
此外,它还支持反向查找和部分映射器。
使用例子:
@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
可能像FindBugs这样的工具会标记这些开关。
困难的答案是重构:
可能性1:可以选择面向对象
如果可行,则取决于案例中的代码。
不是switch (language) {
case EO: ... break;
case IL: ... break;
}
创建一个抽象方法:,例如p
language.p();
或
switch (p.category()) {
case 1: // Less cases.
...
}
2:可能性更高级别
当有许多开关时,在一个enum中,如DocumentType, WORD, EXCEL, PDF, ... .然后创建WordDoc, ExcelDoc, PdfDoc扩展基类Doc。也可以使用面向对象的
在我看来,如果你要执行的代码在枚举的域之外,一种方法是构建一个单元测试用例,循环遍历枚举中的项,并执行包含开关的代码段。如果出现错误或不符合预期,您可以使用断言检查返回值或对象的状态。
您可以将测试作为某些构建过程的一部分执行,并且您将在此时看到任何异常情况。
无论如何,单元测试在许多项目中几乎是强制性的,而且是有益的。
如果交换机内部的代码属于枚举,则按照其他答案的建议将其包含在枚举中。
如果你正在使用Android Studio(至少是版本3及以上),你可以在检查设置中激活这个精确的检查。这可能在其他IntelliJ Java IDE上也可用。
进入Preferences/Inspections
。在"Java/Control flow Issues
"部分,检查"Enum 'switch' statement that misses case
"项。您可以选择将严重性更改为Error
,使其比警告更明显。
我知道这个问题是关于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));
有同样的问题。我在默认情况下抛出一个错误,并添加一个迭代所有枚举值的静态初始化器。简单但容易失败。如果你有一些单元测试覆盖,它就会奏效。
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);
}
}
这是访问者方法的一种变体,它在添加常量时为您提供编译时帮助:
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架构解决方案"。
我通常只使用抽象方法,但如果你真的不想在枚举中添加方法(可能是因为你引入了循环依赖关系),这是一种方法。
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);
}
}
如果在项目的不同层次上有几个必须相互对应的枚举,这可以通过测试用例来确保:
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()));
}