Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Enum Classes

Many of the data types we’ve seen so far can represent a huge number of possible values. An Int variable, for example, can have any of over four billion values, and the number of different values possible for a String is vastly greater than that.

It simply isn’t practical to enumerate all possible values of these fundamental types, and if that’s the case, it follows that it also won’t be practical to enumerate all the possible states for an instance of any class composed of those types.

However, there are situations in which the number of possible values for something is relatively small—small enough that they can be enumerated easily. Take days of the week, for example. There are only seven possible values here: Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday.

We could represent days of the week using an Int with a value between 1 and 7, but how do we restrict it to that range? Another problem is that Int supports operations such as multiplication or division that clearly don’t make any sense for days of the week:

val today = 1               // Monday (minimum value)
val yesterday = today - 1   // has value 0, which isn't a day
val day = today * 3         // what does this even mean?

Like many other programming languages, Kotlin offers a better solution: a way of creating special enumerated types that have a predefined set of possible values.

An Example

In Kotlin, we can represent days of the week by defining them as an enum class:

enum class Day {
    Monday, Tuesday, Wednesday, Thursday,
    Friday, Saturday, Sunday
}

Here, the possible values for a Day variable are provided in the body of the class, as a comma-separated list of constants. For example, Monday is a constant, of type Day. Monday is both the name of this constant, and its value1.

When you use one of these constants in your code, you’ll need to be explicit about its type:

val day = Monday        // 'Unresolved reference' error

val day = Day.Monday    // OK

if (day == Day.Monday) {
    println("Another week begins...")
}

If that becomes too tedious, you could import the constants from the class by putting the following at the top of the .kt file:

import Day.*

This would allow you to refer to them more simply, as Monday, Tuesday, etc.

Built-in Properties & Methods

Enum constants each have a property name, which gives the constant’s name as a string, and a property ordinal, which gives the zero-based position of the constant in the enum’s list of constants:

Day.Monday.name      // "Monday"
Day.Monday.ordinal   // 0
Day.Sunday.ordinal   // 6

Another property, entries, is associated with the enum class, rather than individual constants. This represents all of the constants as a collection, which can be useful if you need to iterate over all possible values:

for (day in Day.entries) {
    // task that needs to be performed for each day
}

A useful method associated with the class is valueOf(). This will parse the given string, returning the enum constant that matches that string:

val day = Day.valueOf(dayString)

Task 12.8.1

  1. Edit Enum.kt, in the tasks/task12_8_1 subdirectory of your repository. Add to this file the enum class for days of the week shown above.

  2. Add to Enum.kt a main() function that asks the user to enter a day, reads that day as a string, then attempts to parse it using Day.valueOf().

  3. Compile and run the program. Try entering Monday as the string. Run it again, this time entering monday. What happens?

  4. Modify the program so that it handles errors more gracefully and indicates to the user what their options are.

    Hints

    Refer to the earlier discussion of exceptions if you need a reminder of how to handle run-time errors gracefully.

    You can use the entries property to get the options for specifying a day.

A More Complex Example

You can create enum classes with additional properties and methods. Every enum constant will acquire those properties, initialized to the values you supply as constructor arguments.

As an example, let’s imagine that you are creating software that plays games using playing cards. A standard 52-card deck contains of four suits: Clubs (♣), Diamonds (♦), Hearts (♥) and Spades (♠). Each suit consists of cards with thirteen different ranks: Ace, Two, Three, etc, up to Ten, followed by Jack, Queen and King.

Here’s how you could represent card suits as an enum class, in which each constant has a symbol property representing the Unicode character of the suit:

enum class Suit(val symbol: Char) {
    Clubs('♣'),
    Diamonds('♦'),
    Hearts('♥'),
    Spades('♠');

    val plainSymbol get() = name[0]

    override fun toString() = "$symbol"
}

The standard syntax is used to specify both the property and a primary constructor to initialize that property. The property is of type Char, so a Char value must be supplied when creating each enum constant in the body of the class definition.

