Kotlin Primer
Operators and Expressions
We won't go into detail on all operators and expressions, but a few in particular can help get you started.
Operators
Most of Java's operators are present in Kotlin, but many are backed by functions that you can override. We'll go over a few here that work differently than in Java.
Warning
I strongly recommend that you only ever override a few, such as get, set, and in.
These are useful to override when you define your own collections or want to provide
multi-value indexing into a data structure. Overriding others can cause confusion for
readers.
| Operator | Description |
|---|---|
in, !in |
Calls contains() to test if something is in (or not in) a collection |
is, !is |
Tests if an instance is (or is not) of a certain type |
as, as? |
Casts an instance to another type. as? is safe, returning a null if it cannot be cast. as will throw an exception |
== |
Compares the value of two expressions, evaluating to objects and using their equals() functions |
=== |
Compares the actual references of two expressions, i.e. do they point to the same object |
x[...] |
Indexes x using its get or set function (depending on if being used to access or change values) |
x..y |
Creates an inclusive Range. Can be used in a for loop (rarely used) or range check |
x..<y |
Creates a Range, inclusive of x and exclusive of y |
Control Expressions
The most important control expressions you'll use are if and when. You generally won't directly
use for, as you'll use functions such as forEach() that are defined on various data structures.
if works exactly as it does in Java, with the difference being it's an expression in Kotlin.
You can assign values to the result of the if expression:
fun main() {
val x = 10
val y =
if (x < 10) {
"Small"
} else {
"Large"
}
println(y)
}
Note
Kotlin doesn't have the "ternary operator" x ? y : z as defined in Java. Instead, you
must use an if expression. Often, if the if expression is very short and would have
been written as a ternary expression in Java, we one-line it and remove the curly braces:
fun main() {
val x = 10
val y = if (x < 10) "Small" else "Large"
println(y)
}
The when expression is similar to Java's switch, but:
- The "cases" in
whendo not fall through to the next case. - Kotlin applies smart-casting when available
whenallows arbitrary boolean expressions for cases
A few examples:
fun main() {
val x = 42
val result =
when (x) {
10 -> "Low"
42 -> "The answer"
100 -> "High"
else -> "I really don't care"
}
println(result)
}
That's a very direct, value-testing version. when is much more flexible when you don't
specify a value, and instead, use boolean expressions for each case.
fun main() {
val x = 42
val result =
when {
x < 42 -> "Low"
x == 42 -> "The answer"
else -> "High"
}
println(result)
}
If you use when as an expression or with , it must be exhaustive. All possibilities must be covered.
If you use it as a statement, such as
fun main() {
val x = 42
when {
x < 42 -> { println("Low") }
x == 42 -> { println("The answer") }
}
}
You don't need to be exhaustive.
Sealed Interfaces
We'll often be using when with sealed interfaces, which is an incredibly cool feature of
Kotlin.
Normally, when you define an interface, whether it's in Java or Kotlin, anyone who has
access to that interface can create a class that implements it. There's no way in your code
to know or control the full set of possible implementations.
A sealed interface locks down implementations to the "module" in which the sealed interface
was defined. A module is a single, separately-compiled compilation group, defining a library or
application, and modules can depend on other modules during compilation.
By locking down implementations to only inside the same module that defined the
sealed interface, we not only prevent other implementations, we can know all implementations
of that interface. This allows us to make when exhaustive across those implementations.
Let's start simple:
sealed interface Animal
sealed interface Mammal: Animal
sealed interface Fish: Animal
class Cat: Mammal
class Dog: Mammal
class Koi: Fish
class Humuhumunukunukuapuaa: Fish
fun main() {
val mammal: Animal = Humuhumunukunukuapuaa()
val result =
when(mammal) {
is Mammal -> "Mammal"
is Fish -> "Fishy fish"
}
println(result)
}
We define sealed interfaces to describe an Animal, which is either (and only) a Mammal or
a Fish. Our when allows us to be exhaustive just by specifying is Animal or is Fish.
We know that the only possible extensions of Animal are defined in our module. We can refine
these checks however we want:
sealed interface Animal
sealed interface Mammal: Animal
sealed interface Fish: Animal
class Cat: Mammal
class Dog: Mammal
class Koi: Fish
class Humuhumunukunukuapuaa: Fish
fun main() {
val mammal: Animal = Humuhumunukunukuapuaa()
val result =
when(mammal) {
is Mammal -> "Mammal"
is Humuhumunukunukuapuaa -> "Humu!"
is Koi -> "Koi!"
}
println(result)
}
This is still exhaustive. We have complete control.
So, why is this important?
First, there are plenty of times when defining an API where you really don't intend or want anyone
outside your module to extend types. sealed types prevent that.
Second, if we didn't have sealed interface (or sealed class, which is
similar), Kotlin will tell us we're not exhaustive and must add an else for other possible types
that might be defined.
interface Animal
interface Mammal: Animal
interface Fish: Animal
class Cat: Mammal
class Dog: Mammal
class Koi: Fish
class Humuhumunukunukuapuaa: Fish
fun main() {
val mammal: Animal = Humuhumunukunukuapuaa()
val result =
when(mammal) {
is Mammal -> "Mammal"
is Fish -> "Fishy fish"
else -> "Unknown"
}
println(result)
}
Let's say later on that we add a new type, Bird. Our existing code will keep working. That may
seem nice, but that makes it really easy to miss things we need to change when adding that
Bird.