Map all values of an enumeration to something

Suppose you have a list of fruits:

public enum Fruit {
  banana, strawberry, blueberry
}

and you need a Map of Fruits to colours associate a colour to each of those.

public Map<Fruit,String> getFruitColours();

Level seven:

Let’s use a properties file!

banana=yellow
strawberry=red
blueberry=blue
public Map<Fruit,String> makeColourMap() {
  Map<Fruit,String> result = new HashMap<>();
  for (Fruit fruit : Fruit.values()) {
    result.put(fruit, myProperties.getProperty(fruit.name()));
  }
  return result;
}

What if someone comes and adds blackberries to your fruit, and forgets to update the properties file? He doesn’t even know about your makeColourMap method which will happily map Fruit.blackberry to null. Level seven.
As an aside: what if you make a typo in the properties files, e.g. you wrote bananana=yellow in line one? There will be no errors or warnings either at compile-time or at runtime but banana will be mapped to null. Writing the mapping in Java offers level one protection against typos:

public Map<Fruit,String> makeColourMap() {
  Map<Fruit,String> result = new HashMap<>();
  result.put(Fruit.bananana, "yellow"); // compile-time error here!
  result.put(Fruit.strawberry, "red");
  result.put(Fruit.blueberry, "blue");
  return result;
}

Level six

Okay we can add a null-check:

public Map<Fruit,String> makeColourMap() {
  Map<Fruit,String> result = new HashMap<>();
  for (Fruit fruit : Fruit.values()) {
    result.put(fruit,
      Preconditions.checkNotNull(myProperties.getProperty(fruit.name()), fruit.name()));
  }
}

Congratulations, you just made it to level six. When using whatever feature of the application requires this map you’ll get a NullPointerException(“blackberry”). Looking at the makeColourMap() method he can trace back the error to the properties file.

Level five

Making it to level five is easy enough: just add a call to makeColourMap() in your application initialisation code:

public void initialise() {
  // ...
  makeColourMap();
}

As long as error stacktraces get printed in full you’ve satisfied the requirements of level five.

Level four

Whenever you have two files that need to be synchronised (Fruit.java and fruitcolours.properties) you can achieve level four for this synchronisation with a unit test.

@Test
public void allFruitsHaveColours() {
  Properties colours = new Properties();
  colours.load(this.getClass().getResourceAsStream("fruitcolours.properties"));
  for (Fruit fruit : Fruit.values()) {
    Assert.assertTrue(colours.containsKey(fruit.name()), fruit.name()));
  }
}

Now your blackberry-adding fellow will see, when adding his fruit, that the allFruitsHaveColours() unit test fails with the message “blackberry” -> Level four.

Level three

Previous examples didn’t make it to level three because there’s a correlation between two separate files. To get to level three you need to merge them, which means adding the colours to Fruit.java itself:

public enum Fruit {
  banana, strawberry, blueberry;

  private String colour;

  static {
    banana.colour = "yellow";
    strawberry.colour = "red";
    blueberry.colour = "blue";
  }

  public String getColour() {
    // returns this.colour if it is not null,
    // throws an exception with message this.name otherwise.
    return Preconditions.checkNotNull(this.colour, this.name());
  }
}

The makeColourMap method may now be implemented as follows:

public Map<Fruit,String> makeColourMap() {
  Map<Fruit,String> result = new HashMap<>();
  for (Fruit fruit : Fruit.values()) {
    result.put(fruit, fruit.getColour());
  }
}

That admittedly contrived example achieves level three because a maintainer adding a fruit is likely to notice (and update) the static block underneath, but if he doesn’t, there’s no compiler warnings, and no compiler errors.

Level two

Suppose for some reason you can’t modify Fruit.java, maybe because it is provided by an external library. You can use the fact that the java compiler (with the right options) will tell you when a switch () statement has missing cases:

public Map<Fruit,String> makeColourMap() {
  Map<Fruit,String> result = new HashMap<>();
  for (Fruit fruit : Fruit.values()) {
    result.put(fruit, getFruitColour());
  }
  return result,
}
private String getFruitColour(Fruit fruit) {
  switch (fruit) {
  case banana:
    return "yellow";
  case strawberry:
    return "red";
  case blueberry:
    return "blue";
  }
  throw new IllegalArgumentException(fruit.name());
}

With the right options the compiler (and at least Eclipse, if you’re using that) will complain that the “blackberry” option is missing from that switch. That makes it to level two!
(Note that if you include a default: case you won’t get your warning, so you won’t get your level two. So much for the recommendation “always put a default case in your switches” that shouldn’t be taken too literally.)

Level one

Due to the way the java compiler works we can’t achieve level one without modifying Fruit.java itself (e.g. by having the incomplete-switch warning be an actual error), because Fruit.java could be recompiled into Fruit.class without recompiling the switch.

If we may modify Fruit.java, level one can be achieved quite easily for this example:

public enum Fruit {
  banana("yellow"), strawberry("red"), blueberry("blue");

  public final String colour;

  private Fruit(String colour) {
    this.colour = colour;
  }
}

and

public Map<Fruit,String> makeColourMap() {
  Map<Fruit,String> result = new HashMap<>();
  for (Fruit fruit : Fruit.values()) {
    result.put(fruit, fruit.colour);
  }
  return result,
}

Suppose your maintainer is very distracted and attempts adding a fruit without specifying its colour:

public enum Fruit {
  banana("yellow"), strawberry("red"), blueberry("blue"),
  blackberry;

  public final String colour;

  private Fruit(String colour) {
    this.colour = colour;
  }
}

You get a compiler error: “The constructor Fruit() is undefined”, pointing to “blackberry”.
Level one.