Notice that this enum class has two additional features, besides the enum constants. First is a computed property, plainSymbol. This provides a simpler symbol for each suit: the first character of each constant’s name. This can be used in situations where it might not be possible to display the Unicode symbols.

The second additional feature is an overridden toString() method. All enum classes have a default implementation of toString() that returns an enum constant’s name, but this can overridden if required. The new version of the method shown here returns a string containing the Unicode symbol of the suit.

Note

Did you spot the semicolon that has now appeared at the end of the list of constants?

You need this if you are adding anything extra to the enum class, besides the list of constants. This is one of the few occasions where semicolons are necessary in Kotlin code!

Task 12.8.2

This uses an Amper project, in the tasks/task12_8_2 subdirectory of your repository.

  1. Examine Suit.kt, in the src subdirectory of the project. This file contains the enum class for playing card suits discussed above.

  2. Edit the file Rank.kt. Add to this file an enum class named Rank that represents the rank of a playing card. Like Suit, this should give each enum constant a symbol property. Use 'A', '2', '3',…, '9', 'T', 'J', 'Q', 'K' as values for this property.

    Check that this new code compiles, using

    ./amper build
    

    Repeat this command after each of the steps that follow.

  3. Give Rank an overridden toString() method just like the one for Suit.

  4. Edit the file Card.kt. In this file, create a class named Card with a primary constructor that defines and initializes two properties: rank, of type Rank, and suit, of type Suit.

  5. Add to the Card class a computed property fullName, which provides the full name of a card, as a String object.

    It should do this by concatenating the name of the card’s rank, the string " of ", and the name of the card’s suit. Thus, if the rank property has the value Rank.Ace and the suit property has the value Suit.Clubs, fullName should be "Ace of Clubs".

  6. Override toString() in Card so that it returns a two-character string consisting of the rank’s symbol and the suit’s symbol. So if the rank property has the value Rank.Ten and the suit property has the value Suit.Hearts, the string "T♥" should be returned.

  7. Edit Main.kt. In this file, write a program that

    • Creates a mutable list of Card objects, to represent a deck of cards
    • Populates that list with a full set of 52 standard playing cards
    • Shuffles the deck randomly
    • Prints the full name of each card in the shuffled deck

    Check program behaviour by running it with

    ./amper run
    

    Hints

    You can use Suit.entries and Rank.entries, in combination with nested for loops or forEach function calls, to populate the list.

    Remember that lists in Kotlin have a shuffle() extension function…

Nested Classes

If you’ve completed the tasks above, you should have enum classes Rank and Suit, plus a regular class Card that uses those enum classes. There is one further improvement we can make to this implementation of playing cards.

If you think about it, the concepts of ‘rank’ and ‘suit’ have no separate meaning outside the context of playing cards. We are unlikely to ever write code that works with ranks or suits on their own, without there being a Card object somewhere.

We can model the intimacy of this relationship between Rank, Suit and Card by nesting the definitions of the enum classes inside the definition of the Card class:

class Card(val rank: Rank, val suit: Suit) {

  enum class Rank { ... }

  enum class Suit { ... }

  ...
}

Kotlin allows class definitions to be nested within other class definitions. It allows you to do the same with functions, too2.

The practical impact of this change is that code outside the Card class will need to refer to the enums as Card.Rank and Card.Suit:

for (suit in Card.Suit.entries) {
    ...
}

However, this can be avoided by importing the enum classes from Card:

import Card.Rank
import Card.Suit

Optional Task Extension

If you like, try modifying your Task 12.8.2 solution so that the Rank and Suit enums are nested inside the Card class, as described above.

Make sure that this new solution compiles and runs successfully.


  1. We’ve used upper camel case as the naming style for these enum constants, which is one of the two accepted styles in the official Kotlin coding conventions. The other accepted style is ‘screaming snake case’, in which constant names are written using capital letters and underscores. You’ll see examples of both styles in Kotlin code.

  2. In general, you can limit the scope of a function or class to a single block of code by defining it within that block of code.