Guide to Using TypeSafe Enums vs Enumerated Types in Java

Guide to Using TypeSafe Enums vs Enumerated Types in Java
Photo by Susan Holt Simpson / Unsplash

As I have learned in Java, enumerated types (enums) are a special data type that enables a variable to be predefined constants. The goal of this article is to allow people to understand traditional enumerated types and typesafe enums. It will also highlighting their implementation, advantages, and usage in the Java Programming Language. Additionally, I will explain how typesafe enums can be customized by adding data and behaviors, and then take a small look at the java.lang.Enum<E extends Enum<E>> class. This class which serves as the base class for all typesafe enums.

Traditional Enumerated Types

Enumerated types have long been used in programming to define a set of related constants. These are often implemented as sequences of integer constants. For instance, consider a scenario where I will need to define the four cardinal directions (North, South, East, and West). The example is right below.

static final int DIR_NORTH = 0;
static final int DIR_WEST  = 1;
static final int DIR_EAST  = 2;
static final int DIR_SOUTH = 3;

Here are the issues with the traditional enumerated types that I find annoying.

  1. Lack of Type Safety: Enumerated type constants are just integers, so any integer can be specified where the constant is required. This can lead to invalid operations, such as (DIR_NORTH + DIR_EAST) / DIR_SOUTH), which are meaningless and potentially harmful.
  2. No Namespace: Constants must be prefixed with a unique identifier (e.g., DIR_) to avoid naming collisions with other enumerated type constants.
  3. Brittle Code: Constants are compiled into class files where their literal values are stored. Changing a constant’s value requires recompiling all dependent class files, or else undefined behavior will occur at runtime.
  4. Insufficient Information: Printing a constant only outputs its integer value, providing no indication of what the value represents or which enumerated type it belongs to.

What are Typesafe Enums?

For us to be able to overcome the limitations of traditional enumerated types, Java Core Developers have introduced typesafe enums. The Typesafe enums in Java are more robust and provide better type safety, encapsulating the benefits of traditional enums while resolving their issues for the everyday Java Developer.

Let's Define Typesafe Enums

Typesafe enums are defined using the enum keyword. This approach allows for the creation of a special kind of class that can have fields, methods, and implement interfaces. Here's how you can define a typesafe enum for the four cardinal directions right below.

public enum Direction {
    NORTH, WEST, EAST, SOUTH;
}

Let's bring up the advantages of using TypeSafe Enums and why they are recommended for Java Developers to use.

  1. Type Safety: Enums ensure that only valid constants can be assigned to variables of the enum type, preventing invalid values.
  2. Namespace: Enum constants are scoped within the enum type, avoiding naming collisions.
  3. Flexibility: Enums can have additional fields and methods, allowing for more functionality and encapsulation of related behaviors.
  4. Built-in Methods: Enums inherit methods like values(), toString(), and compareTo() from java.lang.Enum.

How to Use Typesafe Enums in Switch Statements

Java's switch statements are also enhanced to support typesafe enums directly and easily. They allowing for more readable and maintainable code. Here's an example of using the Direction enum in a switch statement right below also.

public class TEDemo {
    enum Direction { NORTH, WEST, EAST, SOUTH }

    public static void main(String[] args) {
        for (Direction d : Direction.values()) {
            System.out.println(d);
            switch (d) {
                case NORTH:
                    System.out.println("Move north");
                    break;
                case WEST:
                    System.out.println("Move west");
                    break;
                case EAST:
                    System.out.println("Move east");
                    break;
                case SOUTH:
                    System.out.println("Move south");
                    break;
                default:
                    assert false : "unknown direction";
            }
        }
        System.out.println(Direction.NORTH.compareTo(Direction.SOUTH)); 
        // The output would be `-3`
    }
}

In this example, the switch statement handles each enum constant and prints a corresponding message. The compareTo() method compares the ordinal positions of the constants, indicating their relative order.

How to Customizing Typesafe Enums with Data and Behaviors

Typesafe enums in the Java Programming Language can also include fields and methods. This makes them much more versatile than traditional enumerated types. As an example, consider an enum representing different types of coins, each with a value in pennies. The code example of this is right below.

enum Coin {
    NICKEL(5), DIME(10), QUARTER(25), DOLLAR(100);

    private final int valueInPennies;

    Coin(int valueInPennies) {
        this.valueInPennies = valueInPennies;
    }

    int toCoins(int pennies) {
        return pennies / valueInPennies;
    }
}

public class TEDemo {
    public static void main(String[] args) {
        int pennies = Integer.parseInt(args[0]);
        for (Coin coin : Coin.values()) {
            System.out.println(pennies + " pennies contains " +
                coin.toCoins(pennies) + " " +
                coin.toString().toLowerCase() + "s");
        }
    }
}

In this example right above, the Coin enum has a field to store the value in pennies and a method to calculate the number of coins for a given number of pennies. This demonstrates how enums can encapsulate both data and behavior, enhancing their functionality.

What is the java.lang.Enum<E extends Enum<E>> Class?

All enums in Java implicitly extend java.lang.Enum<E extends Enum<E>> class by default. This is the base class for all typesafe enums. This abstract class provides several useful methods and implements the java.lang.Comparable<T> interface, allowing enums to be compared and sorted.

Listed right below are some of the default Key Methods in emuns.

  • clone(): Prevents constants from being cloned to ensure there is only one copy of each constant.
  • equals(): Compares constants by their references, ensuring that constants with the same identity are equal.
  • finalize(): Ensures that constants cannot be finalized.
  • hashCode(): Overridden to support correct equality checks.
  • toString(): Returns the name of the constant.
  • compareTo(): Compares the current constant with another constant to determine their order.
  • getDeclaringClass(): Returns the class object of the enum.
  • name(): Returns the name of the constant.
  • ordinal(): Returns the zero-based position of the constant in the enum.

