Java Enums
Replace fragile state strings with a small, meaningful type. Learn enum constants, metadata, parsing, and switches, then build an order that accepts only the state changes its rules permit.
Before you start: review switch expressions, constructors, and encapsulation. Each runnable example is a complete Java 21 Main.java program requiring no input. Earlier examples isolate syntax; the final model adds the full transition policy.
What you will be able to do
- Replace unrestricted state strings with a dedicated enum type.
- Compare enum constants and handle missing values deliberately.
- Use values(), name(), valueOf(), and an exhaustive switch appropriately.
- Add constructor-supplied metadata without storing per-order state on shared constants.
- Separate Java identifiers, display labels, and explicit external codes.
- Implement and test permitted transitions while preserving state after rejection.
Use a type when the supported choices are known
Suppose an order's current status is stored as a String. NEW, new, and a misspelled value can all fit in that variable, even when the application supports only four states. Every caller must remember the spelling conventions, and mistakes often survive until a later comparison behaves unexpectedly.
OrderStatus declares the supported constants NEW, PAID, SHIPPED, and CANCELLED as values of one dedicated type. An OrderStatus variable cannot receive an unrelated String or a constant from a different enum. That gives the compiler useful information before the program runs.
An enum is suitable for a deliberately finite vocabulary controlled by the program, not a product catalog or list of users that changes at runtime. The following order example is an in-memory teaching model: setting PAID does not verify a real payment, and setting SHIPPED does not contact a delivery provider.
Main.java · Store a supported status rather than arbitrary text
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
NEW
PAID
Enum constants are objects, and null is still possible
Each declared enum constant identifies its own instance. OrderStatus.PAID can be shared by many order references, and comparing enum constants with == is appropriate. This differs from comparing String contents, where equals is normally needed rather than reference comparison.
A variable of an enum reference type can still hold null. missing == OrderStatus.PAID safely returns false, and OrderStatus.PAID.equals(missing) also returns false. Calling missing.label() or missing.equals(...) would dereference null and fail; the enum declaration does not supply automatic null safety.
Choose the missing-value rule at the application boundary. In this lesson every new Order begins in NEW, its status field is private, and transitionTo rejects null. Keeping those rules in one place is more reliable than making every caller guess what a missing state means.
Main.java · Compare constants and account for null
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
true
false
false
Use values() to visit the declared choices
OrderStatus.values() supplies an array of the constants in declaration order. The enhanced-for loop can visit them for menus, diagnostics, or tests without repeating a second manually maintained list. name() returns each constant's exact declared identifier.
The returned array is not the enum definition itself. Replacing an element in that array does not rename or remove the declared constant, and a later values() call still supplies the declared constants. Do not confuse a mutable array of references with a mutable set of enum members.
Declaration order is useful when you intentionally want that presentation order, but it is not a business workflow. An order should not advance by taking the next array element: CANCELLED is not a normal step after SHIPPED, even though it appears later in this declaration.
Main.java · Visit every declared constant
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
NEW
PAID
SHIPPED
CANCELLED
Add fixed metadata with fields and a constructor
The larger OrderStatus stores a machine-facing code and a human-facing label for each constant. Constructor arguments after each constant initialize that constant's private final fields. A semicolon separates the constant list from the following fields, constructor, and methods.
The constructor is private here; enum constructors cannot be public or protected. Code uses the declared constants instead of creating new OrderStatus objects with new. Enums already have the Java Enum base class, so they cannot extend another class, although they can implement interfaces.
Do not store an individual order's tracking number or customer name on an enum constant. Those constants are shared objects. Keep per-order state on Order, and keep the enum's metadata read-only. Enum syntax alone does not prevent someone from adding mutable fields that would accidentally be shared.
Main.java · Keep identifier, external code, and display label separate
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
PAID
paid
Payment recorded
1
Read the metadata output line by line
name()returns the Java identifier PAID.code()returns our explicit external value paid.label()returns the display text Payment recorded.ordinal()returns the current zero-based declaration position, 1.
Try moving PAID in a copy of the declaration. Its ordinal changes, but its explicit code and label do not. Do not use that positional number as a durable ID or workflow step.
Separate names, display labels, and durable external codes
For PAID, name() returns PAID, code() returns paid, and label() returns Payment recorded. They serve different readers. Java identifiers help source code, explicit codes form an intentional external vocabulary, and display labels can be rewritten for users without redefining either of the other two.
ordinal() returns the zero-based declaration position: PAID currently has ordinal 1. That output is shown only to explain the method, not as a recommended saved status ID. Inserting or reordering constants can change ordinals, and ordinal arithmetic does not represent our allowed transitions.
The inherited toString() initially returns the constant name, but an enum may override it for display. name() still returns the identifier. Even explicit external codes require a compatibility policy if changed later; this lesson defines codes but does not implement database persistence, migrations, or an API protocol.
Use an exhaustive switch to make every supported case visible
The switch expression returns one next-step message for each OrderStatus. Inside this enum switch, case labels use the constant names without repeating OrderStatus. Arrow cases do not fall through into the following case, and the whole expression produces the String assigned to nextStep.
Because all four constants are handled, a default branch is unnecessary here. If another constant is added and this source is recompiled, the compiler can flag a now-incomplete switch expression. A catch-all default would make it easier to overlook the need to consider the new state's meaning.
Exhaustive over constants does not mean automatically safe for null. This example supplies a non-null PAID value. Without an explicit case null, a null selector still fails at runtime in Java 21. Our order model prevents null status and handles a null transition target before evaluating its policy.
Main.java · Return a message for every known status
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Ship or cancel
Translate external text at a deliberate boundary
OrderStatus.valueOf requires an exact declared name, including case, with no extra surrounding whitespace. valueOf with paid is not the same as valueOf with PAID. An unknown name throws IllegalArgumentException, while a null name throws NullPointerException.
Our separate fromCode method defines a different contract: reject null or blank input, strip surrounding Java-defined whitespace, normalize case with Locale.ROOT, and match the explicit code field. This lets padded mixed-case input map to PAID without confusing a display label with an identifier.
Unsupported input such as refunded is rejected rather than silently converted to NEW or PAID. That matters because a convenient fallback could disguise a bad request as a valid business state. Catch the documented error at an appropriate caller boundary; do not swallow every exception and pretend parsing succeeded.
Main.java · Distinguish exact enum names from normalized external codes
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
PAID
PAID
Unknown status code: refunded
Trace padded mixed-case input
- Check that the input is neither null nor blank.
- Strip surrounding whitespace and normalize case with Locale.ROOT.
- Compare the normalized text with each explicit code.
- Return the matching constant, or throw the documented unknown-code error.
The parser does not make an unsupported status valid. In particular, a display label is not automatically an accepted code.
A valid state is not automatically a valid state change
NEW and SHIPPED are both legal enum values, but moving directly from NEW to SHIPPED is forbidden in our model. The enum limits the vocabulary; canTransitionTo defines which pairs are permitted. NEW can become PAID or CANCELLED, PAID can become SHIPPED or CANCELLED, and terminal states permit no further transition.
Our policy deliberately rejects repeated transitions to the same state. Other systems may choose idempotent repeats, but that is a business decision rather than an enum feature. The query canTransitionTo returns false for null; the mutating transitionTo method instead gives a specific IllegalArgumentException for a null request.
Order.transitionTo validates before assigning status. An unsupported non-null target causes IllegalStateException and leaves the old state intact. This ordering is essential: setting the field first and checking afterward would corrupt the order even if the method then reported failure.
Main.java · Validate a transition before changing the order
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
NEW
PAID
Cannot move from PAID to NEW.
Still: PAID
Why does the rejected move leave PAID unchanged?
- The current field refers to PAID.
- The target NEW is non-null, so the operation checks PAID.canTransitionTo(NEW).
- The policy returns false, so transitionTo throws before its assignment.
- The status getter therefore still returns PAID after the caller catches the error.
A legal enum constant can still be an illegal next state. Type checking and transition checking solve different problems.
Shared constants do not mean shared order state
Two new Order objects initially refer to the same NEW constant. They do not share one status field: each Order instance owns its own reference. Changing the first order's status assigns a different enum reference to that order only; it does not mutate the NEW constant or the second order.
The first example path reaches SHIPPED through PAID, while the second remains NEW. A separate cancellation path can end at CANCELLED. Terminal-state rules prevent reopening either terminal state in this simplified model, and a real domain would need its own explicit refund, return, or reopening policy.
This is a single-threaded, non-persistent exercise. There is no transactional database update, concurrent-request protection, payment authorization, shipping integration, or webhook idempotency. A production workflow must verify those boundaries separately rather than treating an enum transition as proof of a real-world event.
Main.java · Different orders keep their own current status
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Same constant initially: true
First: SHIPPED
Second: NEW
Verify the complete transition matrix, not just the happy path
Four statuses produce sixteen current-to-target pairs. Write down the four allowed edges and verify every other pair is rejected, including self-transitions and both terminal rows. Compare the policy result with the mutating Order method wherever that starting state is reached through a valid path.
Check valid normalized codes, exact valueOf behavior, unknown codes, nulls, empty text, and whitespace-only text. Verify that rejection never changes an order's current status and that two independent orders do not affect each other. The vocabulary, parser, and transition operation are separate contracts.
Finally try deliberate compilation mistakes: assigning a String to OrderStatus, inventing a misspelled constant, constructing an enum with new, and removing a case from an exhaustive switch expression. Read whether an error belongs to compilation, parsing at runtime, or the business transition policy before changing the code.
Repair exercise: text is not an enum constant
The field is correctly typed OrderStatus, but markPaid assigns the String PAID. This starter must fail compilation. Repair the assignment to use the declared enum constant without weakening the field type to String. This small syntax exercise has no complete transition policy yet.
Main.java · Diagnose a String-to-enum assignment
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
The starter intentionally fails compilation with an incompatible-types diagnostic: String cannot be converted to OrderStatus. No runtime output is expected until the assignment is repaired.
Reveal the explained repair
OrderStatus.PAID is a value of the required enum type; a String containing the same letters is not. The compiler catches the mismatch before runtime. The independent lab goes further by guarding all transitions rather than exposing an unrestricted status setter.
Main.java · Assign the declared constant
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
PAID
Independent lab: an order with guarded state transitions
Build an in-memory Order with enum-based status, stable explicit codes, read-only display metadata, and a single guarded transition method. All printed lines belong in main. The PAID and SHIPPED values are simulated states, not verified payment or delivery events.
- Declare OrderStatus constants in order NEW, PAID, SHIPPED, CANCELLED. Give them codes new, paid, shipped, cancelled and labels Awaiting payment, Payment recorded, Sent to customer, Cancelled. Store code and label as private final fields initialized by a private constructor.
- Expose code() and label(). Implement fromCode(String) to reject null/blank with IllegalArgumentException and Status code must not be blank.; otherwise strip and lowercase with Locale.ROOT, match code fields, or throw Unknown status code: <normalized>.
- Implement canTransitionTo(OrderStatus): NEW allows PAID or CANCELLED; PAID allows SHIPPED or CANCELLED; SHIPPED and CANCELLED allow nothing. Return false for null. Reject all self-transitions and do not use ordinal arithmetic.
- Implement Order with a private status initially NEW and a status() getter. transitionTo(null) throws IllegalArgumentException with Target status must not be null. Unsupported non-null requests throw IllegalStateException with Cannot move from <current> to <target>. Validate before changing the field.
- In main show the initial status and label, parse padded mixed-case paid, transition to PAID then SHIPPED, and catch the rejected move from SHIPPED to NEW. Show the unchanged state, cancel a second new order, and catch an unknown refunded code.
- Verify all sixteen status pairs, both cancellation paths, repeated/self transitions, normalized input, and unchanged state after every rejection. Keep actual order-specific data on Order, not on the shared enum constants.
Write down the allowed edges before implementing the policy. The finished program should produce:
NEW | Awaiting payment
PAID | Payment recorded
SHIPPED | Sent to customer
Rejected: Cannot move from SHIPPED to NEW.
Still: SHIPPED
Second: CANCELLED
Rejected code: Unknown status code: refunded
Main.java · Build the guarded order model
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Stage 1 hints: define values and boundary parsing
Give each constant its code and label, then end the list with a semicolon before declaring fields. Use private final fields and a private constructor. Keep valueOf exact-name behavior separate from the deliberately normalized fromCode contract.
Stage 2 hints: express the transition matrix
Switch on the current constant in canTransitionTo. Each branch asks whether the target is one of that row's allowed choices. Return false for null and terminal states. Self-transitions are not on the allowed list.
Stage 3 hints: protect each order and test rejection
Keep status private and initialize it to NEW. Validate null first, then ask the policy before assigning. Catch the expected errors in main and read the getter afterward to prove that rejection preserved the last valid value.
Reveal the complete explained solution
Main.java · Complete guarded order-state lab
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
The enum defines vocabulary and immutable metadata, the parser translates external codes, and each Order owns its current reference. The policy permits only four directed edges. Because the operation checks before assignment, the rejected terminal-state transition cannot reopen the shipped order. The second order cancels independently.
Test changes and boundaries
- Test all sixteen non-null current/target pairs. Exactly NEW to PAID, NEW to CANCELLED, PAID to SHIPPED, and PAID to CANCELLED must succeed; all others must reject without changing state.
- For every status, round-trip code() through fromCode and test padded mixed-case input. Confirm valueOf accepts exact names only and fromCode does not accept labels or invent unknown statuses.
- Reject null, empty, spaces, and tab/newline codes with the documented error. Reject unknown normalized codes, null targets, and invalid transitions, checking the old state after each failure.
- Verify two orders keep independent references and read-only queries never change either order. Cancelling one order must not alter another order that refers to a shared enum constant.
- Explain why declaration order, ordinal(), name(), and display labels are not interchangeable durable IDs. Add a hypothetical status in a copy and inspect the exhaustive switches the compiler requires you to revisit.
Common mistakes and how to diagnose them
Assigning text instead of a constant
Use OrderStatus.PAID rather than a String containing PAID. Parse external text once at a deliberate boundary instead of weakening every internal field to String.
Assuming valueOf ignores case or spaces
valueOf matches exact declared names. Use the separately documented fromCode policy for normalized external codes, and reject unsupported input explicitly.
Using ordinal as a workflow step or stored ID
ordinal reflects declaration position. Reordering or inserting constants changes it, and cancellation is not the next normal workflow step. Use explicit codes and an explicit transition policy.
Treating null as a real status
Null is not another enum constant. Comparisons may safely return false, but method calls and a switch without case null can fail. Keep the model non-null and validate requests.
Storing per-order fields on shared constants
All orders referring to PAID refer to the same constant instance. Mutable tracking/customer fields there would be shared accidentally. Put order-specific data on each Order instead.
Changing state before validating the edge
Check the old-to-new pair first. Otherwise a rejected request could still overwrite the original state. Enum type safety does not replace encapsulated business validation.
Frequently asked questions
When is an enum better than a String?
Use an enum for a known finite vocabulary controlled by the program, such as these supported states. Keep open-ended user text and runtime-managed catalogs as data rather than continually adding enum constants.
Can enums have constructors, methods, or interfaces?
Yes. They can hold metadata, define methods, and implement interfaces. Constructors cannot be public or protected, and callers use declared constants rather than constructing enum instances with new.
Is == safe for enum comparisons?
Yes, comparing compatible enum references with == checks the constant identity and handles a null operand without dereferencing it. Calling an instance method on a null reference is still unsafe.
Does an enum make my entire workflow valid?
No. It limits the supported values, not every permitted transition. The Order operation separately checks the current-to-target pair and preserves state if the request is rejected.
Why not save ordinal() as the status?
Ordinal is a declaration position, so source reordering or inserted constants can change its meaning. An explicit external code makes the vocabulary intentional, although code changes still require compatibility planning.
Does moving to PAID perform a real payment?
No. This is a single-threaded in-memory model with no Stripe call, authorization, webhook, persistence, or delivery integration. Real external events and concurrent updates require separate validation and operational handling.
A finite vocabulary, plus deliberate behavior
Enums make supported choices explicit. Constants can carry fixed metadata and behavior, while each order owns its current reference. Use exact names only where that contract is intended, explicit codes for external mappings, and explicit edges for transitions. Validate before assignment so a failed operation does not damage state.
Before continuing, explain why a String is not PAID, why an ordinal is not a stable external code, and why NEW → SHIPPED fails even though both constants exist. Next: Java dates and times, where calendar dates, local times, and global instants have different meanings.
Can you separate valid values from valid transitions?
Answer six questions and read the explanations. Quiz answers are temporary and do not save course progress.
References and related learning
The order model, explanations, and exercises are original SovranCode material. These official Java 21 references document the underlying enum and switch rules.
- Java 21 specification: enum classes, constructors, and members
- Java 21 Enum API: names, ordinals, and valueOf
- Java 21 specification: switch expressions
Review switch · Review encapsulation · Java course roadmap.
Last updated: