Feeds:
Posts
Comments

Posts Tagged ‘Enums’

Enums are Brittle

We all know Integer constants are brittle

Consider this example from StackOverflow today:-

Calendar date = Calendar.getInstance();
date.set(2010, 03, 7);
if(date.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY)
        System.out.println("OK");

The questioner mistakenly thought that the java.util.Calendar set(int year, int month, int date) method followed a sensible human format for representing dates, but in fact the API makes use of zero based int constants to represent the month. The questioner was encouraged to avoid this pitfall by changing his code to date.set(2010, Calendar.MARCH, 7). This of course is better, but the root of the problem lies with the poorly designed Calendar API.

If java.util.Calendar class has been written post Java 5 then the set method could have been defined as set(int year, Month month, int date) where Month is an enum type:-

public eum Month {
JANUARY,
FEBRUARY,
MARCH,
...
}

This would prevent any ambiguity over whether the month parameter is zero or one based.

To change and to change for the better are two different things.

Enums may be preferable to int constants, but they are not as flexible as regular Java classes. You may well find that when you come to modify an enum which was written months ago, you need to refactor the whole thing into a regular class. This of course is time-consuming and you would have been better off designing a class from the start.

Below is an example enum representing a 2D direction on a grid. Direction takes two parameters, deltaX and deltaY which represent the offset of moving in that direction. Given that co-ordinate (0, 0) on the grid is located in the top left hand corner, moving NORTH will result in a negative change in deltaY, EAST will result in a positive change in deltaX and so forth.

enum Direction {
	NORTH(0, -1), NORTH_EAST(1, -1), EAST(1, 0), 
        SOUTH_EAST(1, 1), SOUTH(0, 1), SOUTH_WEST(-1, 1), 
        WEST(-1, 0), NORTH_WEST(-1, -1);
	private int deltaX;
	private int deltaY;
	Direction(int deltaX, int deltaY) {
		this.deltaX = deltaX;
		this.deltaY = deltaY;
	}
	public int deltaX() {
		return deltaX;
	}
	public int deltaY() {
		return deltaY;
	}
}

Direction feels like it should be an enum, as we only have eight possible directions to move in, allowing for diagonal moves. Furthermore this example demonstrates another advantage enums have over int constants – enums support methods. In fact, this enum almost appears to be a regular class, besides the enum declarations at the top.

We encounter a problem with enums when we want to add a method that returns the opposite direction. i.e. the opposite of NORTH is SOUTH, NORTH_WEST is SOUTH_EAST and so on. Here is the first technique.

enum Direction {
	NORTH(0, -1), NORTH_EAST(1, -1), EAST(1, 0), 
        SOUTH_EAST(1, 1), SOUTH(0, 1), SOUTH_WEST(-1, 1),
	WEST(-1, 0), NORTH_WEST(-1, -1);
	private int deltaX;
	private int deltaY;
	Direction(int deltaX, int deltaY) {
		this.deltaX = deltaX;
		this.deltaY = deltaY;
	}
	public int deltaX() {
		return deltaX;
	}
	public int deltaY() {
		return deltaY;
	}
	public Direction getOpposite() {
		return new Direction(-deltaX, -deltaY);
	}
}

By simply negating the deltaX and deltaY values we get the opposite direction right? It doesn’t compile because we can’t invoke enum constructors directly, even from within the enum itself! Let’s try it another way: –

enum Direction {
	NORTH(0, -1, SOUTH), NORTH_EAST(1, -1, SOUTH_WEST), 
        EAST(1, 0, WEST), SOUTH_EAST(1, 1, NORTH_WEST),
	SOUTH(0, 1, NORTH), SOUTH_WEST(-1, 1, NORTH_EAST), 
        WEST(-1, 0, EAST), NORTH_WEST(-1, -1, SOUTH_EAST);
	private int deltaX;
	private int deltaY;
	private Direction opposite;
	Direction(int deltaX, int deltaY, Direction opposite) {
		this.deltaX = deltaX;
		this.deltaY = deltaY;
		this.opposite = opposite;
	}
	public int deltaX() {
		return deltaX;
	}
	public int deltaY() {
		return deltaY;
	}
	public Direction getOpposite() {
		return opposite;
	}
}

In the second technique we have hard-coded the opposite directions, passing them into the constructor. This doesn’t compiler either because you cannot refer to types before they are defined. e.g. SOUTH cannot be referenced in NORTH‘s constructor.

Finally we give up trying to find a work around with an enum and change the whole thing to be a regular class.

public class Direction {
	public static final Direction NORTH = 
        new Direction(0, -1);
	public static final Direction NORTH_EAST = 
        new Direction(1, -1);
	public static final Direction EAST = 
        new Direction(1, 0);
	public static final Direction SOUTH_EAST = 
        new Direction(1, 11);
	public static final Direction SOUTH = 
        new Direction(0, 1);
	public static final Direction SOUTH_WEST = 
        new Direction(-1, 1);
	public static final Direction WEST = 
        new Direction(-1, 0);
	public static final Direction NORTH_NORTH = 
        new Direction(-1, -1);
	private int deltaX;
	private int deltaY;
	private Direction(int deltaX, int deltaY) {
		this.deltaX = deltaX;
		this.deltaY = deltaY;
	}
	public int deltaX() {
		return deltaX;
	}
	public int deltaY() {
		return deltaY;
	}
	public Direction getOpposite() {
		return new Direction(-deltaX, -deltaY);
	}
}

The private constructor ensures that we cannot create any instances outside of the class. The eight public instances and the getOpposite() method provide the only means of access. Note that in this class we can write the getOpposite() method using the first technique.

In conclusion, enums may be an improvement over int constants, but are definitely less flexible than a regular class. If you do decide to create an enum, consider that it may outgrow itself, like Direction did. Creating a regular class from the start may be the better option. In some cases a class may be more verbose than an enum, but then Java is pretty verbose language anyway!

Advertisements

Read Full Post »