🏎️ · When expression

7 min read Β· Updated on by

Read on to understand the difference between expressions and statements, and why Kotlin's when construct is a more powerful version of Java's switch.

Expressions vs. statements

What is and isn’t an expression/statement depends on the language.

A simple way of understanding the difference between expressions and statements is this:

  • Expressions in a given language are anything that you can legally use as an argument to a function.
  • Statements are everything else

Intuitively, that might seem to be the same as saying β€œanything that can be on the right hand side of an assignment”, but things get funky when semicolons get involved β€”Β 3Β is an expression,Β 3;Β is a statement.

Reader:Β β€œBut in Java,Β Integer a = 3;Β is legal!”
Author:Β β€œBut that’s because theΒ ;Β is not actually part of the assignment in theΒ AST, as can be seen inΒ String abc = β€œabc”, yes = β€œyes”;β€œ
Reader:Β *rage-quits*

….so I'm purposefully avoiding that definition.

For example: in Java,Β if/elseΒ is a statement, because you can’t writeΒ myFun(if(true) { ... } else { ... })Β β€” this wouldn’t compile. However,Β 4Β andΒ myFun(3).add(5)Β are both expressions, and you'll soon learn that in Kotlin, it’s legal to use anΒ ifΒ as an expression as well!

Most importantly, expressions can be used in place of statements. The reverse is never true.

When statement


//sampleStart
fun main() {
    cases("Hello")
    cases(1)
    cases(3)
    cases(0L)
    cases(MyClass())
    cases("hello")
}
// The Any type in Kotlin is the same as 'Object' in Java
fun cases(obj: Any) {                                
    when (obj) { // 1.
        1, 3 -> println("One or Three") // 2.
        "Hello" -> println("Greeting") // 3.
        is Long -> { // 4.
            val comparison: Int = obj.compareTo(5)
            println(comparison)
        }
        !is String -> println("Not a string") // 5.
        else -> println("Unknown") // 6.
    }   
}
//sampleEnd

class MyClass
  1. This is a when statement (well, technically, it's an expression, but its value is ignored, so iate cold now my tooth hurtst's basically like a statement)
  2. Checks whether obj equals 1 or 3
  3. Checks whether obj equals Hello
  4. Performs type checking. The right-hand side can be a block of code, delimited byΒ {Β andΒ }. Notice howΒ objΒ is automatically cast toΒ Long. This feature is calledΒ smart casting, and we’ll talk about it more in the article aboutΒ Types.
  5. Performs inverse type checking
  6. The default branch. It is optional in certain situations (the above code is one of them) and required in others. These situations will become clear in time, don’t worry about them for now. The compiler will let you know if the branch is missing and you need to include it.

All branch conditions are checked sequentially until one that is satisfied is found. As a consequence, only the first suitable branch will be executed.

You don't need to worry about any of the following, but for those who are curious: the default branch is required when when (hehe) is used as an expression, and the other branches do not provably cover all possible scenarios - in other words, the expression doesn't always evaluate to a definitive value.

One of the defining features of sealed hierarchies, which we'll discuss in a future lesson, is that they allow exhaustive type-checking - that is, if you check for all children, you don't need to add a default branch.

Regular classes do not share this property, because you can't be certain that the class that is being checked won't gain an additional child in the future. For example, imagine creating a library that provides additional functionality to some well known open source graphics library, and imagine this graphics library defines a Shape class, with only two children: Square and Circle. In your code, you write a when expression that covers both situations, compile your code, and release your library to great acclaim.

However, in a few weeks time, a new version of the graphics library comes out which includes a new Shape - Rectangle. You original code is already compiled, so it has no way of knowing that, and executing it with an instance of Rectangle would lead to undefined behavior - the code simply doesn't know what it should do what a Rectangle instance, since no such thing existed when it was compiled. This is why the compiler explicitly prevents these situations by always requiring a default branch when implementing type-checking.

In the lesson on sealed classes, you'll find out why this requirement is not necessary for them.

When expression


//sampleStart
fun main() {
    println(whenAssign("Hello"))
    println(whenAssign(3.4))
    println(whenAssign(1))
    println(whenAssign(2L))
    println(whenAssign(MyClass()))
}

fun whenAssign(obj: Any): Any {
    val result = when (obj) { // 1
        1 -> "one" // 2
        "Hello" -> 1 // 3
        is Long -> {
            val someNum: Long = 5L
            obj > someNum // 4
        }
        else -> 42 // 5
    }
    return result
}
//sampleEnd

