Enums and Sealed Classes
A boolean is a way of modeling a fixed set of states, where there are only two possible states.
Frequently, though, we find ourselves needing to model a fixed set of states that has more than two entries. For example, even with something that is nominally a boolean, we might have “true”, “false”, and “undefined”. The latter state would be for cases where we have not yet gone through any code that positively sets the state to “true” or “false”. For example, for some sort of user preference, perhaps the user has not visited the “settings” screen to state their preference just yet, so their preference is “undefined”.
The more you look, the more you find things that might be modeled this way:
- The error responses from a server API
- The steps in a wizard-style “signup” set of screens
- The modes of transportation that a
Person
object might be taking (walking? biking? driving? flying? skiing? jet-skiing?) - And so on
For these cases, Kotlin offers two programming constructs: enumerations (“enums”) and sealed classes. In this chapter, we will explore each of these.
Enums
Some programming languages — particularly those that grew out of C — offer “enums” as first-class constructs. So, Java has an enum
:
enum ServerError {
INVALID_INPUT,
OUT_OF_STORAGE_SPACE,
USER_NOT_FOUND,
CLIENT_NOT_FOUND,
SERVER_NOT_FOUND,
WHOEVER_YOU_THINK_YOU_ARE_TALKING_TO_NOT_FOUND
}
Kotlin’s enum
option resembles that of Java.
Basic Syntax and Usage
The first difference between Kotlin and Java is in the declaration. In Java, enum
exists as a standalone keyword. In Kotlin, the enum
decorates class
, much like how data
is a type of class:
enum class HungerState {
NOT_HUNGRY,
SOMETIMES_HUNGRY,
ME_ALWAYS_HUNGRY,
RAVENOUS,
YOU_LOOK_LIKE_FOOD
}
You can then refer to enum
values using standard dot notation, such as HungerState.ME_ALWAYS_HUNGRY
.
Since this is a type of class, you use an enum
in Kotlin the same way that you would any other class, such as in a constructor parameter:
enum class HungerState {
NOT_HUNGRY,
SOMETIMES_HUNGRY,
ME_ALWAYS_HUNGRY,
RAVENOUS,
YOU_LOOK_LIKE_FOOD
}
data class Animal(
val species: String,
val ageInYears: Float,
val hungry: HungerState = HungerState.SOMETIMES_HUNGRY
) {
var isFriendly = true
val isCommonlySeenFlyingInTornadoes = false
}
Class Features
Since a Kotlin enum
is a class, you can use many class features in an enum
.
Constructors
The most commonly seen of these is the constructor, used to provide values to define an individual enum
constant:
enum class HttpResponse(val code: Int) {
OK(200),
MOVED_PERMANENTLY(301),
NOT_MODIFIED(304),
UNAUTHORIZED(401),
NOT_FOUND(404),
INTERNAL_SERVER_ERROR(501)
}
fun main() {
println(HttpResponse.OK.code)
}
Here, we use a constructor to associate a code
value with each HttpResponse
constant. This is just an ordinary class property, so you can ask a constant like OK
for its code
.
Functions
An enum
class can implement functions, including overriding base ones like toString()
:
enum class HttpResponse(val code: Int, val message: String) {
OK(200, "OK"),
MOVED_PERMANENTLY(301, "Moved Permanently"),
NOT_MODIFIED(304, "Not Modified"),
UNAUTHORIZED(401, "Unauthorized"),
NOT_FOUND(404, "Not Found"),
INTERNAL_SERVER_ERROR(501, "WTF?");
override fun toString() = message
}
fun main() {
println(HttpResponse.INTERNAL_SERVER_ERROR.toString())
}
The list of constants must be the first thing in the enum
declaration — if we tried to have our toString()
before the OK
, we would fail with a compile error.
Note that in this case the comma-delimited list of constants is ended with a semicolon. If the only thing in the enum
declaration is the list of constants, the overall closing brace of the enum
is sufficient to end the list of constants. If you have other things after the constants, though, such as our toString()
function, you need the semicolon to officially end the list of constants.
An enum
class is intrinsically abstract
, so you can define abstract functions as well, implementing them on individual constants:
enum class WizardStep {
INTRO {
override fun nextStep() = REGISTER
},
REGISTER {
override fun nextStep() = PERMISSIONS
},
PERMISSIONS {
override fun nextStep() = THANKS
},
THANKS {
override fun nextStep() = THANKS
};
abstract fun nextStep(): WizardStep
}
Here, each WizardStep
knows the next WizardStep
in the sequence, by overriding an abstract nextStep()
function declared for WizardStep
. This gets a bit odd with the last step, as by definition the last step is last, so there is no “next step”. You might be tempted to return something like null
from nextStep()
on THANKS
… we will see how that works in an upcoming chapter.
Limitations
An enum
class is allowed to implement interfaces, but it is not allowed to extend another class.
Also, akin to data
classes, an enum
cannot be open
for extension… by classes outside of the enum
class. Each of the enumerated constants (e.g., REGISTER
, THANKS
in the above example) is in effect a subclass of the enum
class, complete with override
methods to match the abstract
ones in the enum
class. But you cannot create new constants from outside the enum
class, and you cannot create arbitrary other subclasses of the enum
class.
Common Properties
Each enumerated constant has two properties, beyond those that you might declare yourself: name
and ordinal
. name
returns the symbolic name that you gave the constant in your code, and ordinal
returns the 0-based index indicating where this constant appears in the list of constants. You can access name
and ordinal
as you can any other property:
enum class HttpResponse(val code: Int, val message: String) {
OK(200, "OK"),
MOVED_PERMANENTLY(301, "Moved Permanently"),
NOT_MODIFIED(304, "Not Modified"),
UNAUTHORIZED(401, "Unauthorized"),
NOT_FOUND(404, "Not Found"),
INTERNAL_SERVER_ERROR(501, "WTF?");
override fun toString() = message
}
fun main() {
println(HttpResponse.INTERNAL_SERVER_ERROR.toString())
println(HttpResponse.INTERNAL_SERVER_ERROR.code)
println(HttpResponse.INTERNAL_SERVER_ERROR.name)
println(HttpResponse.INTERNAL_SERVER_ERROR.ordinal)
}
This yields:
WTF?
501
INTERNAL_SERVER_ERROR
5
The name could be useful in toString()
implementations and similar scenarios. The ordinal is less likely to be useful — in particular, since it is position-dependent, it may be a bit fragile, as somebody reordering lines of your code might change the ordinal values for those constants.
Conversion
Each enum
class is given a few functions to be called on the class itself. One is valueOf()
. This returns a constant given the symbolic name that you gave the constant in your code. In other words, it works like the inverse of the name
property — name
gives you the symbolic name for a constant, while valueOf()
gives you the constant for a symbolic name:
enum class HttpResponse(val code: Int, val message: String) {
OK(200, "OK"),
MOVED_PERMANENTLY(301, "Moved Permanently"),
NOT_MODIFIED(304, "Not Modified"),
UNAUTHORIZED(401, "Unauthorized"),
NOT_FOUND(404, "Not Found"),
INTERNAL_SERVER_ERROR(501, "WTF?");
override fun toString() = message
}
fun main() {
println(HttpResponse.valueOf("NOT_MODIFIED"))
}
This results in:
Not Modified
valueOf()
returns the NOT_MODIFIED
constant. println()
then calls toString()
implicitly on that constant, and our overridden toString()
function returns "Not Modified"
.
Iteration
Also, you can call values()
on the enum
class itself (e.g., HttpResponse.values()
). This will allow you to iterate over all of the constant members of the enum
class, in the order in which they were declared. Hence, this code:
enum class HttpResponse(val code: Int, val message: String) {
OK(200, "OK"),
MOVED_PERMANENTLY(301, "Moved Permanently"),
NOT_MODIFIED(304, "Not Modified"),
UNAUTHORIZED(401, "Unauthorized"),
NOT_FOUND(404, "Not Found"),
INTERNAL_SERVER_ERROR(501, "WTF?");
override fun toString() = message
}
fun main() {
for (constant in HttpResponse.values()) {
println(constant)
}
}
results in:
OK
Moved Permanently
Not Modified
Unauthorized
Not Found
WTF?
Exhaustive when
We saw that you can use when
as an expression, where each one of the branches inside the when
supply the value for the expression when that branch is true
. However, one limitation that we saw was that you needed an else
condition when using when
as an expression, as for any possible condition, the when
needs to generate a value.
One exception to that rule is an “exhaustive when
” based on an enum
class. If you have conditions for each enumerated constant, you do not need an else
condition, since by definition every possibility will have been handled by one of the other conditions.
For example, this script yields the same result as the one shown above:
enum class HttpResponse(val code: Int) {
OK(200),
MOVED_PERMANENTLY(301),
NOT_MODIFIED(304),
UNAUTHORIZED(401),
NOT_FOUND(404),
INTERNAL_SERVER_ERROR(501);
override fun toString() = when(this) {
OK -> "OK"
MOVED_PERMANENTLY -> "Moved Permanently"
NOT_MODIFIED -> "Not Modified"
UNAUTHORIZED -> "Unauthorized"
NOT_FOUND -> "Not Found"
INTERNAL_SERVER_ERROR -> "WTF?"
}
}
fun main() {
for (constant in HttpResponse.values()) {
println(constant)
}
}
This time, toString()
no longer just reads some property. We no longer have those properties, and instead use a when
expression to get the human-readable message to go along with the HTTP response code. We do not need an else
in the when
, since every possible enumerated constant has its own condition. We have “exhausted” all possibilities with the specific conditions, and so this is an “exhaustive” when
.
This particular example is really silly — having these messages as properties would be a better choice. However, you may have other places in your code where you need to branch based on an enum
value, and so long as all possible values are accounted for, you do not need an else
.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.