Enums: Java vs Swift
04 Nov 2023I had always thought that Java enumerations were a rather simple data type, as they are designed in C for example. But I discovered recently, as part of doing some backend work, that they’re actually much more versatile.
Enumerations in Java are implemented through the Enum base class, from which all enumerations inherit. They come with a special syntax (e.g. using the enum
keyword instead of class
to declare them) and a somewhat restricted behaviour. But precisely because they are a class, they also offer most of their functionality.
I thought it would be an interesting exercise to compare their capabilities with enumerations in Swift because Swift is currently the language I use the most and its enumeration type is also powerful.
Throughout this post I will be using the SE 18 Edition of the Java Language Specification (JLS) and version 5.9 of the Swift Programming Language. And it wouldn’t be fair if I didn’t thank Joshua Bloch for his invaluable explanations in Effective Java.
Creating enumerations
The purpose of enumerations is to create a group of related values that we can access from the rest of a program using a name.
One important feature to remember is that in Java these values are constant. Once they’re defined, they don’t change nor can be destroyed. You can think of them as singleton instances of a class.
This is also true in Swift for the most part. But as we will see later, enumerations can also hold additional values that are set at call site.
Enumerations as plain constants
The simplest way of creating enumerations is by defining constants. The two snippets below define Season
with four constant values (a.k.a. cases) that we can access by their name.
// Java
enum Season {
SPRING,
SUMMER,
AUTUMN,
WINTER;
}
var summer = Season.SUMMER;
// Swift
enum Season {
case spring
case summer
case autum
case winter
}
let summer = Season.summer
This trivial example is enough to discover how much functionality they provide out of the box.
In the case of Java, Season
is effectively a subclass of Enum. This means we have methods like name()
, ordinal()
or valueOf()
at our disposal and, by extension, we also get all the functionality of Object.
In comparison it may seem like enumerations in Swift are more limited because they don’t inherit from any parent class nor implement any interface. But in the absence of a superclass like Java’s Object, a Swift enum
comes with an equivalent built-in implementation of interfaces like CustomStringConvertible, Hashable or Codable.
Storing additional values
A more advanced way of creating enumerations is by storing additional values alongside each constant.
The snippet below demonstrates a first approach in Java. In this case we define Size
with three constants, each one storing an additional String
value:
// Java
enum Size {
SMALL("S"),
MEDIUM("M"),
LARGE("L");
private final String label;
Size(String label) {
this.label = label;
}
}
var smallSize = Size.SMALL;
This works by declaring an instance field (label
) and writing a constructor that stores the value in it. We can actually declare as many fields as we need and of any type, although each constant must share the same declaration (remember that constants are “instances” of the same Enum
subclass):
// Java
enum Size {
SMALL("S", 1, false),
MEDIUM("M", 3, false),
LARGE("L", 5, true);
private final String label;
private final double weight;
private final boolean requiresCheck;
Size(String label, double weight, boolean requiresCheck) {
this.label = label;
this.weight = weight;
this.requiresCheck = requiresCheck;
}
}
var smallSize = Size.SMALL;
While these fields could be changed later if we also wrote custom setters, the convention in the Java community seems to have them declared as final
and thus also constant.
The same approach is more limited in Swift. We can only define exactly one value per constant, known as the raw value, and restricted to types adopting the RawRepresentable interface. Moreover, they remain constant once they’re set:
// Swift
enum Size: String {
case small = "S"
case medium = "M"
case large = "L"
}
let smallSize = Size.small
To compensate for that Swift offers a second approach where we can declare a variable number of associated values of any type:
// Swift
enum ReadState {
case pending
case inProgress(page: Int)
case finished(rating: Double)
}
let statesByBookTitle: [String: ReadState] = [
"The Lord of The Rings": .finished(rating: 4.5),
"Drums of Autumn": .finished(rating: 3),
"The Pillars of The Earth": .inProgress(page: 682),
"Effective Java": .inProgress(page: 12),
"Design for Real Life": .pending
]
The example above shows an important difference with Java: associated values are attached to enumeration cases, not to the enumeration itself. There’s no need to declare instance properties and different values can be attached to the same case every time they’re used1.
A similar effect, although not as powerful, can be achieved in Java by implementing additional constructors that set default values to one or more fields:
// Java
enum VideoFormat {
MATROSKA("mkv"),
QUICKTIME("mov", List.of("qt"));
private final String mainExtension;
private final List<String> altExtensions;
VideoFormat(String mainExtension) {
this(mainExtension, List.of());
}
VideoFormat(String mainExtension, List<String> altExtensions) {
this.mainExtension = mainExtension;
this.altExtensions = altExtensions;
}
}
Switching over values
A typical application of enumerations is to use them in switch statements. They work similarly in both languages, syntactical differences aside:
// Java
boolean isWarm(Season season) {
return switch (season) {
case SPRING -> true;
case SUMMER -> true;
case AUTUMN -> false;
case WINTER -> false;
};
}
// Swift
func isWarm(season: Season) -> Bool {
switch season {
case .spring:
true
case .summer:
true
case .autum:
false
case .winter:
false
}
}
Swift allows more advanced comparisons, but that’s more a feature of switch
rather than enumerations. The only aspect where Swift differs slightly from Java is when handling associated values:
// Swift
struct Book {
enum ReadState {
case pending
case inProgress(page: Int)
case finished(rating: Double)
}
let numberOfPages: Int
let readState: ReadState
func progress(book: Book) -> Double {
switch readState {
case .pending:
0
case .inProgress(let page):
page / numberOfPages
case .finished:
100
}
}
}
Note how accessing associated values of a given case (like .finished
in the example above) is completely optional.
Iterating over values
Sometimes it’s useful to have a collection of all constants in an enumeration.
In Java this is straightforward because all values are known at compile time and the Enum
class provides a values() static method returning them:
// Java
enum Season {
SPRING,
SUMMER,
AUTUMN,
WINTER;
}
for (var season: Season.values()) {
System.out.println(season);
}
In Swift the same can be achieved by adopting the CaseIterable interface, which provides an equivalent allCases
property:
// Swift
enum Season: CaseIterable {
case spring
case summer
case autum
case winter
}
for season in Season.allCases {
print(season)
}
The difference is that, while in many cases the Swift compiler is also able to generate the implementation of allCases
for us, there are some exceptions.
For example, if we wanted to use associated values we would need to implement allCases
ourselves, which is often impractical:
// Swift
enum ReadState: CaseIterable {
case pending
case inProgress(page: Int)
case finished(rating: Double)
// Naive implementation using hardcoded limits
static var allCases: [Self] {
let inProgressCases = stride(from: 0, through: 2000, by: 1)
.map(Self.inProgress)
let finishedCases = stride(from: 0.0, through: 5.0, by: 0.25)
.map(Self.finished)
return [.pending] + inProgressCases + finishedCases
}
}
Extending enumerations behaviour
We can extend the behaviour of enumerations in several ways.
One already shown in previous sections is by implementing interfaces. Java’s Enum
base class implements Comparable among others. And we saw how adopting the CaseIterable
interface in Swift allows us to generate all values of an enumeration.
Another obvious way is by writing our own instance methods:
// Java
enum CreditCardType {
SILVER(0.0025),
GOLD(0.005),
PLATINUM(0.0075);
private final double cashbackRate;
CreditCardType(double cashbackRate) {
this.cashbackRate = cashbackRate;
}
double cashback(double amount) {
return amount * cashbackRate;
}
}
var card = CreditCardType.PLATINUM;
var formatter = new DecimalFormat("O###,###.0000");
System.out.println(formatter.format(card.cashback(15))); // 0.1125
// Swift
enum CreditCardType: Decimal {
case silver = 0.0025
case gold = 0.005
case platinum = 0.0075
func cashback(for amount: Decimal) -> Decimal {
amount * rawValue
}
}
let card = CreditCardType.platinum
print(card.cashback(for: 15)) // 0.1125
Note how in both examples above we get the same behaviour regardless of the underlying enumeration constant: amount
is multiplied by the corresponding cashbackRate
. But what if we wanted a different behaviour for each case?
In Swift there’s no other way but switching over self
:
// Swift
enum CompassDirection {
case north
case east
case south
case west
var reversed: Self {
switch self {
case .north:
.south
case .east:
.west
case .south:
.north
case .west:
.east
}
}
}
We can do the same in Java, but switching over this
is often regarded as a code smell2:
// Java
enum CompassDirection {
NORTH,
EAST,
SOUTH,
WEST;
// This is OK, but we can do better
CompassDirection reversed() {
return switch (this) {
case NORTH -> SOUTH;
case EAST -> WEST;
case SOUTH -> NORTH;
case WEST -> EAST;
};
}
}
Instead, we can leverage the fact that Enum
is a class and declare the behaviour we need as an abstract
method:
// Java
enum CompassDirection {
NORTH {
@Override
CompassDirection reversed() {
return SOUTH;
}
},
EAST {
@Override
CompassDirection reversed() {
return WEST;
}
},
SOUTH {
@Override
CompassDirection reversed() {
return NORTH;
}
},
WEST {
@Override
CompassDirection reversed() {
return EAST;
}
};
abstract CompassDirection reversed();
}
This is actually quite clever. Using an abstract method forces us to provide an implementation for each constant because the compiler complains otherwise. And the overall implementation it’s arguably easier to understand because each behaviour is closer to the constant it applies, rather than several lines below in the enum
body.
Conclusion
We have only scratched the surface of what’s possible with enumerations in Java and Swift. Although both offer the same basic functionality, some language design decisions have resulted in different, more advanced capabilities.
In Swift, the most notable is perhaps associated values, which can be of a variable number and from heterogeneous types.
In the case of Java, I was pleasantly surprised by the ability to associate different behaviours with each constant by having abstract methods instead of switching over this
.
I hope you’ve found this small comparison of such an essential type useful. I have left out things like recursive enumerations, in hope that you can discover them and continue exploring on your own.
-
Swift’s
enum
is in this way quite similar to a tagged union. ↩ -
This seems a widespread view in Object-Oriented Programming and has been catalogued by Martin Fowler as a code smell in “Refactoring: Improving the Design of Existing Code”. ↩