Effective Kotlin Item 41: Use enum to represent a list of values
When we have to represent a constant set of possible options, a classic choice is to use an enum. For instance, if our website offers a concrete set of payment methods, we can represent them in our service using the following enum class:
enum class PaymentOption {
CASH,
CARD,
TRANSFER
}
Since each enum is an instance of the enum class, it can hold values that are always item-specific:
import java.math.BigDecimal
enum class PaymentOption {
CASH,
CARD,
TRANSFER;
// Do not define mutable state in enum values
var commission: BigDecimal = BigDecimal.ZERO
}
fun main() {
val c1 = PaymentOption.CARD
val c2 = PaymentOption.CARD
print(c1 == c2) // true,
// because it is the same object
c1.commission = BigDecimal.TEN
print(c2.commission) // 10
// because c1 and c2 point to the same item
val t = PaymentOption.TRANSFER
print(t.commission) // 0,
// because commission is per-item
}
It is a bad practice to define mutable variables in enum classes, because they are static for each item, so this way we create a global static mutable state (see Item 1: Limit mutability). However, this functionality is often used to attach some constant values to each item. These constant values can be attached during the creation of each item using the primary constructor:
import java.math.BigDecimal
enum class PaymentOption(val commission: BigDecimal) {
CASH(BigDecimal.ONE),
CARD(BigDecimal.TEN),
TRANSFER(BigDecimal.ZERO)
}
fun main() {
println(PaymentOption.CARD.commission) // 10
println(PaymentOption.TRANSFER.commission) // 0
val paymentOption: PaymentOption =
PaymentOption.values().random()
println(paymentOption.commission) // 0, 1 or 10
}
Kotlin enums can even have methods whose implementations are also item-specific. When we define them, the enum class itself needs to define an abstract method, and each item must override it:
enum class PaymentOption {
CASH {
override fun startPayment(
transaction: Transaction
) {
showCashPaymentInfo(transaction)
}
},
CARD {
override fun startPayment(
transaction: Transaction
) {
moveToCardPaymentPage(transaction)
}
},
TRANSFER {
override fun startPayment(
transaction: Transaction
) {
showMoneyTransferInfo()
setupPaymentWatcher(transaction)
}
};
abstract fun startPayment(transaction: Transaction)
}
However, this option is rarely used because it is more convenient to use a primary constructor parameter of functional type:
enum class PaymentOption(
val startPayment: (Transaction) -> Unit
) {
CASH(::showCashPaymentInfo),
CARD(::moveToCardPaymentPage),
TRANSFER({
showMoneyTransferInfo()
setupPaymentWatcher(it)
})
}
An even more convenient option is to define an extension function. Notice that when we use enum values in a when
expression, we do not need to add an else
clause when we cover all the values.
enum class PaymentOption {
CASH,
CARD,
TRANSFER
}
fun PaymentOption.startPayment(transaction: Transaction) {
when (this) {
CASH -> showCashPaymentInfo(transaction)
CARD -> moveToCardPaymentPage(transaction)
TRANSFER -> {
showMoneyTransferInfo()
setupPaymentWatcher(transaction)
}
}
}
The power of enum is that its items are specific and constant. We can get all the items using this enum’s companion object’s values()
function or the top-level enumValueOf
function. We can also read enum from a String
using its companion object’s valueOf(String)
or top-level enumValueOf(String)
. All enums are a subtype of Enum<T>
.
enum class PaymentOption {
CASH,
CARD,
TRANSFER
}
inline fun <reified T : Enum<T>> printEnumValues() {
for (value in enumValues<T>()) {
println(value)
}
}
fun main() {
val options = PaymentOption.values()
println(options.map { it.name })
// [CASH, CARD, TRANSFER]
val options2: Array<PaymentOption> =
enumValues<PaymentOption>()
println(options2.map { it.name })
// [CASH, CARD, TRANSFER]
val option: PaymentOption =
PaymentOption.valueOf("CARD")
println(option) // CARD
val option2: PaymentOption =
enumValueOf<PaymentOption>("CARD")
println(option2) // CARD
printEnumValues<PaymentOption>()
// CASH
// CARD
// TRANSFER
}
Iterating over enum values is easy. All enums have the ordinal
property and implement the Comparable
interface. Enums also automatically implement toString
, hashCode
and equals
. Their serialization and deserialization are simple (they are represented just by name), efficient, and automatically supported by most libraries for serialization (like Moshi, Gson, Jackson, Kotlinx Serialization, etc). As a result, enums are perfect for representing a concrete set of constant values. But how do they compare to sealed classes?
As shown in Item 39: Use sealed classes and interfaces to express restricted hierarchies; sealed classes and interfaces can also represent a set of values, but all their subclasses have to be object declarations.
sealed class PaymentOption
object Cash : PaymentOption()
object Card : PaymentOption()
object Transfer : PaymentOption()
For just a set of values, enum should be preferred: sealed classes cannot be automatically serialized or deserialized; they are not so easy to iterate over (although we can do it with reflection); and they do not have a natural order. However, there are some cases when we might choose sealed classes with object subclasses anyway: classes can keep values, so if we think we might need to transform objects into classes at some point, we should prefer sealed classes. For instance, if we define messages to some actor, we should use sealed classes even if all the messages are now object declarations. This is because it is likely we will need to transform some of these objects into classes in the future.
sealed class ManagerMessage()
object ProductionStarted : ManagerMessage()
object ProductionStopped : ManagerMessage()
// In the future me might add something like this:
class Alert(val message: String) : ManagerMessage()
// or we might need to add data to an existing message
class ProductionStarted(
val time: DateTime
) : ManagerMessage()
When messages need to hold data, it is a clear-cut case: we need classes, not merely an enum. This is how our payment types, which hold the data required to make transactions, could be represented:
sealed class Payment
data class CashPayment(
val amount: BigDecimal,
val pointId: Int
) : Payment()
data class CardPayment(
val amount: BigDecimal,
val pointId: Int
) : Payment()
data class BankTransfer(
val amount: BigDecimal,
val pointId: Int
) : Payment()
fun process(payment: Payment) {
when (payment) {
is CashPayment -> {
showPaymentInfo(
payment.amount,
payment.pointId
)
}
is CardPayment -> {
moveToCardPaiment(
payment.amount,
payment.orderId
)
}
is BankTransfer -> {
val transferRepo = BankTransferRepo()
val transferDetails = transferRepo.getDetails()
displayTransferInfo(
payment.amount,
transferDetails
)
transferRepo.setUpPaymentWathcher(
payment.orderId,
payment.amount,
transferDetails
)
}
}
}
- The advantage of enum classes is that they can be serialized and deserialized out of the box. They have the companion object methods
values()
and valueOf(String)
. We can also get enum values by the type using the enumValues()
and enumValueOf(String)
functions. Each enum value has ordinal
, and we can hold per-item data. They are perfect for representing a constant set of possible values. - Enum classes represent a concrete set of values, while sealed classes represent a concrete set of classes. Since these classes can be object declarations, we can use sealed classes to a certain degree instead of enums, but not the other way around.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.
Nicola Corti is a Google Developer Expert for Kotlin. He has been working with the language since before version 1.0 and he is the maintainer of several open-source libraries and tools.
He's currently working as Android Infrastructure Engineer at Spotify in Stockholm, Sweden.
Furthermore, he is an active member of the developer community.
His involvement goes from speaking at international conferences about Mobile development to leading communities across Europe (GDG Pisa, KUG Hamburg, GDG Sthlm Android).
In his free time, he also loves baking, photography, and running.