‘when’ expression in Kotlin VS ‘switch’ statement in Java
Switch in Java
Consider the following code in Java
void action(Bar bar) {
Foo f;
switch(bar) {
case Bar.VALUE:
f = value42();
case Bar.ELSE:
f = value444();
default:
throw ThisCanNeverHappenException();
}
doSomeMightyWork(f);
}
Widespread pattern, isn’t it? There are several things wrong with it! First, Foo f
variable is not final,
so it can be ocassionally changed in the future after some code changes.
Next, we switll have default
branch which is not necessary as we cover all enum values. But if
someone dares to add extra enum value, this code will still compile correctly!
Enough problems? Nope! I forgot break;
in each case
branch. Surprise.
An improvement is still possible to solve some of the problems above. Use return
statement! So we have
Foo select(Bar bar) {
switch(bar) {
case Bar.VALUE:
return value42();
case Bar.ELSE:
return value444();
default:
throw ThisCanNeverHappenException();
}
}
void action(Bar bar) {
final Foo f = select(bar);
doSomeMightyWork(f);
}
Is it better now? More or less yes. But would you create an additional function for every switch
? I’m not sure.
When in Kotlin
The equivalent constuction to switch
in Java is when
in Kotlin. You may take a look to the
when expression documentation
for more details. Note, when
expression is more functional than switch
in Java.
fun action(bar: Bar) {
var f : Foo
when(bar) {
Bar.VALUE ->
f = value42()
Bar.ELSE ->
f = value444()
else ->
throw ThisCanNeverHappenException()
}
doSomeMightyWork(f)
}
This was just one-to-one conversion to Kotlin and I see the warning to convert var f
to val f
at first.
It is good to notice, when
can be used as expression! We may avoid assigning f
variable inside each case.
The right of ->
is also an expression. If you need more things to do, use {
and }
. There is no break
exists or required.
If when
is used as expression, else
branch is not necessary, the compiler can prove all branches are included.
At least this works if you checking enum or
sealed classes.
Compilation fails if a missing branch is detected.
Overall I turn the example into this:
void action(bar: Bar) {
val f = when(bar) {
Bar.VALUE -> value42();
Bar.ELSE -> value444();
}
doSomeMightyWork(f);
}
Looks concise, isn’t it?
Generated bytecode
Let’s look into bytecode for the when
statement call. Note. I use IntelliJ IDEA 2017.1 EAP with Kotlin
1.0.6 plugin. The generated bytecode may change with a future version of tools.
// access flags 0x19
public final static action(LBar;)V
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 0
LDC "e"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 13 L1
ALOAD 0
GETSTATIC WhKt$WhenMappings.$EnumSwitchMapping$0 : [I
SWAP
INVOKEVIRTUAL Bar.ordinal ()I
IALOAD
LOOKUPSWITCH
1: L2
2: L3
default: L4
L2
LINENUMBER 14 L2
INVOKESTATIC WhKt.value42 ()LFoo;
GOTO L5
L3
LINENUMBER 15 L3
INVOKESTATIC WhKt.value444 ()LFoo;
GOTO L5
L4
NEW kotlin/NoWhenBranchMatchedException
DUP
INVOKESPECIAL kotlin/NoWhenBranchMatchedException.<init> ()V
ATHROW
L6
LINENUMBER 13 L6
L5
ASTORE 1
L7
LINENUMBER 18 L7
ALOAD 1
INVOKESTATIC WhKt.doSomeMightyWork (LFoo;)V
L8
LINENUMBER 19 L8
RETURN
L9
LOCALVARIABLE f LFoo; L7 L9 1
LOCALVARIABLE e LBar; L0 L9 0
MAXSTACK = 2
MAXLOCALS = 2
From the bytecode we see it throws kotlin/NoWhenBranchMatchedException
in case it turned out
there is an additional case. For example, this may happen if we are running a compiled code
againt updated Bar
enum with additional case added.
Still, a re-compilation will show there is an errors.
The only problem is that NoWhenBranchMatchedException
contains no information on what
kind of instance was there, maybe a toString()
or getClass().getName()
were nice to
have helpers. Still, in general, such calls may also throw exceptions.
Conclusion
In this post we see how when
expression can help one avoid trivial errors
in switches. We discussed the benefit of using when
as expression