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

I have several switch statements that check enum . All enum values ​​must be handled by switch by the case . During code refactoring, it may happen that enum shrinks and grows. When enum is compressed, the compiler throws an error. But the error does not occur if enum grows. The compliance state is forgotten and throws a run-time error. I would like to move this error from runtime to compile time. In theory, it should be possible to detect missing cases of enum at compile time. Is there any way to achieve this?

The question already exists How to determine the new value added to the enumeration and not processed in the switch ", but it does not contain an answer only to Work around Eclipse.

+26
java
May 28 '13 at 16:57
source share
9 answers

Another solution uses a functional approach. You just need to declare the enum class according to the following pattern:

 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 any one enum constant, you will get a compilation error:

 getDirection().switchValue(new Direction.SwitchResult() { public void UNKNOWN() { /* */ } public void FORWARD() { /* */ } // public void BACKWARD() { /* */ } // <- Compilation error if missing }); 
+2
Dec 12 '16 at 17:16
source share

In Effective Java, Joshua Bloch recommends creating an abstract method that will 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 will function as a secure switch, forcing you to implement this method if you add a new constant.

EDIT: To eliminate some confusion, here is 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; } } } 
+17
May 28 '13 at 17:56
source share

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, which is not included in the switch.

+11
May 28 '13 at 17:02
source share

In my opinion, and if the code that you are going to execute is outside the domain of your enumeration, the way to do this is to create a unit test case that will go through your elements in the enumeration and execute part of the code that contains the switch. If something goes wrong or not as expected, you can check the return value or state of the object with the statement.

You can run the tests as part of some build process, and at the moment you will see any anomalies.

In any case, unit testing is almost mandatory and useful in many projects.

If the code inside the switch belongs to an enumeration, include it inside, as suggested in other answers.

+3
May 28 '13 at 18:23
source share

Perhaps a tool like FindBugs will tag such switches.

The hard answer is refactoring:

Option 1: Object Oriented Can Go

If possible, depends on the code in cases.

Instead

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

create an abstract method: say p

 language.p(); 

or

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

Opportunity 2: Higher Level

If there are many switches in the enumeration type DocumentType, WORD, EXCEL, PDF, .... Then create WordDoc, ExcelDoc, PdfDoc, expanding the base class Doc. And again, you can work object-oriented.

+2
May 28 '13 at 17:16
source share

You can also use the Visitor template adaptation to enumerations that prevent all types of unrelated states from being placed in the enum class.

Compilation time will fail if one of them changes the enumeration carefully enough, but it is not guaranteed.

You will still have a crash earlier than the RTE in the default statement: it will not succeed if one of the visitor classes loads, which may happen when the application starts.

Here is the code:

You start with an enumeration that looks like this:

 public enum Status { PENDING, PROGRESSING, DONE } 

Here's how you convert it to use a visitor template:

 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 enumeration, if you do not forget to add the visitXXX method to the abstract class StatusVisitor, you will immediately have the compilation error that you would expect wherever you used the visitor (which should replace every switch you made on the enumeration):

 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. This is the highest class of the visitors hierarchy that implements the visit method and provides code at startup (test or application) if you forget to update the 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); } } } 

There are several ways to implement / improve this code. I choose to go up the class hierarchy, stop when the superclass is EnumVisitor and read the parameterized type from there. You can also do this with a constructor parameter, which is an enum class.

You could use a smarter naming strategy to have less ugly names, etc.

The disadvantage is that it is a bit more verbose. Benefits

  • compile-time error [in most cases, anyway]
  • works even if you don’t own the listing code
  • no dead code (default instruction for all enumeration values)
  • sonar / pmd / ... not complaining that you have a switch statement without a default statement
+2
Oct 02 '15 at 12:47
source share

The Enum Mapper project provides an annotation handler that at compile time will verify that all enum constants are processed.
In addition, it supports reverse search and palette maps.

Usage example:

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

An annotation handler will generate the java class Seasons_MapperFull , which can be used to match all enumeration constants with arbitrary values.

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

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

Now you can use mapper to get values ​​or reverse lookups

 String germanSummer = germanSeasons.getValue(Seasons.SUMMER); // returns "Sommer" ExtremeSeasons.getEnumOrNull("Sommer"); // returns the enum-constant SUMMER ExtremeSeasons.getEnumOrRaise("Fruehling"); // throws an IllegalArgumentException 
+2
May 18 '17 at 9:11 a.m.
source share

This is a variant of the Visitor approach that gives you compilation help when adding 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, huh? I call it the "Rube Goldberg Architecture Solution".

Usually I just use an abstract method, but if you really do not want to add methods to your enumeration (perhaps because you introduce circular dependencies), this is the way.

0
Jan 28 '16 at 8:06
source share

If there are several enumerations at different levels of the project, which must correspond to each other, this can be provided 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())); } 
0
Jan 19 '17 at 18:27
source share



All Articles