Why this matters
Many programs have a small, fixed set of choices. A coin is a penny, a nickel, a dime or a quarter. An order is pending, paid or shipped. A direction is north, east, south or west. Programmers often store these choices as plain numbers, such as 0 for pending and 1 for paid. That works until someone reads the number and cannot tell what it means, or types 7 where only 0 to 2 are valid.
An enum (short for enumeration) is a type with a fixed list of named values. A value of the enum type can only be one of those names. The compiler checks the names, so a typo like Coin.DMIE fails before the program runs. Some enums also carry extra data for each name, such as the value of a coin in cents. This lesson shows how to attach data and methods to enum values in Java and in C++, and when each language's choice is the better one.
By the end of this lesson you will write a Java enum with a constructor and methods, write a C++ enum class with a function that looks up data for each name, search an enum by a text label, and explain which tool to pick for a given problem.
A fixed set of names
Start with the problem. Suppose a program stores a coin as a number.
int coin = 10; // what is 10? cents? a code? the dime?A reader has to guess. Someone else may use 10 for a different meaning, and nothing stops a value like 99 from being stored. An enum fixes both problems. Each case has a name, and the name is the only thing the code uses.
The rest of this lesson uses one running example, a coin. Each coin has a name and a value in cents. Java and C++ both support enums, but they treat them differently. In Java an enum case is a full object. In C++ an enum class value is a small number with a name attached.
Java: an enum is a small class
In Java, you declare an enum with the keyword enum. Each case is listed by name. After the list you can add fields, a constructor and methods, the same way you would in a normal class. Here is the coin enum.
import java.util.Optional;
enum Coin {
PENNY(1, "penny"),
NICKEL(5, "nickel"),
DIME(10, "dime"),
QUARTER(25, "quarter");
private final int cents;
private final String label;
Coin(int cents, String label) {
this.cents = cents;
this.label = label;
}
int cents() {
return cents;
}
String label() {
return label;
}
boolean isLargerThan(Coin other) {
return this.cents > other.cents;
}
static Optional<Coin> fromLabel(String text) {
for (Coin c : values()) {
if (c.label.equals(text)) {
return Optional.of(c);
}
}
return Optional.empty();
}
}Read it in three parts.
The list at the top, PENNY(1, "penny") and so on, creates the four coin objects. Each one passes two values to the constructor. A constructor is the block that runs when an object is created. Java runs it once for each name, when the program first uses the enum. So there are exactly four Coin objects, and each one keeps its own cents and label.
The fields cents and label are marked final. A final field is set once and cannot change later. Since there is only one object per name, a coin's value should never change, and final says so.
The methods cents(), label(), isLargerThan and fromLabel work the same way they would in any class. Calling Coin.DIME.cents() returns 10. Inside a method, values() returns an array holding all the cases in the order they were listed.
Every Java enum also gets some methods for free. name() returns the case name as text, ordinal() returns its position in the list (the first case is 0), and valueOf turns a name into a case. The next file calls all of them.
public class Main {
public static void main(String[] args) {
Coin c = Coin.DIME;
System.out.println(c);
System.out.println(c.name() + " " + c.ordinal() + " " + c.cents() + " " + c.label());
System.out.println(Coin.valueOf("QUARTER").cents());
System.out.println(Coin.QUARTER.isLargerThan(Coin.NICKEL));
System.out.println(Coin.fromLabel("nickel"));
System.out.println(Coin.fromLabel("half dollar"));
int total = 0;
for (Coin coin : Coin.values()) {
total += coin.cents();
}
System.out.println("total " + total);
System.out.println(Coin.valueOf("Dime"));
}
}Save the enum as Coin.java and the main class as Main.java in the same folder. Java requires a file to be named after the public class it holds, so Main must sit in Main.java. Compile both files with javac Coin.java Main.java and run with java Main. The output on JDK 25 is:
DIME
DIME 2 10 dime
25
true
Optional[NICKEL]
Optional.empty
total 41
Exception in thread "main" java.lang.IllegalArgumentException: No enum constant Coin.Dime
at java.base/java.lang.Enum.valueOf(Enum.java:293)
at Coin.valueOf(Coin.java:3)
at Main.main(Main.java:15)The last line is an error on purpose. valueOf matches the case name exactly, and the name is DIME in capitals. Dime is not a name, so Java throws an exception. An exception is an error that stops the code that is running unless something catches it. The message names the enum and the text it could not find. Stack trace lines are shortened here.
A second example: a lookup table built once
The fromLabel method loops over all four coins every time it runs. With four coins that costs nothing. A lookup table is faster when the enum is large, and it makes the code easier to read. Java lets an enum build a Map once, when the enum is first loaded. The example below is a new enum, Op, for the three basic maths operators. Each case has its own method body, which is the third idea in this section.
import java.util.HashMap;
import java.util.Map;
enum Op {
ADD("+") {
int apply(int a, int b) { return a + b; }
},
SUB("-") {
int apply(int a, int b) { return a - b; }
},
MUL("*") {
int apply(int a, int b) { return a * b; }
};
private static final Map<String, Op> BY_SYMBOL = new HashMap<>();
static {
for (Op op : values()) {
BY_SYMBOL.put(op.symbol, op);
}
}
private final String symbol;
Op(String symbol) {
this.symbol = symbol;
}
String symbol() {
return symbol;
}
abstract int apply(int a, int b);
static Op fromSymbol(String s) {
Op op = BY_SYMBOL.get(s);
if (op == null) {
throw new IllegalArgumentException("unknown operator: " + s);
}
return op;
}
}The braces after ADD("+") open a body for that one case. Each case gets its own apply method. The line abstract int apply(int a, int b); says that every case must provide one. If a case forgets, the code does not compile.
The static { ... } block runs once, after the three cases exist. It puts each case into BY_SYMBOL, a map from the symbol text to the case. The lookup in fromSymbol is then a single map read, not a loop.
A small main class shows it working. It also uses EnumMap, a map built for enum keys, with the Coin enum from the first example.
import java.util.EnumMap;
public class OpMain {
public static void main(String[] args) {
System.out.println(Op.fromSymbol("*").apply(6, 7));
System.out.println(Op.ADD.apply(2, 3) + " " + Op.SUB.apply(2, 3));
System.out.println(Op.MUL.getClass() + " " + Op.MUL.getDeclaringClass());
EnumMap<Coin, Integer> counts = new EnumMap<>(Coin.class);
counts.put(Coin.QUARTER, 2);
counts.put(Coin.PENNY, 5);
System.out.println(counts);
System.out.println(Op.fromSymbol("/"));
}
}Compile with javac Coin.java Op.java OpMain.java and run with java OpMain. The output is:
42
5 -1
class Op$3 class Op
{PENNY=5, QUARTER=2}
Exception in thread "main" java.lang.IllegalArgumentException: unknown operator: /
at Op.fromSymbol(Op.java:38)
at OpMain.main(OpMain.java:12)Two details in that output are worth noting. Op.MUL.getClass() prints Op$3, which shows that a case with its own body is a small subclass of Op. getDeclaringClass() gives back Op, the enum itself. EnumMap prints the keys in the order they are declared in the enum, not in the order they were added. PENNY comes before QUARTER even though QUARTER was added first.
C++: an enum class holds only a name
C++ has enums too, and the modern form is enum class. Here is the coin as a C++ enum class.
enum class Coin { penny, nickel, dime, quarter };This line creates a new type called Coin with four values. Unlike Java, the values are not objects. Each one is a number under the hood. A C++ enum class cannot hold fields, and it cannot have methods. The cents value has to live somewhere else, so C++ programmers usually write a function that takes the enum value and returns the data.
The word class in enum class means the names are scoped. You must write Coin::dime, not dime. This stops two enums from clashing when they both use a name such as small. It also means the names never leak into the surrounding code.
Here is the coin with two lookup functions and a search by text.
#include <array>
#include <iostream>
#include <optional>
#include <string_view>
enum class Coin { penny, nickel, dime, quarter };
constexpr int cents(Coin c) {
switch (c) {
case Coin::penny: return 1;
case Coin::nickel: return 5;
case Coin::dime: return 10;
case Coin::quarter: return 25;
}
return 0;
}
constexpr std::string_view label(Coin c) {
switch (c) {
case Coin::penny: return "penny";
case Coin::nickel: return "nickel";
case Coin::dime: return "dime";
case Coin::quarter: return "quarter";
}
return "unknown";
}
constexpr std::array<Coin, 4> all_coins{Coin::penny, Coin::nickel, Coin::dime, Coin::quarter};
std::optional<Coin> coin_from_label(std::string_view text) {
for (Coin c : all_coins) {
if (label(c) == text) {
return c;
}
}
return std::nullopt;
}
int main() {
Coin c = Coin::dime;
std::cout << cents(c) << ' ' << label(c) << '\n';
std::cout << (c == Coin::dime) << '\n';
if (auto found = coin_from_label("nickel")) {
std::cout << "found " << cents(*found) << '\n';
}
std::cout << coin_from_label("half dollar").has_value() << '\n';
int total = 0;
for (Coin coin : all_coins) {
total += cents(coin);
}
std::cout << "total " << total << '\n';
std::cout << static_cast<int>(c) << '\n';
return 0;
}Compile it with g++ -std=c++20 -Wall -Wextra -pedantic coin.cpp -o coin and run ./coin. The output with g++ 15.2 is:
10 dime
1
found 5
0
total 41
2The first function, cents, is a switch over the enum. A switch picks one branch based on the value. The constexpr keyword tells the compiler it can run the function while the program is being built, if the input is known then. Each branch returns the matching number of cents.
The second function, label, does the same for the text name. It returns a std::string_view, which is a light way to refer to text without copying it. The text is a fixed string in the program, so returning a view is safe.
The third step, coin_from_label, is the lookup the Java fromLabel method also does. It walks the array all_coins, asks label for each one, and returns the first match. The return type is std::optional<Coin>. An optional holds either a value or nothing. std::nullopt means nothing was found. The caller must check before using the value. The line if (auto found = coin_from_label("nickel")) does that check, and *found reads the coin out of the optional.
The last line of main prints 2. A C++ enum class does not turn into an integer by itself. The static_cast<int> in that line is needed to see the number underneath, which is the position of the name in the list. penny is 0, nickel is 1, dime is 2 and quarter is 3.
A second example: a switch that catches a missing case
The switch in cents lists every case. What happens when you add a new case to the enum and forget the switch? The compiler warns you. Here is a short program with a missing case.
#include <iostream>
enum class Coin { penny, nickel, dime, quarter };
int cents(Coin c) {
switch (c) {
case Coin::penny: return 1;
case Coin::nickel: return 5;
case Coin::dime: return 10;
}
return 0;
}
int main() {
int x = Coin::dime;
std::cout << cents(Coin::dime) << x << '\n';
}Compiling this with g++ -std=c++20 -Wall err2.cpp -o err2 prints a warning and an error. The warning is:
err2.cpp: In function 'int cents(Coin)':
err2.cpp:6:12: warning: enumeration value 'quarter' not handled in switch [-Wswitch]The error comes from the int x = Coin::dime; line, which is the next section's subject. The warning matters here. -Wall turns on the warning, so the compiler names the case that has no branch. A warning does not stop the build, but treat it as a bug. Most projects set warnings to errors so the build stops.
A third example: a lookup that works both ways
The same enum often needs a text form for printing and a parsing form for input. Here is a function that prints the name, plus a function that reads text in any case, using the same switch.
#include <cctype>
#include <optional>
#include <string>
#include <string_view>
enum class Coin { penny, nickel, dime, quarter };
std::optional<Coin> parse_coin(std::string_view text) {
std::string lower;
for (char ch : text) {
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
}
if (lower == "penny") return Coin::penny;
if (lower == "nickel") return Coin::nickel;
if (lower == "dime") return Coin::dime;
if (lower == "quarter") return Coin::quarter;
return std::nullopt;
}Calling parse_coin("DIME") returns the dime, and parse_coin("half dollar") returns nothing. The static_cast<unsigned char> before std::tolower is required. Passing a negative char to std::tolower is undefined behaviour, which means the program may do anything. Casting to unsigned char first avoids that.
Mistakes
Each mistake below comes with the error text the tools printed. Read the first line of a compiler error first, since the lines after it often repeat the same problem in more detail.
Java: creating an enum with new
Java does not let you create a new enum value with new. The enum creates its own cases, so the code below is an error. It sits in Bad.java, next to the Coin enum from the first example.
public class Bad {
public static void main(String[] args) {
Coin c = new Coin();
}
}The message from javac is:
Bad.java:3: error: enum classes may not be instantiated
Coin c = new Coin();
^Use the existing names instead, such as Coin c = Coin.PENNY;.
Java: forgetting a constructor argument
Every case must pass the values the constructor asks for. Leaving one out gives this error:
enum BadCtor {
ONE(1), TWO;
private final int n;
BadCtor(int n) { this.n = n; }
}BadCtor.java:2: error: constructor BadCtor in enum BadCtor cannot be applied to given types;
ONE(1), TWO;
^
required: int
found: no arguments
reason: actual and formal argument lists differ in lengthThe message says the constructor needs an int and the case TWO gave none. Add (2) after TWO.
Java: matching names with the wrong capitals
valueOf and the switch statement both compare names exactly. Coin.valueOf("Dime") throws the No enum constant Coin.Dime error shown earlier. When text comes from a user, convert it first with toUpperCase(), or use a lookup like fromLabel that you wrote yourself, so the rule is explicit.
C++: using an unscoped name
A C++ enum class requires the enum name in front of each value. Writing dime alone fails.
enum class Coin { penny, nickel, dime, quarter };
int main() {
Coin c = dime;
return static_cast<int>(c);
}err3.cpp:3:14: error: 'dime' was not declared in this scope; did you mean 'Coin::dime'?The compiler even suggests the fix. Write Coin::dime.
C++: printing an enum class with <<
std::cout has no idea how to print a Coin. Writing std::cout << c causes a long error. The first lines of the message are the useful part.
err1.cpp:7:15: error: no match for 'operator<<' (operand types are 'std::ostream' {aka 'std::basic_ostream<char>'} and 'Coin')The fix is to write the label function from earlier and print its result, as in std::cout << label(c). Or cast to int with static_cast<int>(c) if you only need the number.
C++: treating the enum as an integer
Because the values sit on top of numbers, it is tempting to assign them as numbers. C++ does not allow that for an enum class.
err2.cpp:15:19: error: cannot convert 'Coin' to 'int' in initializationUse static_cast<int>(Coin::dime) when you need the number. In the other direction, static_cast<Coin>(3) makes a Coin, but it accepts any number, even one that is not a listed name. Prefer a function like parse_coin that only returns the listed names.
C++: a missing switch branch
The warning shown in the second example is easy to ignore. Make sure the build shows warnings with -Wall, and fix each one. A missing branch means a function returns the fallback value, such as 0 in cents, for the new case without any sign of error.
Choosing between a Java enum and a C++ enum class
The two tools solve the same problem in different ways. Both give you a fixed set of names. The difference is what each value is.
In Java an enum value is an object. It can hold fields, have methods, and sit in an EnumMap or a switch. Each case is created once and stays alive for the whole program. Use a Java enum when the value needs to carry data or behaviour, such as Coin.DIME.cents() or Op.ADD.apply(2, 3). The Op example shows a case that does its own work, which a C++ enum class cannot do directly.
In C++ an enum class value is a small number. Its size is one number of the default type, which is int. The check sizeof(Coin) printed 4 on this machine, and the value costs nothing to copy. Use a C++ enum class when you only need the names, plus a few functions that take the value and return data. A switch in a function keeps the logic next to the enum, and the compiler warns when a case is missing. If the data is large or changes, store it in a table or a struct instead.
Some practical rules follow from that difference:
- If the values need several fields, Java keeps them together with the name. In C++, the data sits in functions or a table, and you must keep them in step with the enum.
- If the program runs in a tight loop and the values are only compared, the C++ form is cheaper.
- A Java enum can be used as a key in
EnumMap, which is fast and keeps the declaration order. C++ has no direct equivalent, so you would use an array indexed by the value.
Practice
-
In the Java
Coinenum, add a new caseHALF_DOLLAR(50, "half dollar"). Predict whatCoin.fromLabel("half dollar")returns, then check it by running the code.Show the answer: It returns
Optional[HALF_DOLLAR].fromLabelloops overvalues(), which now includes the new case, and returns the first one whose label matches. -
Write a C++ function
bool is_silver(Coin c)that returnstruefor a nickel, a dime or a quarter, andfalsefor a penny. Use aswitchso that the compiler can warn if a case is missing.Show the answer: Write the function as below. Every case is listed, so
-Wallstays silent.bool is_silver(Coin c) { switch (c) { case Coin::nickel: case Coin::dime: case Coin::quarter: return true; case Coin::penny: return false; } return false; } -
Predict the output of this line in C++:
std::cout << static_cast<int>(Coin::quarter) << '\n';, given the enumenum class Coin { penny, nickel, dime, quarter };.Show the answer: It prints
3.pennyis 0,nickelis 1,dimeis 2 andquarteris 3. -
Write a Java enum
Directionwith the casesNORTH,EAST,SOUTHandWEST, and a methodopposite()that returns the opposite direction. Test it withDirection.NORTH.opposite().Show the answer: The code below compiles, and
Direction.NORTH.opposite()printsSOUTH.enum Direction { NORTH, EAST, SOUTH, WEST; Direction opposite() { switch (this) { case NORTH: return SOUTH; case SOUTH: return NORTH; case EAST: return WEST; case WEST: return EAST; default: throw new IllegalStateException(); } } } -
Explain in your own words why
Coin.valueOf("penny")in Java throws an exception, and how you would make a version that accepts any capitals.Show the answer:
valueOfcompares the text with the case names exactly, and the names are in capitals, sopennydoes not matchPENNY. To accept any capitals, calltoUpperCase()on the text beforevalueOf, or write a lookup likefromLabelthat compares each label withequalsIgnoreCase.
Summary
- An enum is a type with a fixed list of names, which the compiler checks.
- In Java, each enum case is an object. A constructor can give it fields, and methods can be added.
- Java enums come with
name(),ordinal()andvalueOf.valueOfmatches names exactly and throws if none matches. - In C++, an
enum classholds only a number with a name. Put the data in aswitchfunction, and usestd::optionalfor lookups that can fail. - Turn on
-Wallso the compiler warns about aswitchthat misses a case. - Pick a Java enum when each value needs its own data or behaviour. Pick a C++
enum classwhen you only need names and a few functions.