> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Use Java Enums with Fields and Methods (Java 21)
- URL: https://www.ggorantala.dev/how-to-use-java-enums-fields-methods/
- Published: 2026-09-23T05:17:00.000Z
- Updated: 2026-10-31T06:32:12.000Z
- Description: Build a library loan desk in plain Java 21: an enum with fields, a lookup by code, per-constant behaviour, and a small state machine, all checked by 36 tests.
- Author: Gopi Gorantala
- Tags: how-to-tutorials, java-tutorials, core-java

## What the desk prints when you're done

This is the output of the program you're about to write. A library charges late fees, and the amount depends on two things: who borrowed the item (their membership tier) and what it is (a book or a DVD). Both of those are enums.

```log
TIER       ITEM       DUE          LATE FEE
BASIC      BOOK       2026-03-16   $8.75   
BASIC      DVD        2026-03-16   $17.50  
STANDARD   BOOK       2026-03-23   $5.60   
STANDARD   DVD        2026-03-23   $11.20  
PREMIUM    BOOK       2026-04-13   $0.00   
PREMIUM    DVD        2026-04-13   $0.00   

code 'S' -> STANDARD
code 'X' -> Optional.empty
ON_LOAN -> OVERDUE allowed? true
LOST -> AVAILABLE allowed? false

```

There's no `if (tier == BASIC)` chain anywhere in the code that produced it. Loan length, loan limits and fine rates live on the tier itself, and the fee formula lives on the item type. That's the point of a Java enum with fields and methods: each constant carries its own data and, when it needs to, its own behaviour. You'll build exactly that, with a lookup by short code and a small state machine, and then break it on purpose seven ways so you recognise the compiler messages when they show up in your own code.

