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:

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.