Enums

When writing programmes we often end up with a series of fixed options from which we might want to select a value. These most often appear where the type of data corresponds to a form of category. Consider the Odometer class from the Classes tutorial. If we decided we wanted to customise the ability to display the distance travelled so it could use miles, kilometers or nautical miles (for amphibious vehicles). We would need some way of specifying which units the getTotalDistanceTravelled method would return values for. One option would be to have a string property as follows:

public class Odometer {
    private double totalMilesTravelled;
    private String displayUnits;
    
    public void setDisplayUnits(String displayUnits) {
        this.displayUnits = displayUnits;
    }

    public double getTotalDistanceTravelled() {
        
        switch (displayUnits) {
            case "kilometers": 
                return totalMilesTravelled * 1.60934;
            case "nauticalMiles":
                return totalMilesTravelled * 0.868976;
            default:
                return totalMilesTravelled;
        }
        
    }
    
    //rest of class omitted for brevity
}
An issue arises however, if a developer writes the following code when configuring an instance of odometer:
Odometer carOdo = new Odometer();
carOdo.setDisplayUnits("km"); //this line is wrong, but will not generate an error in the IDE
carOdo.addMileage(5);
double distance = carOdo.getTotalDistanceTravelled(); //will be 5.0 (should be 8.0467)
The reason the code doesn't behave as expected is because the Switch statement in the Odometer class's getTotalDistanceTravelled only looks for "kilometers" and not "km". Worse, the error is likely not to be discovered until runtime.

A String used to determine functionality such as as described above is often referred to as a 'magic string'. Be assured, there's nothing magical about them - and they can be a frustrating source of errors.

Solving the magic string problem - first steps

In order to avoid such errors, we ideally need a simple way of determining a list of acceptable values. A first step to a solution might be defining static variables that represent the acceptable values for display units as follows

  1. Add static, final, variables to the class to represent the values for distance. A static variable is available for use without having to create an instance of the class in which it is declared.
    static final String KILOMETERS = "kilometers";
    static final String MILES = "miles";
    static final String NAUTICAL_MILES = "nauticalMiles";
  2. Change the Switch statement in the class to use the static variable name (this can avoid errors if we change the underlying string value later on)
    switch (displayUnits) {
        case KILOMETERS:
            return totalMilesTravelled * 1.60934;
        case NAUTICAL_MILES:
            return totalMilesTravelled * 0.868976;
        default:
            return totalMilesTravelled;
    }
  3. Use the static variable name when setting the units
    carOdo.setDisplayUnits(Odometer.KILOMETERS);
This removes some sources of potential error, but unfortunately, still doesn't prevent the developer typing in a String instead of using one of the final variables - they could still write the same code as above
carOdo.setDisplayUnits("km");

Using an enum instead

What we need to do is ideally specify the display unit needs to be a certain type. The Enum feature of Java allows us to do that, by adding the following code inside the Odometer class:

public enum DistanceUnit {
        MILES, KILOMETERS, NAUTICAL_MILES;
    }

We would then change the data type of the displayUnit variable to Distance Unit, and modify the setter appropriately - this would give us the following code:

private DistanceUnit displayUnits;
    
public void setDisplayUnits(DistanceUnit displayUnits) {
    this.displayUnits = displayUnits;
}

At this stage, we have limited the values of display unit to those specified in the enum. Next we can amend the Switch statement:

switch (displayUnits) {
    case KILOMETERS:
        return totalMilesTravelled * 1.60934;
    case NAUTICAL_MILES:
        return totalMilesTravelled * 0.868976;
    case MILES:
        return totalMilesTravelled;
     default:
        return 0; // this will never be reached unless the enum is modified
}
Somewhat annoyingly, we cannot remove the default clause (even though the the case statements cover every enum value)

The final change is to modify the programme code that it correctly specifies the distance units for the instance of Odometer as follows:

carOdo.setDisplayUnits(Odometer.DistanceUnit.KILOMETERS);
Because the setDisplayUnits requires a value from the DistanceUnit enum, it is not not possible to make an error as shown in the first and second examples.

Using DistanceUnit elsewhere

In the above example, DistanceUnit was placed inside the Odometer class, however in theory, other classes may use it (for example a class with responsibility for determining the distance between two locations). We can make the DistanceUnit enum available elsewhere in the package by placing it in it's own file with the same name as the enum - in this case the file would be named DistanceUnit.java. Ensure the package matches that of the rest of your code. The code would appear similar to this:

package com.company;
public enum DistanceUnit {
    MILES, KILOMETERS, NAUTICAL_MILES;
}
and the only change would be that when specifying an enum value, the Odometer profix could be excluded as follows:
carOdo.setDisplayUnits(DistanceUnit.KILOMETERS);
This makes the code more reusable, and more readable.

You can download the example project here

Task

Re-visit the calendar task from the Classes Tutorial. Meetings in most calendar programmes result in the user's calendar displaying one of the following states:

Modify your Calendar class, such that you use an Enum to define these possible states for any given meeting.