You should already be comfortable with classes, `static`, and `switch`. You don't need to know anything about enums beyond "a fixed list of constants". If you want the bare-bones introduction first, there's [an intro to Java enums](https://www.ggorantala.dev/introduction-to-java-enums/) on this site. This article starts where that one stops.

I'll use a library catalogue as the example domain. The build took me about an hour, and most of that went on the compile errors in the second-to-last section, because I wanted the exact messages rather than the ones I remembered.

## Versions I used

| Tool       | Version I used                                                        | Check it with  |
| ---------- | --------------------------------------------------------------------- | -------------- |
| JDK        | OpenJDK 21.0.12.1 (Ubuntu build)                                      | java -version  |
| javac      | 21.0.12.1                                                             | javac -version |
| Build tool | none; plain javac                                                     |                |
| IDE        | IntelliJ IDEA or Eclipse (steps from the vendors' docs, not executed) | Help → About   |

Enums have worked this way since Java 5\. I also compiled everything with `javac --release 17` and it went through, so Java 17 readers are fine. No Maven, no Gradle, no container: the whole project is five files.

## Set up the library desk project

Make a folder with this layout. The package name is `com.example.library`; change it if you like, but keep the folder structure in step with it.

```log
enums/
└── src/com/example/library/
    ├── MembershipTier.java
    ├── MediaType.java
    ├── LoanStatus.java
    ├── LoanDesk.java
    ├── Main.java
    └── Checks.java

```

Compile and run from the `enums` folder (not from inside `src`; I did that wrong on my first attempt and got `file not found`):

```bash
mkdir -p out
javac -Xlint:all -d out src/com/example/library/*.java
java -cp out com.example.library.Main

```

On Windows PowerShell, replace the glob with `(Get-ChildItem src -Recurse -Filter *.java).FullName`.

**In IntelliJ IDEA:** File → New → Project, choose "New Project", language Java, build system IntelliJ, JDK 21\. Create the package under `src` and paste the files in. Right-click `Main` and choose Run. **In Eclipse:** File → New → Java Project, set the execution environment to JavaSE-21, then create the package and the files. Run As → Java Application on `Main`. I didn't run either IDE for this article; the menu names come from the vendors' current documentation.

If you'd rather use Maven or Gradle, put the files under `src/main/java/com/example/library/` and any standard Java 21 build file will do. Nothing here needs a dependency.

## How to use Java enums with fields and methods, step by step

### Step 1: Give each constant its own data

**Goal:** model the membership tiers with a loan length, a loan limit and a fine rate attached to each constant.

`src/com/example/library/MembershipTier.java`

```java
package com.example.library;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public enum MembershipTier {
    BASIC("B", 14, 3, 25),
    STANDARD("S", 21, 8, 20),
    PREMIUM("P", 42, 20, 0);

    private static final Map<String, MembershipTier> BY_CODE = new HashMap<>();

    static {
        for (MembershipTier tier : values()) {
            BY_CODE.put(tier.code, tier);
        }
    }

    private final String code;
    private final int loanDays;
    private final int maxLoans;
    private final int finePerDayCents;

    MembershipTier(String code, int loanDays, int maxLoans, int finePerDayCents) {
        this.code = code;
        this.loanDays = loanDays;
        this.maxLoans = maxLoans;
        this.finePerDayCents = finePerDayCents;
    }

    public String code()          { return code; }
    public int loanDays()         { return loanDays; }
    public int maxLoans()         { return maxLoans; }
    public int finePerDayCents()  { return finePerDayCents; }

    public static Optional<MembershipTier> fromCode(String code) {
        return Optional.ofNullable(BY_CODE.get(code));
    }
}

```

Four things are worth looking at here.

The constants (`BASIC("B", 14, 3, 25)`) are calls to the constructor. The list of constants comes first and ends with a semicolon, and only then do the fields and methods start. Forget that semicolon and you get a confusing error pointing at the next line.

The constructor has no `public` or `private` in front of it. It's implicitly private, and you can't change that, which is how Java guarantees nobody creates a fourth tier at runtime.

Fine rates are `int` cents, not `double` dollars. Money in floating point goes wrong in ways that show up on customers' invoices; I cover that in [the BigDecimal article](https://ggorantala.dev/how-to-use-bigdecimal-financial-calculations-java/?ref=ggorantala.dev). For a whole-cents value an `int` is enough.

The fields are `final`. An enum constant is shared by the whole JVM. If one field were mutable, changing it in one place would change it everywhere.

**Checkpoint:** compile what you have so far.

```bash
javac -d out src/com/example/library/MembershipTier.java

```

No output means it compiled.

### Step 2: Look a constant up by its short code

**Goal:** turn the `"S"` stored in a database column or a form back into `STANDARD`, without a loop at every call site.

This is already in the file above, in the `BY_CODE` map and the static block that fills it. Look at the order of the declarations: the map is declared **before** the static block that uses it. That ordering isn't style. Move the field below the block and `javac` refuses:

```log
MembershipTier.java:14: error: illegal forward reference
            BY_CODE.put(tier.code, tier);
            ^

```

Also note why I fill the map in a static block and not in the constructor. The constructor runs once per constant, while the class is still being initialised, and Java doesn't allow it to touch static fields. You'll see that error in the troubleshooting section.

`fromCode` returns an `Optional`, so a caller who passes `"X"` can't forget to handle the miss. The built-in `valueOf(String)` is different: it takes the constant's *name* (`"STANDARD"`), is case-sensitive, and throws `IllegalArgumentException` when nothing matches. I'd keep `valueOf` for names you control and use your own lookup for anything coming from outside. If you want the Optional-chaining style used here, see [how to use Optional](https://ggorantala.dev/how-to-use-optional-java/?ref=ggorantala.dev).

### Step 3: Give each constant its own behaviour

**Goal:** let each media type calculate its own late fee, so the fee logic can't drift away from the type.

`src/com/example/library/MediaType.java`

```java
package com.example.library;

public enum MediaType {
    BOOK("BK", true) {
        @Override
        public int lateFeeCents(int daysLate, MembershipTier tier) {
            return Math.min(daysLate * tier.finePerDayCents(), 2_000);
        }
    },
    DVD("DV", true) {
        @Override
        public int lateFeeCents(int daysLate, MembershipTier tier) {
            return Math.min(daysLate * tier.finePerDayCents() * 2, 3_000);
        }
    },
    REFERENCE("RF", false) {
        @Override
        public int lateFeeCents(int daysLate, MembershipTier tier) {
            throw new IllegalStateException("Reference items never leave the building");
        }
    };

    private final String prefix;
    private final boolean loanable;

    MediaType(String prefix, boolean loanable) {
        this.prefix = prefix;
        this.loanable = loanable;
    }

    public String prefix()     { return prefix; }
    public boolean loanable()  { return loanable; }

    public abstract int lateFeeCents(int daysLate, MembershipTier tier);
}

```

`lateFeeCents` is declared `abstract` on the enum, and every constant supplies its body inside braces. That's called a *constant-specific class body*. Behind the scenes each of those constants becomes its own anonymous subclass of `MediaType`. The compiler enforces this: delete the `REFERENCE` body and it tells you the enum is abstract and can't be instantiated.

A DVD costs double per day and has a higher cap; a book is capped at $20.00\. `REFERENCE` shows the other option for a constant that has no sensible answer: throw. I like this better than returning 0, because a silent zero would hide a bug in the caller.

The `loanable` field is separate on purpose. A flag on the constant is cheaper than overriding a method when the answer is plain data.

**Checkpoint:**

```bash
javac -d out src/com/example/library/MembershipTier.java src/com/example/library/MediaType.java

```

### Step 4: Model allowed state changes with EnumMap and EnumSet

**Goal:** record which loan status can follow which, so `LOST` can never become `AVAILABLE`.

`src/com/example/library/LoanStatus.java`

```java
package com.example.library;

import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;

public enum LoanStatus {
    AVAILABLE, ON_LOAN, OVERDUE, LOST;

    private static final Map<LoanStatus, Set<LoanStatus>> NEXT = new EnumMap<>(LoanStatus.class);

    static {
        NEXT.put(AVAILABLE, EnumSet.of(ON_LOAN, LOST));
        NEXT.put(ON_LOAN,   EnumSet.of(AVAILABLE, OVERDUE, LOST));
        NEXT.put(OVERDUE,   EnumSet.of(AVAILABLE, LOST));
        NEXT.put(LOST,      EnumSet.noneOf(LoanStatus.class));
    }

    public boolean canMoveTo(LoanStatus target) {
        return NEXT.get(this).contains(target);
    }
}

```

Here the constants have no fields at all. The transitions live in a map, and the map lives *outside* the constants for a reason: you can't pass `ON_LOAN` to `AVAILABLE`'s constructor, because it hasn't been created yet. Passing later constants into an earlier one's constructor (`AVAILABLE(ON_LOAN, LOST)`) fails with `illegal forward reference`, which I reproduced in the errors section. Building the table in a static block after every constant exists avoids the problem.

`EnumMap` and `EnumSet` are the collections made for enum keys. Internally they're an array indexed by ordinal and a bit set, so they're compact and iterate in declaration order. They're worth reaching for any time the key is an enum.

### Step 5: Use the enums from ordinary code

**Goal:** a small class that does the date and fee arithmetic, plus a `main` that prints the table at the top of the article.

`src/com/example/library/LoanDesk.java`

```java
package com.example.library;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

public final class LoanDesk {

    private LoanDesk() { }

    public static LocalDate dueDate(LocalDate borrowed, MembershipTier tier) {
        return borrowed.plusDays(tier.loanDays());
    }

    public static int lateFeeCents(MediaType type, MembershipTier tier,
                                   LocalDate due, LocalDate returned) {
        long late = ChronoUnit.DAYS.between(due, returned);
        if (late <= 0) {
            return 0;
        }
        return type.lateFeeCents((int) late, tier);
    }

    public static String money(int cents) {
        return String.format("$%d.%02d", cents / 100, cents % 100);
    }
}

```

`src/com/example/library/Main.java`

```java
package com.example.library;

import java.time.LocalDate;

public class Main {
    public static void main(String[] args) {
        LocalDate borrowed = LocalDate.of(2026, 3, 2);
        LocalDate returned = LocalDate.of(2026, 4, 20);

        System.out.printf("%-10s %-10s %-12s %-8s%n", "TIER", "ITEM", "DUE", "LATE FEE");
        for (MembershipTier tier : MembershipTier.values()) {
            LocalDate due = LoanDesk.dueDate(borrowed, tier);
            for (MediaType type : MediaType.values()) {
                if (!type.loanable()) {
                    continue;
                }
                int fee = LoanDesk.lateFeeCents(type, tier, due, returned);
                System.out.printf("%-10s %-10s %-12s %-8s%n",
                        tier, type, due, LoanDesk.money(fee));
            }
        }
        System.out.println();
        System.out.println("code 'S' -> " + MembershipTier.fromCode("S").orElseThrow());
        System.out.println("code 'X' -> " + MembershipTier.fromCode("X"));
        System.out.println("ON_LOAN -> OVERDUE allowed? " + LoanStatus.ON_LOAN.canMoveTo(LoanStatus.OVERDUE));
        System.out.println("LOST -> AVAILABLE allowed? " + LoanStatus.LOST.canMoveTo(LoanStatus.AVAILABLE));
    }
}

```

`LoanDesk` never asks "which tier?" or "which type?". It asks the enum. If someone adds a `STUDENT` tier next month, `LoanDesk` doesn't change. For the date arithmetic see [LocalDate and ZonedDateTime](https://ggorantala.dev/how-to-work-localdate-localdatetime-zoneddatetime-java/?ref=ggorantala.dev).

The loop uses `values()`, which returns every constant in declaration order. Each call hands back a fresh copy of the internal array, so don't call it inside a hot loop; keep a reference or use `EnumSet.allOf`.

## Run it and read the table

```bash
javac -Xlint:all -d out src/com/example/library/*.java
java -cp out com.example.library.Main

```

You should see the table from the top of the article and nothing from `-Xlint`. Check one row by hand. A BASIC member borrowed on 2 March gets 14 days, so the due date is 16 March. They return on 20 April, which is 35 days late. At 25 cents a day that's $8.75 for a book, under the $20.00 cap. A DVD doubles the daily rate for $17.50\. The STANDARD book is due 23 March, so 28 days late at 20 cents: $5.60\. PREMIUM has a rate of 0, so everything is $0.00.

If those numbers match, the hard part's done.

## Pin it down with 36 checks

I didn't have JUnit available offline when I built this, so `Checks` is a plain Java class that prints PASS or FAIL. It's a quick way to see what is guaranteed; for a real project, convert each `check(...)` into an `assertThat(...)` in a JUnit 5 test (there's a JUnit 5 tutorial planned for this site: [how to write unit tests with JUnit 5](https://ggorantala.dev/how-to-write-unit-tests-junit-5-java/?ref=ggorantala.dev)).

`src/com/example/library/Checks.java`

```java
package com.example.library;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;

public class Checks {
    static int passed, failed;

    static void check(String name, boolean ok) {
        if (ok) { passed++; System.out.println("PASS " + name); }
        else    { failed++; System.out.println("FAIL " + name); }
    }

    static boolean throwsType(Class<? extends Throwable> t, Runnable r) {
        try { r.run(); return false; } catch (Throwable e) { return t.isInstance(e); }
    }

    public static void main(String[] args) {
        // constants and fields
        check("three tiers", MembershipTier.values().length == 3);
        check("BASIC loan days", MembershipTier.BASIC.loanDays() == 14);
        check("PREMIUM fine is zero", MembershipTier.PREMIUM.finePerDayCents() == 0);
        check("ordinal follows declaration order", MembershipTier.STANDARD.ordinal() == 1);
        check("name() matches identifier", MembershipTier.PREMIUM.name().equals("PREMIUM"));

        // lookup
        check("fromCode S", MembershipTier.fromCode("S").orElseThrow() == MembershipTier.STANDARD);
        check("fromCode unknown is empty", MembershipTier.fromCode("X").isEmpty());
        check("fromCode null is empty", MembershipTier.fromCode(null).isEmpty());
        check("valueOf unknown throws IAE",
                throwsType(IllegalArgumentException.class, () -> MembershipTier.valueOf("GOLD")));
        check("valueOf is case sensitive",
                throwsType(IllegalArgumentException.class, () -> MembershipTier.valueOf("basic")));
        check("valueOf null throws NPE",
                throwsType(NullPointerException.class, () -> MembershipTier.valueOf(null)));
        check("every tier code round-trips",
                Arrays.stream(MembershipTier.values())
                        .allMatch(t -> MembershipTier.fromCode(t.code()).orElseThrow() == t));

        // values() is a fresh copy
        check("values() returns a new array each call",
                MembershipTier.values() != MembershipTier.values());
        MembershipTier[] copy = MembershipTier.values();
        copy[0] = null;
        check("mutating the copy leaves the enum alone", MembershipTier.values()[0] == MembershipTier.BASIC);

        // abstract behaviour
        MembershipTier basic = MembershipTier.BASIC;
        check("BOOK 3 days late, BASIC = 75c", MediaType.BOOK.lateFeeCents(3, basic) == 75);
        check("DVD 3 days late, BASIC = 150c", MediaType.DVD.lateFeeCents(3, basic) == 150);
        check("BOOK fee capped at 2000", MediaType.BOOK.lateFeeCents(500, basic) == 2_000);
        check("DVD fee capped at 3000", MediaType.DVD.lateFeeCents(500, basic) == 3_000);
        check("PREMIUM pays nothing", MediaType.BOOK.lateFeeCents(30, MembershipTier.PREMIUM) == 0);
        check("REFERENCE refuses fees",
                throwsType(IllegalStateException.class,
                        () -> MediaType.REFERENCE.lateFeeCents(1, basic)));
        check("constant bodies are subclasses",
                MediaType.BOOK.getClass() != MediaType.class);
        check("getDeclaringClass is the enum",
                MediaType.BOOK.getDeclaringClass() == MediaType.class);
        check("REFERENCE not loanable", !MediaType.REFERENCE.loanable());

        // desk
        LocalDate borrowed = LocalDate.of(2026, 3, 2);
        LocalDate due = LoanDesk.dueDate(borrowed, basic);
        check("due date BASIC", due.equals(LocalDate.of(2026, 3, 16)));
        check("returned on due date = no fee",
                LoanDesk.lateFeeCents(MediaType.BOOK, basic, due, due) == 0);
        check("returned early = no fee",
                LoanDesk.lateFeeCents(MediaType.BOOK, basic, due, due.minusDays(5)) == 0);
        check("returned 2 days late",
                LoanDesk.lateFeeCents(MediaType.BOOK, basic, due, due.plusDays(2)) == 50);
        check("money format", LoanDesk.money(2005).equals("$20.05"));
        check("money format cents pad", LoanDesk.money(5).equals("$0.05"));

        // state machine
        check("AVAILABLE -> ON_LOAN", LoanStatus.AVAILABLE.canMoveTo(LoanStatus.ON_LOAN));
        check("AVAILABLE -> OVERDUE not allowed", !LoanStatus.AVAILABLE.canMoveTo(LoanStatus.OVERDUE));
        check("LOST is terminal", Arrays.stream(LoanStatus.values())
                .noneMatch(LoanStatus.LOST::canMoveTo));

        // EnumMap / EnumSet / compareTo / ==
        Map<MembershipTier, Integer> members = new EnumMap<>(MembershipTier.class);
        members.put(MembershipTier.PREMIUM, 2);
        members.put(MembershipTier.BASIC, 9);
        check("EnumMap iterates in declaration order",
                members.keySet().iterator().next() == MembershipTier.BASIC);
        check("EnumSet.range", EnumSet.range(MembershipTier.BASIC, MembershipTier.STANDARD).size() == 2);
        check("compareTo uses ordinal", MembershipTier.BASIC.compareTo(MembershipTier.PREMIUM) < 0);
        check("== and equals agree", MembershipTier.valueOf("BASIC") == MembershipTier.BASIC);

        System.out.println();
        System.out.println(passed + " passed, " + failed + " failed");
        if (failed > 0) System.exit(1);
    }
}

```

```bash
java -cp out com.example.library.Checks

```

```log
PASS three tiers
PASS BASIC loan days
[... lines cut]
PASS compareTo uses ordinal
PASS == and equals agree

36 passed, 0 failed

```

Two of these checks are lessons in disguise. `values() returns a new array each call` proves the defensive copy, and `mutating the copy leaves the enum alone` shows you can't corrupt the constants by writing to that array. And `constant bodies are subclasses` is where this prints:

```log
MediaType.BOOK.getClass() = class com.example.library.MediaType$1
MediaType.BOOK.getDeclaringClass() = class com.example.library.MediaType

```

If you ever write `if (x.getClass() == MediaType.class)` it will be false for `BOOK`. Use `getDeclaringClass()`, or better, compare constants with `==`.

## What javac builds from an enum

An enum is a class. `javac` turns `MembershipTier` into a `final` class that extends `java.lang.Enum<MembershipTier>`, with one `public static final` field per constant, and adds two methods for you: `values()` and `valueOf(String)`. The private constructor is called once per constant, in declaration order, when the class is first loaded. That's why constant identity is safe: there's exactly one `STANDARD` in the JVM, so `==` works and is the right comparison. It's null-safe too, where `.equals` isn't.

Constants with a body (`MediaType.BOOK`) become anonymous subclasses, and the enum itself becomes `abstract` as soon as it has an abstract method. Those are the `$1`, `$2` class files you'll see in `out/`.

Initialisation order is the source of every odd enum error. The constants are built first, then static fields and static blocks run in textual order. That is why a constructor can't read a static field (the field isn't ready), and why the lookup map has to be filled from a static block placed after it is declared.

Every enum also gets `ordinal()`, the position in the declaration. I don't use it for anything that is stored. Here's why:

I saved `STANDARD.ordinal()` (1), then recompiled the enum with a new `STUDENT` constant inserted before it, and looked the saved number up again:

```log
saved ordinal = 1 -> STANDARD
saved ordinal 1 -> STUDENT

```

The stored `1` meant `STANDARD` yesterday and means `STUDENT` after a perfectly reasonable edit. Store the `code` field (or the name) and look it up with `fromCode`. Same thing for `compareTo`: it compares ordinals, so it follows declaration order, which is fine for display and wrong as a business rule.

## Errors I forced, and what they mean

Each of these comes from my own `javac` 21.0.12.1 run, with the smallest change that triggers it.

**You touch a static field from the constructor.**

```log
MembershipTier.java:30: error: illegal reference to static field from initializer
        BY_CODE.put(code, this);
        ^

```

Constructors run while the class is still initialising. Fill static maps from a static block, as in Step 1.

**You call `new` on an enum.**

```log
Bad2.java:2: error: enum classes may not be instantiated

```

By design. The constants you declared are the only instances.

**You extend an enum.**

```log
Bad3.java:2: error: cannot inherit from final MembershipTier
Bad3.java:2: error: enum classes are not extensible

```

If you need extra variants, add constants, or have the enum implement an interface and write a second implementation of that interface.

**You leave out an abstract method body.**

```log
MediaType.java:16: error: MediaType is abstract; cannot be instantiated
    REFERENCE("RF", false);
             ^

```

The message is misleading until you realise the enum became abstract when you declared the method. Add the body to the constant.

**You reference a later constant in a constructor argument.**

```log
Fwd.java:2: error: illegal forward reference
enum Fwd { A(B), B(A); ... }
             ^

```

Build such relationships after all constants exist, as in `LoanStatus`.

**A `switch` expression doesn't cover every constant.**

```log
Sw.java:2: error: the switch expression does not cover all possible input values

```

This is a gift. When you later add a tier, every enum switch without a `default` stops compiling until it handles the new one. I wouldn't add `default` to those switches. [The switch-expressions tutorial](https://ggorantala.dev/how-to-use-switch-expressions-pattern-matching-java/?ref=ggorantala.dev) shows the pattern.

**You write `public` on a constructor.**

```log
Pub.java:2: error: modifier public not allowed here

```

One runtime failure to know too:

```log
Exception in thread "main" java.lang.IllegalArgumentException: No enum constant com.example.library.MembershipTier.GOLD
	at java.base/java.lang.Enum.valueOf(Enum.java:293)

```

That's `valueOf("GOLD")` on a name that doesn't exist. If you're parsing user input, go through `fromCode`.

## Rules I'd put in a code review

Compare with `==`. Keep fields `final`. Persist the code, never the ordinal. Don't add a `default` to a switch over your own enum. Keep `values()` out of tight loops, and cache an `EnumSet` or a `List` if you need the same set often.

If an enum grows past a few hundred lines or starts reading from a database, it has stopped being an enum; the data belongs in a table and a class. Enums are for sets that change when the code changes, not when the business does. Tier rates that Finance edits every quarter shouldn't be compiled into your JAR.

One thing I'd do next in a real system is put `lateFeeCents` behind an interface, so each library branch can plug in its own rule. That's the [strategy pattern](https://ggorantala.dev/how-to-implement-strategy-pattern-java/?ref=ggorantala.dev), and enums are a fine place to keep the default strategies.

## Checklist

- Constants listed first, ending in a semicolon; fields `final`; constructor implicit private.
- Lookup by code through a static map filled in a static block placed after the field.
- Abstract method on the enum when behaviour differs per constant; a plain field when only data does.
- `EnumMap` and `EnumSet` for enum-keyed collections.
- Stored value is the code, not the ordinal.
- Exhaustive switches without `default`.

## Enum questions people search for

**Can a Java enum have fields and methods?** Yes. Declare the fields, a private constructor, and pass values in each constant's parentheses (`BASIC("B", 14, 3, 25)`). Methods work like on any class, including `static` ones.

**Can an enum have a constructor?** Yes, but it's always private. You can't write `public`, and you can't call `new` on an enum.

**How do I get an enum constant from a string or a code?** `valueOf("STANDARD")` matches the exact name and throws if there is no match. For anything else, build a static map as in Step 2 and return an `Optional`.

**Can an enum implement an interface or extend a class?** It can implement interfaces. It can't extend a class, because it already extends `java.lang.Enum`.

**Should I use ordinal()?** Not for anything that is stored or sent over the wire. See the section on what javac builds.

## References

- [Java Language Specification, Java SE 21, section 8.9: Enum Classes](https://docs.oracle.com/javase/specs/jls/se21/html/jls-8.html?ref=ggorantala.dev#jls-8.9)
- [java.lang.Enum, Java SE 21 API](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Enum.html?ref=ggorantala.dev)
- [EnumMap](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/EnumMap.html?ref=ggorantala.dev) and [EnumSet](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/EnumSet.html?ref=ggorantala.dev), Java SE 21 API
- [Enum types, The Java Tutorials](https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html?ref=ggorantala.dev)