1. Introduction
This tutorial introduces the when{} block in Kotlin language and demonstrates the various ways that it can be used.
To understand the material in this article, basic knowledge of the Kotlin language is needed. You can 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 then 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 no break statements are needed in 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 }
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 the Java’s switch statement is that the when{} block in Kotlin can be used both as a statement and as an expression. Kotlin follows the principles of other functional languages and 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 type of the return value is compatible with the type expected by the client and will inform us in case 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 in case it is not.
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, Nothing is used 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 the when block is used 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 do not need to cover every possible value for the argument and the value computed in each case block, if any, is just ignored. When used as a statement, the when block can be used similarly to how the switch statement is used in Java.
Let’s use 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 from the example 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 for the respective block of code to be executed, so 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 in a simple if-elseif expression that sequentially checks cases and executes the block of code of the first matching case. If we omit the argument in the when block, then the case expressions should evaluate to 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, the switch statement can only be used 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, it is not required that the cases are 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 a syntactic sugar for the contains() method. This means that Kotlin behind the scenes translates the case element in to collection.contains(element).
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) }
@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 do not 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.
To 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) }
3. Conclusion
In this article, we have seen 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.