class MyClass
  1. This is a when expression. It must always evaluate to a specific value (try omitting the else branch and see what happens)
  2. Sets the value of result to "one" if obj equals to one.
  3. Sets the value of result to 1 if obj equals to Hello.
  4. Sets the value to obj > 5 if obj is an instance of Long. Notice how, as before, you can use code blocks β€” the last expression in a code block is taken to be the value of the entire code block (and this is true generally, not only in when expressions). Also notice how, as before, obj gets smart-casted to Long.
  5. Sets the value β€œ42” if none of the previous conditions are satisfied. Unlike in aΒ whenΒ statement, the default branch is usually required in aΒ whenΒ expression, except for cases where the compiler can verify that the branches already cover all possible cases (more on this when we coverΒ Sealed Classes).

When conditions

The following can be used in a when condition:

Expressions

The check succeeds if the when subject (the value being tested) is equal to the expression


import kotlin.random.Random

//sampleStart
fun main() {
    test(1)
    test(2)
    test(3)
    test(4)
    test(5)
}

fun test(number: Int) = when(number) {
    1 -> println("It is one")
    Random.nextInt(1, 5) -> println("Ooo, you got lucky!")
    number - 1 -> println("This will never execute")
    number % 4 -> println("Number is equal to itself modulo 4")
    else -> println("Some other kind of number")
}    
//sampleEnd

in and !in

The check succeeds if the value is in (or !in) the right hand side of the operator. This is very useful when testing for the presence in a collection, or a range (we'll talk about ranges in a future lesson).


import kotlin.random.Random

//sampleStart
fun main() {
    test(1)
    test(2)
    test(3)
    test(4)
    test(5)
}

fun test(number: Int) = when(number) {
    in setOf(1, 3, 5) -> println("It is either one, three or five")
    // The next line uses ranges, which we'll discuss in a future lesson. 
    // Don't worry about understanding them for now!
    !in 1..10 -> println("It's not one of the first 10 numbers")
    in listOf(2, 4, 6, 8) -> println("It is either two, four, six or eight")
    else -> println("Some other kind of number")
}    
//sampleEnd

is and !is

The check succeeds if the value is (or isn't) an instance of the given type.


//sampleStart
fun main() {
    classify("abc")
    classify(1)
    classify(listOf(1, 2, 3))
}

fun classify(input: Any) = when(input) {
    is Int -> println("It's an integer")
    is String -> println("It's a string")
    else -> println("It's something else")
}    
//sampleEnd

A combination of the above

Multiple conditions can be specified by separating them with a comma. The check succeeds if at least one of the conditions matches, i.e. the comma represents OR.


//sampleStart
fun main() {
    classify("abc")
    classify(1)
    classify(2)
    classify(listOf(1, 2, 3))
}

fun classify(input: Any) = when(input) {
    is Int, "a" -> println("It's either an integer, or the string \"a\"")
    is String, in 1..10 -> println("It's either a string, or between 1 and 10")
    else -> println("It's something else")
}    
//sampleEnd

Check out the docs to find out about how to replace long if-else chains by omitting the when subject, or how to capture the subject in a separate variable.


Frequently Asked Questions

How can I include multiple conditions in a when?

To include multiple conditions in a single branch, e.g. "is a String" and "is in the range 1..10", separate the conditions by a comma.


//sampleStart
fun main() {
    classify("abc")
    classify(1)
    classify(2)
    classify(listOf(1, 2, 3))
}

fun classify(input: Any) = when(input) {
    is String, in 1..10 -> println("It's either a string, or between 1 and 10")
    else -> println("It's something else")
}    
//sampleEnd

The branch will match if at least one of the conditions evaluates to true.

How do I use when with an enum?

You can use when with an enum like you would with any other type. Most often, you will test for equality to a certain value.


//sampleStart
enum class TrafficLight {
    RED, ORANGE, GREEN
}

fun main() {
    println(canIGo(TrafficLight.RED))
    println(canIGo(TrafficLight.ORANGE))
    println(canIGo(TrafficLight.GREEN))
}

fun canIGo(trafficLight: TrafficLight) = when (trafficLight) {
    TrafficLight.RED, TrafficLight.ORANGE -> false
    TrafficLight.GREEN -> true
}    
//sampleEnd

Notice how we didn't have to include the default branch since the compiler can prove that we covered all possible scenarios (since you can't add an enum value from outside of its definition).

We'll talk more about enum classes in a future lesson.


Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer