In this chapter, you’ll learn how Java enum types help you model a fixed set of values safely.
Coming from JavaScript, you may be used to passing strings or numbers around for choices such as statuses, roles, and options. Java lets you do that too, but it also gives you a stronger tool: an enum. An enum gives a name to each allowed option and lets the compiler check that you only use valid values.
We’ll start with the problem of magic strings and numbers, improve the code with constants, and then use enums when the values form a fixed group.
Magic strings and numbers are called magic because their meaning exist only in the developer’s head. Take the below code for example
Magic strings and numbers are called “magic” because their meaning exists only in the developer’s head. Take this code, for example:
if (account.getType().equals(2)) {
...
}
Who knows what 2 means here? Mostly the developer who wrote it. You could look through documentation to find the account type specification, but that takes time for something the code could have told you directly. If you need to ask another team what the value means, the delay becomes even worse.
Magic strings and numbers are not all equally risky. Some are confusing immediately. Others are understandable because they come from a well-known standard, but they are still less clear than a named value. Before going further, try this quick exercise:
💬 Try sorting the scenarios below by tolerability. Number them from 1 to 4, with 1 being least tolerable and 4 being the least urgent to refactor.
"PENDING": status of a transaction or request, used six times in one class.if (role.equals("admin")): user role compared with a raw string.if (status == 2): primitive int value used to check a local status variable.if (responseCode == 404): HTTP status code checked with a raw number.<aside> ⚠️
The exercise above is not meant to justify magic strings or numbers. Later in this chapter, you’ll learn tools to handle all these cases, and you should use them. Even if you have a reason for a magic value, it is usually better to avoid it so your code stays easy to review.
</aside>
One solution for magic strings and numbers is a named constant. You already know the idea from JavaScript:
const applicationName = "WeatherAPI";
Java handles this idea differently, so let’s separate two related concepts: final variables and static final class constants.
<aside> 💡
const is a reserved keyword in Java, but you cannot use it to declare constants. In Java, you normally use final or static final instead.
</aside>
final keywordThe final keyword means that a variable or field can be assigned only once. That makes it useful for values that should not be reassigned after initialization.
For example, the manufacturer field below is set once in the constructor. After the Car object is created, that field cannot be reassigned:
package com.hyf;
public class Car {
// A final String that is not set directly
private final String manufacturer;
public Car(String manufacturer) {
// Here the manufacturer field is being set and becomes constant
this.manufacturer = manufacturer;
}
public String getManufacturer() { return manufacturer; }
}
This does not mean every Car has the same manufacturer. Each object can still receive a different value when it is created.
static finalCompile-time constant is a value the compiler can know before the application runs, such as a static final String or primitive value initialized with a literal. In plain English, the value you see, will be the value your running JVM alwaus sees.
Let’s run the first Car example:
import com.hyf.Car;
void main() {
Car bmw = new Car("BMW");
Car audi = new Car("Audi");
IO.println("Car 1 manufacturer: " + bmw.getManufacturer());
IO.println("Car 2 manufacturer: " + audi.getManufacturer());
}
The manufacturer field is final, but it is still different for each object. If every Car should share one value, such as the transport type, make that value static final:
public class Car {
private final String manufacturer;
// Compile-time constant. Can now be public as it's fully immutable
public static final String TRANSPORT_TYPE = "Land";
public Car(String manufacturer) {
this.manufacturer = manufacturer;
}
public String getManufacturer() {return manufacturer;}
}
void main() {
Car bmw = new Car("BMW");
Car audi = new Car("Audi");
IO.println("Car 1 manufacturer: " + bmw.getManufacturer() + " and transport type: " + Car.TRANSPORT_TYPE);
IO.println("Car 2 manufacturer: " + audi.getManufacturer() + " and transport type: " + Car.TRANSPORT_TYPE);
}
Use an instance final field when the value belongs to one object. Use a static final class constant when one named value belongs to the class or application.
Let’s revisit our magic number example to see how class constants help:
package com.hyf.constants;
public class OrderStatus {
public static final Integer COMPLETED = 0;
public static final Integer CANCELLED = 1;
public static final Integer PENDING = 2;
}
Then we can import and use the constants:
import com.hyf.constants.OrderStatus;
void shipOrder(int status) {
if (OrderStatus.PENDING.equals(status)) {
IO.println("Shipping order...");
}
}
This is much better because OrderStatus.PENDING explains the meaning of the value. There is still one issue: shipOrder accepts any int. A call like shipOrder(999) still compiles, even though 999 is not a valid order status. This is where enums come in.
An enum is a type-safe way to define a fixed set of values. Instead of using magic strings like "PENDING" or magic numbers like 2, you create a small list of allowed options: OrderStatus.PENDING, UserType.MEMBER, or HttpStatus.FORBIDDEN.
This makes your code safer and easier to write. The compiler stops typos such as OrderStatus.PENDNG, and it stops you from passing an unrelated value such as 999. Your IDE can also show autocomplete with only the valid choices.
Defining an enum in Java is simple:
public enum OrderStatus {
COMPLETED,
SHIPPED,
PENDING,
CANCELLED
}
Now we can use the enum as a method parameter:
// The method now takes OrderStatus enum rather than an "unbounded" integer
void shipOrder(OrderStatus status) {
if (OrderStatus.PENDING.equals(status)) {
IO.println("Shipping order...");
}
}
<aside> 💡
With autocomplete, enum values are often easier to type than raw strings or numbers. Give it a try in IntelliJ IDEA.
</aside>
Java enums automatically provide useful built-in methods. Let’s go over four common ones:
name()Returns the exact constant name as a String.
void main() {
IO.println(OrderStatus.PENDING.name());
}
ordinal()Returns the constant’s zero-based position in the enum declaration. In our enum above for example, COMPLETED would have oridnal of 0, CANCELLED would be 3.
<aside> ⚠️
Do not base business logic on ordinal(). The value depends on the order of the enum constants in the source code. If someone reorders the constants later, your logic can break.
</aside>
values()Returns all constants of the enum in declaration order. It is a static method, so you call it on the enum type itself:
import com.hyf.constants.OrderStatus;
void main() {
for (OrderStatus orderStatus : OrderStatus.values()) {
IO.println("Enum " + orderStatus + " has ordinal value: " + orderStatus.ordinal());
}
}
// OUTPUT:
// Enum COMPLETED has ordinal value: 0
// Enum SHIPPED has ordinal value: 1
// Enum PENDING has ordinal value: 2
// Enum CANCELLED has ordinal value: 3
valueOf(String)Converts a String back to the matching enum constant. The text must match the enum constant name exactly. If the name does not exist, Java throws an IllegalArgumentException.
This is a static method.
OrderStatus orderStatus = OrderStatus.valueOf("PENDING");
IO.println(orderStatus);
<aside> 💭
values() and valueOf() methods are static methods compared to ordinal() and name(). Can you think of a reason why? Or why is it that enum values are sometimes called constants and sometimes called instances?
You’ll find out why very briefly in the next section!
</aside>
You might be wondering why values() and valueOf() are static methods compared to ordinal() and name() and why enum values are sometimes called constants and sometimes called instances.
The answer is that enum constants are objects!
Each enum constant is one instance of the enum type. Java creates these instances when the enum class is first used. You cannot create more enum instances with new.
The enum below has four constants, so Java creates four OrderStatus instances:
public enum OrderStatus {
COMPLETED,
SHIPPED,
PENDING,
CANCELLED
}
We can make that visible by adding a constructor:
💬 How does the new keyword create an object from a class? For example, what happens in new Car("BMW")?
Enum constructors work differently because your code cannot call them directly. Java calls them once for each enum constant.
But since we know what Java calls, we can override that method to print a statement and learn even more about enums:
package com.hyf.enums;
public enum OrderStatus {
COMPLETED,
SHIPPED,
PENDING,
CANCELLED; // Semicolon is the end of the constants section
// From here below, enums function more-or-less like a class
// Some things you can't do but compiler/IDE will quickly let you know
// Adding a constructor without any arguments and hoping it gets called.
OrderStatus() {
// If the constructor is called then this line will be printed
IO.println("Initializing the order status " + this.name());
}
}
And now testing:
import com.hyf.enums.OrderStatus;
void main() {
// Reference one enum constant so Java initializes the enum class.
IO.println(OrderStatus.CANCELLED);
}
// OUTPUT:
// Initializing the order status COMPLETED
// Initializing the order status SHIPPED
// Initializing the order status PENDING
// Initializing the order status CANCELLED
// CANCELLED
<aside> 🎉
</aside>
Now that you know enum constants are objects, you can attach data and behavior to them. One useful customization is to store related messages in an enum:
package com.hyf.enums;
public enum ErrorMessage {
// 2. So that our constants can take another value, the String here
ACCOUNT_NOT_FOUND("Account could not be found in our system"),
USER_NOT_AUTHORIZED("Your credentials are not sufficient, please try login in again.");
// 3. Which is being set to this private field "message"
private final String message;
// 1. We've customized our enum constructor here
ErrorMessage(String message) {
this.message = message;
}
// Getter public method to safely retrieve the private field message
public String getMessage() {
return message;
}
}
Usage:
import com.hyf.enums.ErrorMessage;
void printErrorMessage(ErrorMessage errorMessage) {
IO.println(errorMessage.getMessage());
}
void main() {
printErrorMessage(ErrorMessage.ACCOUNT_NOT_FOUND);
}
// OUTPUT:
// Account could not be found in our system
<aside> 💡
</aside>
Use a static final constant when you have a single, unchanging value that does not belong to a fixed group. Examples include DEFAULT_TIMEOUT_SECONDS, API_BASE_URL, TAX_RATE, or APPLICATION_NAME.
Use an enum when you have a fixed, closed set of related options, such as statuses, roles, or payment types.
Online stores, especially in the Netherlands, need to handle different payment options. Each option can have its own display name and transaction fee. Now that you know enums, can you model this with type safety, fields, a constructor, and a useful method?
Your task:
Create an enum called PaymentMethod with these values:
CREDIT_CARDPAYPALIDEALBANK_TRANSFERAdd: