1. Overview

In this tutorial, we’ll deep dive into Kotlin enums.

With the evolution of programming languages, the usage and application of enums have also advanced.

Enum constants today aren’t just mere collections of constants – they can have properties, implement interfaces, and much more.

2. Basic Kotlin Enums

Let’s look at the basics of enums in Kotlin.

2.1. Defining Enums

Let’s define an enum as having three constants describing credit card types:

enum class CardType {
    SILVER, GOLD, PLATINUM
}

2.2. Initializing Enum Constants

Enums in Kotlin, just like in Java, can have a constructor. Since enum constants are instances of an Enum class, the constants can be initialized by passing specific values to the constructor.

Let’s specify color values to various card types:

enum class CardType(val color: String) {
    SILVER("gray"),
    GOLD("yellow"),
    PLATINUM("black")
}

We can access the color value of a specific card type with:

val color = CardType.SILVER.color

3. Enum Constants as Anonymous Classes

We can define specific enum constant behavior by creating them as anonymous classes. Constants then need to override the abstract functions defined within the Enum definition.

For example, for each card type, we may have different cash-back calculations.

Let’s see how we can implement it:

enum class CardType {
    SILVER {
        override fun calculateCashbackPercent() = 0.25f
    },
    GOLD {
        override fun calculateCashbackPercent() = 0.5f
    },
    PLATINUM {
        override fun calculateCashbackPercent() = 0.75f
    };

    abstract fun calculateCashbackPercent(): Float
}

We can invoke the overridden methods of the anonymous constant classes with:

val cashbackPercent = CardType.SILVER.calculateCashbackPercent()

4. Enums Implementing Interfaces

Let’s say there’s an ICardLimit interface that defines the card limits of various card types:

interface ICardLimit {
    fun getCreditLimit(): Int
}

Now, let’s see how our enum can implement this interface:

enum class CardType : ICardLimit {
    SILVER {
        override fun getCreditLimit() = 100000
    },
    GOLD {
        override fun getCreditLimit() = 200000
    },
    PLATINUM {
        override fun getCreditLimit() = 300000
    }
}

To access the credit limit of a card type, we can use the same approach as in the previous example:

val creditLimit = CardType.PLATINUM.getCreditLimit()

5. Common Enum Constructs

5.1. Getting Enum Constants by Name

To get an enum constant by its String name, we use the valueOf() static function:

val cardType = CardType.valueOf(name.toUpperCase())

5.2. Iterating Through Enum Constants

To iterate through all enum constants, we use the values() static function:

for (cardType in CardType.values()) {
    println(cardType.color)
}

Alternatively, if we’re using Kotlin 1.9.0 or later, we can iterate over all the enum constant values by using the entries property:

for (cardType in CardType.entries) {
    println(cardType.color)
}

Now, it’s worth noting that the entries property gives an EnumEntries object, an immutable implementation of the List interface:

val enumEntries: EnumEntries<CardType> = CardType.entries
Assertions.assertEquals(CardType.values().size, enumEntries.size)

Although Kotlin continues to support the values() method, we should use the entries property over the values() method because of performance benefits.

Lastly, let’s take advantage of the find() method applicable for a List to find an enum constant by its color:

val actualCardType = CardType.entries.find { it.color == "gray" }
Assertions.assertEquals(CardType.SILVER, actualCardType)

Great! It looks like we’ve got this one right.

5.3. Static Methods

To add a “static” function to an enum, we can use a companion object:

companion object {
    fun getCardTypeByName(name: String) = valueOf(name.toUpperCase())
}

We can now invoke this function with:

val cardType = CardType.getCardTypeByName("SILVER")

Note that Kotlin doesn’t have a concept of static methods. What we’ve shown here a way to get the same functionality as in Java, but using Kotlin’s features.

6. Ordinal and Non-Ordinal Enums

In Kotlin, the ordinal property of an enum refers to its implicit position within its class of declaration. As a result, there is a one-to-one mapping between an enum and its ordinal value.

In this section, let’s learn about the ordinal property of enums and the concepts of ordinal vs. non-ordinal enums in Kotlin.

6.1. Ordinal Enum

Ordinal enums are enums where the ordinal value also signifies its inherent value. So, once defined, we shouldn’t change the position of their declarations.

Let’s understand this by defining the Weekday enum class:

enum class Weekday {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
}

We must note that in most calendars, the week starts on SUNDAY. So, we’ve positioned SUNDAY at the first position, followed by MONDAY, TUESDAY, and so on.

Now, let’s verify the ordinal property of each enum constant within the Weekday enum class:

Assertions.assertEquals(0, Weekday.SUNDAY.ordinal)
Assertions.assertEquals(1, Weekday.MONDAY.ordinal)
Assertions.assertEquals(2, Weekday.TUESDAY.ordinal)
Assertions.assertEquals(3, Weekday.WEDNESDAY.ordinal)
Assertions.assertEquals(4, Weekday.THURSDAY.ordinal)
Assertions.assertEquals(5, Weekday.FRIDAY.ordinal)
Assertions.assertEquals(6, Weekday.SATURDAY.ordinal)

It’s evident that the ordinal property uses zero-based indexing. As a result, SUNDAY has an ordinal value of 0, MONDAY has an ordinal value of 1, and so on.

Further, we can also retrieve an enum constant using its ordinal value and the Weekday.values() array:

Assertions.assertEquals(Weekday.SUNDAY, Weekday.values()[0])
Assertions.assertEquals(Weekday.MONDAY, Weekday.values()[1])
Assertions.assertEquals(Weekday.TUESDAY, Weekday.values()[2])
Assertions.assertEquals(Weekday.WEDNESDAY, Weekday.values()[3])
Assertions.assertEquals(Weekday.THURSDAY, Weekday.values()[4])
Assertions.assertEquals(Weekday.FRIDAY, Weekday.values()[5])
Assertions.assertEquals(Weekday.SATURDAY, Weekday.values()[6])

Lastly, let’s see how the compareTo() method uses the ordinal value of an enum to compare with the other enum constants:

Assertions.assertEquals(1, Weekday.MONDAY.compareTo(Weekday.SUNDAY))
Assertions.assertEquals(1, Weekday.TUESDAY.compareTo(Weekday.MONDAY))
Assertions.assertEquals(1, Weekday.WEDNESDAY.compareTo(Weekday.TUESDAY))
Assertions.assertEquals(1, Weekday.THURSDAY.compareTo(Weekday.WEDNESDAY))
Assertions.assertEquals(1, Weekday.FRIDAY.compareTo(Weekday.THURSDAY))
Assertions.assertEquals(1, Weekday.SATURDAY.compareTo(Weekday.FRIDAY))

Great! With this, we have a good understanding of the ordinal property of enum.

6.2. Non-Ordinal Enum

The non-ordinal enums are those enums that don’t associate their inherent value with their ordinal value. In this section, let’s explore non-ordinal enums by defining playing cards as enum values.

First, let’s define the four suits in the Suit enum class:

enum class Suit {
    HEARTS, DIAMONDS, CLUBS, SPADES
}

Next, let’s define the rank of cards from ACE to KING in the Rank enum class:

enum class Rank(val value: Int) {
    ACE(1),
    TWO(2),
    THREE(3),
    FOUR(4),
    FIVE(5),
    SIX(6),
    SEVEN(7),
    EIGHT(8),
    NINE(9),
    TEN(10),
    JACK(11),
    QUEEN(12),
    KING(13)
}

We’ve explicitly passed the rank values as a constructor argument because we don’t want to infer the ranks from the positions. So, this makes Rank a non-ordinal enum.

Likewise, let’s use the Rank and Suit enum to define the non-ordinal enums in the PlayingCard class:

enum class PlayingCard(val rank: Rank, val suit: Suit) {
    KING_OF_SPADES(Rank.KING, Suit.SPADES),
    QUEEN_OF_DIAMONDS(Rank.QUEEN, Suit.DIAMONDS);
    // Other such declarations
}

Now, we must understand that the ordinal property exists for all enum constants. So, we can still access it for the PlayingCard enums as well. It’s just that we don’t want to associate any significance to ordinality for such enums implicitly:

Assertions.assertEquals(0, PlayingCard.KING_OF_SPADES.ordinal)
Assertions.assertEquals(1, PlayingCard.QUEEN_OF_DIAMONDS.ordinal)

Next, let’s see the behavior of the compareTo() method for non-ordinal enums within the PlayingCard class:

Assertions.assertTrue(PlayingCard.KING_OF_SPADES.compareTo(PlayingCard.QUEEN_OF_DIAMONDS) < 0)

Unfortunately, the comparison logic seems incorrect because we’d want to see KING_OF_SPADES higher than QUEEN_OF_DIAMONDS, but it’s the reverse. That’s because the compareTo() method uses the ordinal property, which is irrelevant for non-ordinal enums. Worse, the compareTo() method is final, so we can’t define a custom comparison logic.

Nonetheless, we can use the Comparator class to define custom comparators for non-ordinal enums. So, let’s go ahead and define rankComparator and playingCardComparator:

val rankComparator = Comparator<Rank> { r1, r2 ->
    r1.value - r2.value
}
val playingCardComparator = Comparator<PlayingCard> { pc1, pc2 ->
    rankComparator.compare(pc1.rank, pc2.rank)
}

Finally, let’s see these comparators in action by sorting a list of playing cards:

val playingCards = listOf(PlayingCard.KING_OF_SPADES, PlayingCard.QUEEN_OF_DIAMONDS)
val sortedPlayingCards = playingCards.sortedWith(playingCardComparator)

Assertions.assertEquals(PlayingCard.QUEEN_OF_DIAMONDS, sortedPlayingCards[0])
Assertions.assertEquals(PlayingCard.KING_OF_SPADES, sortedPlayingCards[1])

Perfect! We got this one right, as the KING_OF_SPADES has a higher rank.

7. Extending Enums

In this section, we’ll learn some of the limitations of extending enums and a few strategies to extend them from the behavioral perspective.

7.1. Limitation

Strictly speaking, we can’t create a subclass of an enum class in Kotlin because they are final. As such, this limitation is enforced by design because enums represent a definite set of values while extending them will break this contract.

Let’s go ahead and validate that enum classes such as Weekday are final:

val weekdayClazz = Weekday::class.java
val isFinal = java.lang.reflect.Modifier.isFinal(weekdayClazz.modifiers)
Assertions.assertTrue(isFinal)

As a result, if we try to extend an enum class, we’ll get a compilation failure for our project. Let’s attempt to extend the Weekday class:

enum class MyWeekday: Weekday {
}

If we use an IDE such as IntelliJ Idea, we’ll see red wiggles indicating something is wrong. Further, we can see the error when we hover the mouse over it.

Nonetheless, let’s go ahead and compile our Maven project:

$ mvn compile
[ERROR] Failed to execute goal org.jetbrains.kotlin:kotlin-maven-plugin:1.8.20:compile (compile) ... Compilation failure: 
[ERROR] Weekday.kt:[13,23] Enum class cannot inherit from classes
[ERROR] Weekday.kt:[13,23] This type is final, so it cannot be inherited from

As expected, our attempt to create a subclass of an enum failed.

7.2. Extending Behavior Using Interfaces

Although we can’t extend enum classes, we can use interfaces to extend behavior based on the underlying hierarchy of the interfaces. Let’s understand this concept by defining colors as enum constants, each with a behavior to paint with its color and get the type.

First, let’s start by defining the ColorType enum class:

enum class ColorType {
    PRIMARY,
    SECONDARY;
}

Now, let’s define the IColor interface to capture the type() and paint() behavior:

interface IColor {
    fun type(): ColorType

    fun paint(): String
}

Further, to segregate the primary and secondary colors into different groups, let’s extend the IColor interface by defining the IPrimaryColor and ISecondaryColor interfaces:

interface IPrimaryColor : IColor {
    override fun type() = ColorType.PRIMARY
}
interface ISecondaryColor : IColor {
    override fun type() = ColorType.SECONDARY
}

Next, let’s define the PrimaryColor enum class and define the RED color:

enum class PrimaryColor : IPrimaryColor {
    RED {
        override fun paint(): String {
            return "red"
        }
    };
}

We can verify that the RED enum constant inherits the type() behavior from the parent interface IPrimaryColor:

val type: ColorType = PrimaryColor.RED.type()
Assertions.assertEquals(ColorType.PRIMARY, type)

Similarly, let’s define the SecondaryColor enum class by adding the GREEN color:

enum class SecondaryColor : ISecondaryColor {
    GREEN {
        override fun paint(): String {
            return "green"
        }
    };
}

Like earlier, we can check that the GREEN enum constant inherits the type() behavior from the parent interface ISecondaryColor:

val type: ColorType = SecondaryColor.GREEN.type()
Assertions.assertEquals(ColorType.SECONDARY, type)

Lastly, let’s verify runtime polymorphism using the PrimaryColor and SecondaryColor enum classes:

val colors = listOf(PrimaryColor.RED, SecondaryColor.GREEN)
for(color in colors) {
    Assertions.assertTrue(color is IColor)
    val myColor = color.paint()
    Assertions.assertNotNull(myColor)
}

As expected, the color object exhibits different behavior for the paint() method at runtime, depending on whether it gets the value as PrimaryColor.Red or SecondaryColor.GREEN.

7.3. Extending Behavior Using Sealed Classes

Kotlin also offers the generalized version of an enum class through sealed classes. We can use these classes when the number of objects is fixed, and all the objects exhibit specific behavior, each in its own way.

Let’s understand this by defining the Color sealed class:

sealed class Color {

    abstract fun type(): ColorType

    abstract fun paint(): String
}

We can notice that all instances of the Color class will exhibit the type() and paint() behavior.

Next, let’s define the first member in the Color class with specific definitions for the type() and paint() methods:

object RED : Color() {
    override fun paint(): String {
        return "red"
    }

    override fun type(): ColorType {
        return ColorType.PRIMARY
    }
}

Further, we can verify its behavior by calling the type() method:

val type: ColorType = Color.RED.type()
Assertions.assertEquals(ColorType.PRIMARY, type)

Similarly, let’s add another member to the Color class, with its own definition of the type() and paint() methods:

object GREEN : Color() {
    override fun paint(): String {
        return "green"
    }

    override fun type(): ColorType {
        return ColorType.SECONDARY
    }
}

Additionally, we can check that it’s behaving as expected:

val type: ColorType = Color.GREEN.type()
Assertions.assertEquals(ColorType.SECONDARY, type)

Finally, let’s simulate the polymorphism behavior by invoking the paint() method on an object of Color sealed class at runtime:

val colors = listOf(Color.RED, Color.GREEN)
for (color in colors) {
    val myColor = color.paint()
    Assertions.assertNotNull(myColor)
}

Great! As expected, we can see that different objects of the same sealed class can execute the same method while still exhibiting different behavior.

8. Conclusion

This article makes an introduction to enums in Kotlin language and its key features.

We’ve introduced some simple concepts like defining enums and initializing the constants. We’ve also shown some advanced features like defining enum constants as anonymous classes, enums implementing interfaces, and ways to extend behavior for enums. Furthermore, we learned about the ordinal property of enums and the significance of ordinal and non-ordinal enums.

The implementation of all these examples and code snippets can be found in the GitHub project. This is a Maven project, so it should be easy to import and run as it is.

Comments are closed on this article!