How to use the Emun Methods?

Using the money example above in the article, here is some example code showing off how to use some of these methods.

enum Coin {
    PENNY, NICKEL, DIME, QUARTER;

    public static void main(String[] args) {
        Coin penny = Enum.valueOf(Coin.class, "PENNY");
        System.out.println(penny.name()); // Output: PENNY
        System.out.println(penny.ordinal()); // Output: 0
        System.out.println(penny.getDeclaringClass()); // Output: class Coin
    }
}

In the above example, the code shows how to use the name(), ordinal(), and getDeclaringClass() methods to retrieve information about an enum constant.

How to Add Custom Methods to Enums?

In addition to the inherited methods, enums can have custom methods. For example, let's enhance the Direction enum to include a method for getting the opposite direction in the code example right below.

public enum Direction {
    NORTH, SOUTH, EAST, WEST;

    public Direction getOpposite() {
        switch (this) {
            case NORTH: return SOUTH;
            case SOUTH: return NORTH;
            case EAST: return WEST;
            case WEST: return EAST;
            default: throw new AssertionError("Unknown direction: " + this);
        }
    }
}

public class DirectionDemo {
    public static void main(String[] args) {
        Direction dir = Direction.NORTH;
        System.out.println("Opposite of " + dir + " is " + dir.getOpposite()); // Output: Opposite of NORTH is SOUTH
    }
}

In the upper example, the Direction enum includes a getOpposite() method that returns the opposite direction. This demonstrates how enums can encapsulate complex logic and behaviors, making them more powerful and flexible.

Enum Constants with Fields and Methods

Enums can also have fields and methods tailored to each constant. For example, consider an enum representing different planets, each with a mass and radius. The code example for this is right below.

public enum Planet {
    MERCURY(3.303e+23, 2.4397e6),
    VENUS(4.869e+24, 6.0518e6),
    EARTH(5.976e+24, 6.37814e6),
    MARS(6.421e+23, 3.3972e6),
    JUPITER(1.9e+27, 7.1492e7),
    SATURN(5.688e+26, 6.0268e7),
    URANUS(8.686e+25, 2.5559e7),
    NEPTUNE(1.024e+26, 2.4746e7);

    private final double mass; // in kilograms
    private final double radius; // in meters

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }

    public double getMass() {
        return mass;
    }

    public double getRadius() {
        return radius;
    }

    public double surfaceGravity() {
        final double G = 6.67300E-11; 
        // universal gravitational constant (m^3 kg^-1 s^-2)
        
        return G * mass / (radius * radius);
    }

    public double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }
}

public class PlanetDemo {
    public static void main(String[] args) {
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight / Planet.EARTH.surfaceGravity();
        for (Planet p : Planet.values()) {
            System.out.printf("Your weight on %s is %f%n", p, p.surfaceWeight(mass));
        }
    }
}

In this example right above, the Planet enum defines constants for each planet in the solar system, each with a mass and radius. The surfaceGravity() and surfaceWeight() methods calculate the gravitational force and weight on each planet, demonstrating how enums can encapsulate complex scientific calculations.

Enum Implementing Interfaces

Enums can also implement interfaces, allowing them to be used polymorphically. For example, consider an enum implementing a DirectionInterface.

interface DirectionInterface {
    void showDirection();
}

public enum Direction implements DirectionInterface {
    NORTH {
        public void showDirection() {
            System.out.println("Heading North");
        }
    },
    SOUTH {
        public void showDirection() {
            System.out.println("Heading South");
        }
    },
    EAST {
        public void showDirection() {
            System.out.println("Heading East");
        }
    },
    WEST {
        public void showDirection() {
            System.out.println("Heading West");
        }
    }
}

public class DirectionDemo {
    public static void main(String[] args) {
        Direction dir = Direction.NORTH;
        dir.showDirection();
        // The output is going to be Heading North
    }
}

In this example, each constant in the Direction enum implements the showDirection() method from the DirectionInterface. This allows for polymorphic behavior and enhances the flexibility of enums.

Enum Singleton Pattern

Enums can also be used to implement the Singleton pattern, ensuring that only one instance of the enum exists. This is a common design pattern for ensuring a class has only one instance and provides a global point of access to it. Here’s an example below.

public enum Singleton {
    INSTANCE;

    public void showMessage() {
        System.out.println("Hello from Singleton");
    }
}

public class SingletonDemo {
    public static void main(String[] args) {
        Singleton singleton = Singleton.INSTANCE;
        singleton.showMessage(); // Output: Hello from Singleton
    }
}

In this example, the Singleton enum has a single instance, INSTANCE, and a method showMessage(). This demonstrates how enums can be used to implement the Singleton pattern in a concise and thread-safe manner.

Conclusion

Typesafe enums in Java offer a robust and flexible alternative to traditional enumerated types. By encapsulating values and behaviors within a class-like structure, enums provide type safety, meaningful values, and additional functionalities. They overcome the limitations of traditional enumerated types by ensuring only valid constants are used, providing a namespace to avoid naming collisions, and supporting additional fields and methods.

Through various examples, we've seen how typesafe enums can be used in switch statements, customized with additional data and behaviors, and even implement interfaces and design patterns like Singleton. The java.lang.Enum<E extends Enum<E>> class serves as the foundation for all enums, providing essential methods for comparison, retrieval, and manipulation.

Overall, typesafe enums enhance the readability, maintainability, and robustness of Java code, making them a powerful feature for developers to leverage in their applications.