1. Overview

This tutorial introduces the when{} block in Kotlin language and demonstrates the various ways that we can use it.

To understand the material in this tutorial, we need basic knowledge of the Kotlin language. Have a look at the introduction to the Kotlin language article on Baeldung to learn more about the language.

2. Kotlin’s when{} Block

when{} block is essentially an advanced form of the switch-case statement known from Java.

In Kotlin, if a matching case is found, only the code in the respective case block is executed, and execution continues with the next statement after the when block.

This essentially means that we don’t need break statements at the end of each case block.

To demonstrate the usage of when{}, let’s define an enum class that holds the first letter in the permissions field for some of the file types in Unix:

enum class UnixFileType {
    D, HYPHEN_MINUS, L
}
Let’s also define a hierarchy of classes that model the respective Unix file types:
sealed class UnixFile {

    abstract fun getFileType(): UnixFileType

    class RegularFile(val content: String) : UnixFile() {
        override fun getFileType(): UnixFileType {
            return UnixFileType.HYPHEN_MINUS
        }
    }

    class Directory(val children: List<UnixFile>) : UnixFile() {
        override fun getFileType(): UnixFileType {
            return UnixFileType.D
        }
    }

    class SymbolicLink(val originalFile: UnixFile) : UnixFile() {
        override fun getFileType(): UnixFileType {
            return UnixFileType.L
        }
    }
}

2.1. when{} as an Expression

A big difference from Java’s switch statement is that we can use the when{} block in Kotlin both as a statement and as an expression. Kotlin follows the principles of other functional languages. Flow-control structures are expressions and the result of their evaluation can be returned to the caller.

If the value returned is assigned to a variable, the compiler will check that the type of the return value is compatible with the type expected by the client, and it will inform us if it is not:

@Test
fun testWhenExpression() {
    val directoryType = UnixFileType.D

    val objectType = when (directoryType) {
        UnixFileType.D -> "d"
        UnixFileType.HYPHEN_MINUS -> "-"
        UnixFileType.L -> "l"
    }

    assertEquals("d", objectType)
}

There are two things to notice when using when as an expression in Kotlin.

First, the value that is returned to the caller is the value of the matching case block or, in other words, the last defined value in the block.

The second thing to notice is that we need to guarantee that the caller gets a value. For this to happen, we need to ensure that the cases in the when block cover every possible value that can be assigned to the argument.

2.2. when{} as an Expression With Default Case

A default case will match any argument value that is not matched by a normal case and in Kotlin is declared using the else clause.

In any case, the Kotlin compiler will assume that every possible argument value is covered by the when block and will complain if it is not.

Here’s how to add a default case in Kotlin’s when expression:

@Test
fun testWhenExpressionWithDefaultCase() {
    val fileType = UnixFileType.L

    val result = when (fileType) {
        UnixFileType.L -> "linking to another file"
        else -> "not a link"
    }

    assertEquals("linking to another file", result)
}

2.3. when{} Expression With a Case That Throws an Exception

In Kotlin, throw returns a value of type Nothing.

In this case, we use Nothing to declare that the expression failed to compute a value. Nothing is the type that inherits from all user-defined and built-in types in Kotlin.

Therefore, since the type is compatible with any argument that we would use in a when block, it is perfectly valid to throw an exception from a case even if we use the when block as an expression.

Let’s define a when expression where one of the cases throws an exception:

@Test(expected = IllegalArgumentException::class)
fun testWhenExpressionWithThrowException() {
    val fileType = UnixFileType.L

    val result: Boolean = when (fileType) {
        UnixFileType.HYPHEN_MINUS -> true
        else -> throw IllegalArgumentException("Wrong type of file")
    }
}

2.4. when{} Used as a Statement

We can also use the when block as a statement.

In this case, we don’t need to cover every possible value for the argument, and the value computed in each case block, if any, is just ignored. As a statement, we can use the when block similarly to how we use the switch statement in Java.

Let’s look at the when block as a statement:

@Test
fun testWhenStatement() {
    val fileType = UnixFileType.HYPHEN_MINUS

    when (fileType) {
        UnixFileType.HYPHEN_MINUS -> println("Regular file type")
        UnixFileType.D -> println("Directory file type")
    }
}

We can see that it is not mandatory to cover all possible argument values when we are using when as a statement.

2.5. Combining when{} Cases

Kotlin’s when expression allows us to combine different cases into one by concatenating the matching conditions with a comma.

Only one case has to match to execute the respective block of code, so the comma acts as an OR operator.

Let’s create a case that combines two conditions:

@Test
fun testCaseCombination() {
    val fileType = UnixFileType.D

    val frequentFileType: Boolean = when (fileType) {
        UnixFileType.HYPHEN_MINUS, UnixFileType.D -> true
        else -> false
    }

    assertTrue(frequentFileType)
}

2.6. when{} Used Without an Argument

Kotlin allows us to omit the argument value in the when block.

This essentially turns when into a simple if-elseif expression that sequentially checks cases and runs the block of code of the first matching case. If we omit the argument in the when block, the case expressions should evaluate as either true or false.

Let’s create a when block that omits the argument:

@Test
fun testWhenWithoutArgument() {
    val fileType = UnixFileType.L

    val objectType = when {
        fileType === UnixFileType.L -> "l"
        fileType === UnixFileType.HYPHEN_MINUS -> "-"
        fileType === UnixFileType.D -> "d"
        else -> "unknown file type"
    }

    assertEquals("l", objectType)
}

2.7. Dynamic Case Expressions

In Java, we can only use the switch statement with primitives and their boxed types, enums and the String class.

In contrast, Kotlin allows us to use the when block with any built-in or user-defined type.

