Lecture 16 Enums

Joseph Haugh

University of New Mexico

Free Recall

  • Get out a sheet of paper or open a text editor
  • For 2 minutes write down whatever comes to mind about the last class
    • This could be topics you learned
    • Questions you had
    • Connections you made

Today’s Topics

  • The problem with representing a fixed set of choices using ints or Strings
  • Enums: a type that defines a fixed, named set of values
  • Using enums with switch
  • Adding methods to enums
  • The limits of enums

Motivation: The Integer Approach

  • Suppose you want to represent the state of a traffic light
  • A traffic light can only be red, yellow, or green
  • A first attempt using integers:
// 0 = red, 1 = yellow, 2 = green
String lightMessage(int light) {
    return switch (light) {
        case 0 -> "STOP";
        case 1 -> "SLOW DOWN";
        case 2 -> "GO";
        default -> "invalid!";
    };
}

Motivation: The Integer Approach

void main() {
    IO.println(lightMessage(0)); // STOP
    IO.println(lightMessage(2)); // GO
    IO.println(lightMessage(9)); // invalid!
}
  • The numbers 0, 1, 2 are magic numbers, you have to memorize the mapping
  • Nothing stops lightMessage(9) which is an invalid value accepted at runtime
  • The default case should be unreachable, yet the compiler forces us to write it

Motivation: The String Approach

  • Using strings seems more readable:
String lightMessage(String light) {
    return switch (light) {
        case "red"    -> "STOP";
        case "yellow" -> "SLOW DOWN";
        case "green"  -> "GO";
        default       -> "invalid!";
    };
}

Motivation: The String Approach

void main() {
    IO.println(lightMessage("red"));    // STOP
    IO.println(lightMessage("grren"));  // invalid! typo, no compiler warning
    IO.println(lightMessage("purple")); // invalid! any string accepted
}
  • Strings are more readable, but typos become silent runtime bugs
  • The compiler cannot check that only valid values are passed
  • The default case is still needed

The Root Cause

  • Both problems come from the same mistake:
    • We are using a general-purpose type (int or String) to represent a restricted set of values
  • int has billions of possible values; we only want three
  • String has infinitely many possible values; we only want three
  • Java gives us a better tool: the enum

Introducing Enums

  • An enum (short for enumeration) defines a type with a fixed, named set of constants
  • The compiler knows the complete list, invalid values are impossible
  • Switching on an enum allows us to be exhaustive, no default required
  • Enums are declared with the enum keyword, similar to class

Enum Syntax

enum EnumName {
    CONSTANT_ONE,
    CONSTANT_TWO,
    CONSTANT_THREE
}
  • The constants are written in ALL_CAPS by convention
  • Each constant is a value of the enum type
  • Access a constant with dot notation: EnumName.CONSTANT_ONE

Example 1: Traffic Light

  • Let’s rewrite our traffic light using an enum:
enum TrafficLight {
    RED,
    YELLOW,
    GREEN
}

Example 1: Using the Enum

enum TrafficLight {
    RED,
    YELLOW,
    GREEN
}
void main() {
    TrafficLight light = TrafficLight.RED;
    IO.println(light); // RED

    light = TrafficLight.GREEN;
    IO.println(light); // GREEN
}
  • The type of TrafficLight.RED is TrafficLight
  • IO.println prints the name of the constant automatically

Example 1: Enum with Switch

String lightMessage(TrafficLight light) {
    return switch (light) {
        case RED    -> "STOP";
        case YELLOW -> "SLOW DOWN";
        case GREEN  -> "GO";
    };
}
  • When switching on an enum, use just the constant name, no prefix needed
  • The switch is exhaustive if you have a case for each value of the enum
    • the compiler knows all three cases
  • No default needed and if you add a new constant later, the compiler will flag any switch that does not handle it

Example 1: Putting It Together

void main() {
    TrafficLight light = TrafficLight.GREEN;
    IO.println(lightMessage(light)); // GO

    light = TrafficLight.RED;
    IO.println(lightMessage(light)); // STOP

    // lightMessage(9);        COMPILE ERROR: int is not a TrafficLight
    // lightMessage("red");    COMPILE ERROR: String is not a TrafficLight
    // lightMessage(TrafficLight.PURPLE); COMPILE ERROR: no such constant
}
  • Invalid values are impossible, the compiler catches every mistake

Example 2: Days of the Week

Another common use of enums: a fixed set of named values

enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

Enums Can Have Methods

  • Like classes, enums can have methods
  • this inside a method refers to the current constant
enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

    boolean isWeekend() {
        return switch (this) {
            case SATURDAY, SUNDAY                             -> true;
            case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> false;
        };
    }
}
  • The semicolon after the last constant is required when the enum has methods
  • The switch on this is still exhaustive, no default needed

Example 2: Using Day Methods

void main() {
    Day today = Day.WEDNESDAY;
    IO.println(today.isWeekend()); // false

    Day weekend = Day.SATURDAY;
    IO.println(weekend.isWeekend()); // true
}
  • You call methods on enum values with the same dot notation as objects
  • Each constant “knows” what it is through this, so the method behaves differently per constant

Limitations: Each Constant Must Share the Same Structure

  • Enums can have fields, but all constants must use the same constructor
  • This means every constant holds the same fields
  • What if you wanted a Shape enum where:
    • CIRCLE stores just a radius
    • RECTANGLE stores a width and a height
    • These have different shapes of data, can enums handle this?

Limitations: What You Cannot Do

// This does NOT compile in Java
enum Shape {
    CIRCLE(5.0),          // one value: radius
    RECTANGLE(3.0, 4.0)   // two values: width and height
}
  • An enum has one constructor shared by all constants
  • CIRCLE and RECTANGLE would need different constructors, that is not allowed
  • When your variants need structurally different data, enums are not the right tool

Limitations: What You Can Do

  • If all constants share the same structure, fields work fine:
enum PizzaSize {
    SMALL(8),
    MEDIUM(12),
    LARGE(16);

    private final int diameterInches;

    PizzaSize(int diameterInches) {
        this.diameterInches = diameterInches;
    }
    public int getDiameter() { return diameterInches; }
    
}
  • Every constant stores the same field (diameterInches) with a different value
  • This works because all three constants share the same structure

Limitations: What You Can Do

void main() {
    PizzaSize size = PizzaSize.LARGE;
    IO.println(size.getDiameter()); // 16

    PizzaSize small = PizzaSize.SMALL;
    IO.println(small.getDiameter()); // 8
}

Summary

Enums

  • Define a type with a fixed set of named constants
  • Avoid magic numbers and error-prone strings
  • Switching on an enum is automatically exhaustive
  • Can have methods (using this)
  • Can have fields, but all constants must share the same structure
  • Use ALL_CAPS for constant names by convention

When to Use Enums

Situation Enum?
Fixed set of named choices Yes
All constants share the same data Yes
Constants need different shapes of data No
Arbitrary strings or numbers No