The labs on this site need JavaScript to run. The same content is written out below.
Learn › Java › Generics, enums, and annotations
Enum classes
Why does an enum saved as a number come back as a different constant?
An enum is a type that can have only a fixed set of values, such as the status of an order. Each constant is an object of which there is only one in the program, so constants can be compared with ==, and they can also have fields and methods. But if you save a constant to a database by writing down its position in the declaration (its ordinal), the same number refers to a different constant once the enum declaration is changed later.
Checked against real execution · 45 combinations were actually compiled and run on JDK 21 and checked against the results
Concepts in this lab
- Enum class
- This is the syntax for creating a type that can have only a few fixed values. When you declare enum Status { ORDERED, PAID }, ORDERED and PAID each become an object of type Status, and these objects are created only once and no more can be created with new. There is only one object for each constant, so constants can be compared with == and can also be used in a switch. An enum is also a class, so it can have fields, constructors, and methods, which lets you attach a display name or a code value to each constant. Unlike representing a state with a string or a number, using a value that is not defined is a compile-time error, so enums are used to represent states and kinds.
- ordinal() and name()
- These are two methods that every enum constant has. ordinal() returns the position of the constant in the declaration, counted from 0, and name() returns the name of the constant, as written in the declaration, as a string. ordinal() is determined by the declaration order, so its value changes when a constant is inserted in the middle or the order is changed. For this reason, if you save ordinal() values in a database or a file, the same number can later be read as a different constant. When saving, use name() or the value of a field that you define separately in the enum.
- valueOf method of an enum class
- This is a static method that takes a name string and returns the enum constant with that name, and it is declared automatically in every enum. Status.valueOf("PAID") returns Status.PAID. The name must match exactly, including case, and when there is no such constant, the method does not return null but throws an IllegalArgumentException. So when you pass a value that cannot be trusted, such as a string entered by a user, you must be prepared for the exception. values(), which returns an array containing all the constants in declaration order, is declared together with it.
- Identity and equality
- Identity means that two references point to the same object in memory, and you compare it with the == operator. Equality means that two objects logically have the same value, and you compare it with the equals() method. If you use == to compare strings or value objects, the result can be false even when the contents are the same.
Try these
- Default settings — The ordinal is saved, and then a constant is inserted in the middle. "Payment completed" is read as "Order canceled".
- Changing only the order — Even if no constant is added, changing only the order makes the number refer to a different constant.
- Adding at the very end — The ordinals of the existing constants stay the same, so nothing goes wrong.
- A constant that is safe by luck — The change is the same, but the ordinal of the first constant does not change.
- Saving by name — The same constant comes back even after a constant is inserted in the middle.
- Saved by name, then renamed — valueOf receives a name that does not exist and throws an IllegalArgumentException.
- Saving a code you define yourself — The same constant comes back whether the order is changed or the name is changed.
Running with the default settings
- The old enum declaration listed ORDERED, PAID, SHIPPED in that order: Each enum constant is one object. PAID("Payment completed") is a Status object created by passing "Payment completed" to the constructor, and it is the only PAID object in the whole program. You cannot create another one with new.
- An order with the status PAID was saved to the database: An enum object cannot be stored as it is, so a value that stands for the constant is written instead. Here that value is PAID.ordinal(), which is 1. ordinal() returns the position of the constant in the declaration, counted from 0.
- After that, the enum declaration was changed: The new constant CANCELED was inserted between ORDERED and PAID. The ordinals are now ORDERED(0), CANCELED(1), PAID(2), SHIPPED(3). The value written in the database (1) is not updated and stays as it was.
- Status.values()[1] runs: values() returns an array that contains the constants in the order of the current declaration. The element at index 1 is CANCELED. The saved constant was PAID, but a different constant came back. There is no exception and no warning.
- System.out.println(s + " (" + s.label() + ")") runs: The output is "CANCELED (Order canceled)". label() is a method that you write yourself inside the enum, and it returns the label value that the constant received when it was created. An enum is also a class, so it can have fields and methods like this.
- s == Status.PAID runs: The result is false. There is only one object for each enum constant, so the same constant is the same object no matter how it was obtained. That is why you can compare with == instead of equals. The result is false not because of how the comparison is done, but because s really is a different constant (CANCELED). The program ends normally.
Result of this run
- ordinal() is only the position in the declaration, so when a constant is inserted in the middle or the order is changed, a number that was already saved refers to a different constant. No exception is thrown, so wrong data is used silently: an order that is "Payment completed" is shown as "Order canceled". Do not save ordinal() values.
- Use == to compare enum constants. There is only one object for each constant, so the result is the same as with equals, no exception is thrown even when one side is null, and comparing constants of different enum classes is reported as a compile-time error.
Code
enum Status {
ORDERED("Order received"),
CANCELED("Order canceled"),
PAID("Payment completed"),
SHIPPED("Shipping started");
private final String label;
Status(String label) {
this.label = label;
}
String label() {
return label;
}
}
// The value of PAID.ordinal(), saved under the old declaration (order: ORDERED, PAID, SHIPPED)
int saved = 1;
Status s = Status.values()[saved];
System.out.println(s + " (" + s.label() + ")");
System.out.println(s == Status.PAID);
Assumptions of this model
All 45 combinations were checked by actually compiling and running the code shown on the screen (the enum declaration after the change and the code below it) on JDK 21 on this PC, and comparing the saved value, the two lines of output, the exit code, and the exception message. The saved values were obtained by actually getting ordinal(), name(), and code from the enum with the old declaration. In the code on the screen, the five lines below the enum declaration are inside the main method, and the declaration of main is omitted. The following were also checked in the same way: the same constant is the same object no matter how it is obtained, valueOf also throws an exception for a name that differs only in case, == does not throw an exception even when one side is null, and comparing constants of different enum classes with == is a compile-time error. No real database was used. The saved value was written directly in the code to imitate one. A change that removes a constant is not included in the settings. If a constant is removed, reading by ordinal returns a later constant that has moved up or throws an exception because the index is outside the array (ArrayIndexOutOfBoundsException), and reading by name makes valueOf throw an exception (this was also checked by running it separately).
Previous lab: Generics · Next lab: Annotations