In addition, the cases don’t need to be constant expressions, as in Java. Cases in Kotlin can be dynamic expressions that are evaluated at runtime. For example, cases could be the result of a function as long as the function return type is compatible with the type of the when block argument.

Let’s define a when block with dynamic case expressions:

@Test
fun testDynamicCaseExpression() {
    val unixFile = UnixFile.SymbolicLink(UnixFile.RegularFile("Content"))

    when {
        unixFile.getFileType() == UnixFileType.D -> println("It's a directory!")
        unixFile.getFileType() == UnixFileType.HYPHEN_MINUS -> println("It's a regular file!")
        unixFile.getFileType() == UnixFileType.L -> println("It's a soft link!")
    }
}

2.8. Range and Collection Case Expressions

It is possible to define a case in a when block that checks if a given collection or a range of values contains the argument.

For this reason, Kotlin provides the in operator, which is syntactic sugar for the contains() method. This means that behind-the-scenes Kotlin translates the case element in to collection.contains(element).

Here’s how to check if the argument is in a list:

@Test
fun testCollectionCaseExpressions() {
    val regularFile = UnixFile.RegularFile("Test Content")
    val symbolicLink = UnixFile.SymbolicLink(regularFile)
    val directory = UnixFile.Directory(listOf(regularFile, symbolicLink))

    val isRegularFileInDirectory = when (regularFile) {
        in directory.children -> true
        else -> false
    }

    val isSymbolicLinkInDirectory = when {
        symbolicLink in directory.children -> true
        else -> false
    }

    assertTrue(isRegularFileInDirectory)
    assertTrue(isSymbolicLinkInDirectory)
}
Now we’ll check that the argument is in a range:
@Test
fun testRangeCaseExpressions() {
    val fileType = UnixFileType.HYPHEN_MINUS

    val isCorrectType = when (fileType) {
        in UnixFileType.D..UnixFileType.L -> true
        else -> false
    }

    assertTrue(isCorrectType)
}

Even though REGULAR_FILE type is not explicitly contained in the range, its ordinal is between the ordinals of DIRECTORY and SYMBOLIC_LINK, and therefore the test is successful.

2.9. is Case Operator and Smart Cast

We can use Kotlin’s is operator to check if the argument is an instance of a specified type. The is operator is similar to the instanceof operator in Java.

However, Kotlin provides us with a feature called smart cast. After we check if the argument is an instance of a given type, we don’t have to explicitly cast the argument to that type since the compiler does that for us.

Therefore, we can use the methods and properties defined in the given type directly in the case block.

Let’s use the is operator with the smart cast feature in a when block:

@Test
fun testWhenWithIsOperatorWithSmartCase() {
    val unixFile: UnixFile = UnixFile.RegularFile("Test Content")

    val result = when (unixFile) {
        is UnixFile.RegularFile -> unixFile.content
        is UnixFile.Directory -> unixFile.children.map { it.getFileType() }.joinToString(", ")
        is UnixFile.SymbolicLink -> unixFile.originalFile.getFileType()
    }

    assertEquals("Test Content", result)
}
Without explicitly casting unixFile to RegularFile, Directory or SymbolicLink, we were able to use RegularFile.content, Directory.children and SymbolicLink.originalFile respectively.

2.10. when Expressions and Loops

As of Kotlin 1.4, it’s possible to break or continue a loop even inside a when expression:

val colors = setOf("Red", "Green", "Blue")
for (color in colors) {
    when(color) {
        "Red" -> break
        "Green" -> continue
        "Blue" -> println("This is blue")
    }
}

Here break terminates the nearest enclosing loop, and continue proceeds to the next step, as expected.

Before Kotlin 1.4, however, only qualified break and continue were allowed in a when expression inside a loop:

LOOP@ for (color in colors) {
    when(color) {
        "Red" -> break@LOOP
        "Green" -> continue@LOOP
        "Blue" -> println("This is blue")
    }
}

As shown above, the break and continue are qualified with the @LOOP expression.

3. Multiple Statements in when{} Block

In this section, we’ll learn how to execute multiple statements in the when{} block for each of its branches. For this purpose, let’s imagine a use case where we want to evaluate if a given integer is positive and print self-explanatory statements when it’s positive, negative, or zero.

First, let’s go ahead and write the isPositiveInt() function that accepts an Int value and returns a Boolean value:

fun isPositiveInt(number: Int): Boolean {
    val result = when (number) {
        0 -> {
            println("number is zero.")
            print("It's neither positive nor negative.")
            return false
        }
        in -1 downTo Int.MIN_VALUE -> {
            print("number is negative")
            return false
        }
        else -> {
            print("number is positive")
            return true
        }
    }
    return result
}

As we can observe, we have accounted for three different intervals for an integer value: positive, negative, and zero. Further, we used the curly braces to add multiple statements for each branch.

Next, let’s prepare to test the isPositiveInt() function by capturing the standard output stream into an instance of ByteArrayOutputStream:

val outputStream = ByteArrayOutputStream()
System.setOut(PrintStream(outputStream))

Lastly, let’s verify our method for a scenario where the given number is zero:

val givenNumber = 0
val expectedOutput = "number is zero.\nIt's neither positive nor negative."
val isPositive = isPositiveInt(givenNumber)
assertFalse(isPositive)
assertEquals(expectedOutput, outputStream.toString())

Great! It works as expected for this scenario. Similarly, we can also validate the isPositiveInt() function for positive and negative values.

4. Conclusion

In this article, we saw several examples of how to use the when block offered by the Kotlin language.

Even though it’s not possible to do pattern matching using when in Kotlin, as is the case with the corresponding structures in Scala and other JVM languages, the when block is versatile enough to make us totally forget about these features.

The complete implementation of the examples for this article can be found over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.