Type systems are a set of rules that determine the type of a language construct.
A (good) type system will help you with:
- Making sure that the constituent parts of your program are connected in a consistent way
- Understanding your program (by reducing your cognitive load)
- Expressing business rules
- Automatic low-level optimizations
We have already covered enough ground to understand Kotlin's type system.
All types in Kotlin extend from the Any
type (hold on a second, actually this isn't true but for the sake of the explanation, bear with me).
Every class and interface that we create implicitly extends Any
. So, if we write a method that takes Any
as a parameter, it will receive any value:
fun main(args: Array<String>) { val myAlmondCupcake = Cupcake.almond() val anyMachine = object : Machine<Any> { override fun process(product: Any) { println(product.toString()) } } anyMachine.process(3) anyMachine.process("") anyMachine.process(myAlmondCupcake) }
What about a nullable value? Let's have a look at it:
fun main(args: Array<String>) { val anyMachine = object : Machine<Any> { override fun process(product: Any) { println(product.toString()) } } val nullableCupcake: Cupcake? = Cupcake.almond() anyMachine.process(nullableCupcake) //Error:(32, 24) Kotlin: Type mismatch: inferred type is Cupcake? but Any was expected }
Any
is the same as any other type and also has a nullable counterpart, Any?
. Any
extends from Any?
. So, in the end, Any?
is the top class of Kotlin's type system hierarchy.
Due to its type inference and expression evaluation, sometimes there are expressions in Kotlin where it is not clear which type is being returned. Most languages resolve this problem by returning the minimum common type between the possible type options. Kotlin takes a different route.
Let's take a look at an example of an ambiguous expression:
fun main(args: Array<String>) { val nullableCupcake: Cupcake? = Cupcake.almond() val length = nullableCupcake?.eat()?.length ?: "" }
What type does length
have? Int
or String
? No, length
value's type is Any
. Pretty logical. The minimum common type between Int
and String
is Any
. So far, so good. Let's look at the following code now:
val length = nullableCupcake?.eat()?.length ?: 0.0
Following that logic, in this case, length
should have the Number
type (the common type between Int
and Double
), shouldn't it?
Wrong, length
is still Any
. Kotlin doesn't search for the minimum common type in these situations. If you want a specific type, it must be explicitly declared:
val length: Number = nullableCupcake?.eat()?.length ?: 0.0
Kotlin doesn't have methods with void
return (as Java or C do). Instead, a method (or, to be precise, an expression) could have a Unit
type.
A Unit
type means that the expression is called for its side effects, rather than its return. The classic example of a Unit
expression is println()
, a method invoked just for its side effects.
Unit
, like any other Kotlin type, extends from Any
and could be nullable. Unit?
looks strange and unnecessary, but is needed to keep consistency with the type system. Having a consistent type system have several advantages, including better compilation times and tooling:
anyMachine.process(Unit)
Nothing
is the type that sits at the bottom of the entire Kotlin hierarchy. Nothing
extends all Kotlin types, including Nothing?
.
But, why do we need a Nothing
and Nothing?
types?
Nothing
represents an expression that can't be executed (basically throwing exceptions):
val result: String = nullableCupcake?.eat() ?: throw RuntimeException() // equivalent to nullableCupcake!!.eat()
On one hand of the Elvis operator, we have a String
. On the other hand, we have Nothing
. Because the common type between String
and Nothing
is String
(instead of Any
), the value result
is a String
.
Nothing
also has a special meaning for the compiler. Once a Nothing
type is returned on an expression, the lines after that are marked as unreachable.
Nothing?
is the type of a null value:
val x: Nothing? = null val nullsList: List<Nothing?> = listOf(null)