Read on to discover a detailed explanation of the problems in the Java type system, and how Kotlin fixes them using Any
, Unit
and Nothing
.
The topic of types can be confusing to newcomers, and I don’t want to bog you down with the details when you’re just starting to learn. However, it is still important to understand what’s really going on. Therefore, I split this section into two parts.
Basics
- Kotlin does not have primitive types — everything is an object. There is no
int
, onlyInt
. This does not affect performance, as these types are automatically compiled to primitive values whenever possible. - The equivalent of the Java
Object
type is calledAny
in Kotlin - Instead of
void
, useUnit
- If you ever encounter
Nothing
, read the next section
Kotlin has type inference — in some situations, types can be omitted:
- when using
=
, i.e. when assigning a value to a variable or when defining a function as an expression - when a function returns
Unit
- after a type check
Type casts are performed using the keyword as
. If the cast is illegal, a ClassCastException
is raised. Alternatively, you can use the safe cast operator as?
, which evaluates to null
if it fails.
//sampleStart fun main() { // val test1 = 3 as String // Throws ClassCastException val test2 = 3 as? String // test2 is now null } //sampleEnd
Kotlin has function types. The type (Int, String) -> Int
represents a function that takes two arguments, an Int
and a String
, and returns an Int
. A lot more will be said about function types, lambdas and higher order functions in a future lesson. Don’t worry about them for now.
Any, Unit, Nothing
If you want, skip this and come back to it when you encounter type-related behavior you don’t understand.
The problems of Java types
The type system in Java has three large holes in it:
- There is no type that represents “no value returned”. Java has
void
, but that is not a type - you cannot writex instanceof void
,void.class
, etc. - There is no type that represents “
null
value returned”. Try writingpublic ??? iReturnNull() { return null; }
— what should you put instead of the???
? It turns out that Java will accept any non-primitive value in place of???
, i.e. any class. - There is no type that represents “value not returned”. This is not the same as
void
! Compare the following:
public void iReturnNoValue() { }
public ??? iDontReturnAtAll() { throw new RuntimeException(); }
public ??? iDontReturnAtAll2() { while(true) { } }
Java will accept anything in the place of ???
, including primitive types.
There are many consequences of these problems:
- Type checking is much weaker, which makes the code error prone (e.g. NPE’s)
- Co/contra-variance don’t work properly. For example, there is no reasonable way to define the type of an empty list. Think about it — a list of
Int
s isList<Int>
and a list ofString
s isList<String>
, and you can never interchange the two except when the lists are empty — an empty list is an empty list, it does not matter of what, there’s nothing in it. So whatever it is, it has to be useable in place of anyList<T>
, which means being a subtype ofList<T>
for anyT
, which is impossible in Java. - You cannot write
Function<T, void>
orFunction<void, T>
. Lambdas which return no value (or accept no value) need completely separate types (Supplier
,Consumer
) - Combining the previous with the horror of checked exceptions, it is impossible to have reasonable functional types in Java, which in turn makes working with higher order functions (
map
,flatMap
,filter
etc.) very unwieldy
The Kotlin type system is designed to remedy these problems. It’s type hierarchy is structurally very similar to the Java type hierarchy, with four notable differences:
- The
Any
type - The
Unit
type/object - The
Nothing
type - Nullable types (discussed previously)
Thanks to these, in Kotlin, every expression always has a single, well-defined type. This is not true in Java.
The Any type
The Any
type in Kotlin is the same as Object
in Java - everything inherits from it. That's all.
The Unit type
The Unit
type is to void
what a Real Boy is to Pinocchio - it's what it always wanted to be. Like void
, Unit
represents "no value", or to be more precise, "empty value". Unlike void
, it is an actual type (and as all types, Unit
inherits from Any
).
//sampleStart // Java class Class { public void iReturnVoid() { System.out.println("Hello World!"); } } //sampleEnd
//sampleStart // Kotlin fun iReturnUnit(): Unit { println("Hello World!") } //sampleEnd
In Java, a call to iReturnVoid()
cannot be assigned:
//sampleStart ??? result = iReturnVoid(); //sampleEnd
This is precisely because void
is not a type - we cannot write void
in place of ???
.
In Kotlin, a call to iReturnUnit()
can be assigned:
//sampleStart val result: Unit = iReturnUnit() //sampleEnd
A natural question to ask is: what value does result
contain? To answer this, you need one last piece of information: Unit
is actually a singleton - a type which has exactly one instance associated with it.
In Java, you would have something like this:
public final class Unit { public static final Unit INSTANCE; private Unit() {} static { INSTANCE = new Unit(); } }
You would write Unit
to refer to the type, and write Unit.INSTANCE
to refer to the value.
However, there is no added benefit of writing different things for the type and the value, because types can never appear in the same places in code as values. Therefore, in Kotlin, both the type and the value are referred to by the same text, Unit
, because the location is enough to specify if we are talking about the type or the value.
//sampleStart fun iReturnUnit(): Unit = println("Hello World!") fun main() { // Here, the text 'Unit' after the ':' refers to the TYPE val result: Unit = iReturnUnit() // This calls the toString() method of the VALUE println("My value is $result, which is the same as ${Unit.toString()}") val isResultUnit = if(result is Unit) "true" else "false" println("The statement 'My type is Unit' is $isResultUnit") // Here, the first `Unit` is a VALUE, the second `Unit` is a TYPE val isUnitUnit = if(Unit is Unit) "true" else "false" println("The statement 'The type of the Unit value is the Unit type' is $isUnitUnit") val unitCanBeAssigned: Unit = println("Hello") val unitCanAlsoBeAssignedDirectly: Unit = Unit } //sampleEnd
Woa woa woa, hold your horses, I hear you saying. Didn’t we start this whole thing by saying that Unit
, like void
, means "no value"? But we just spent two friggin' paragraphs talking about the value of Unit
!
Think of it this way. A list is a collection of objects. It most definitely is a “thing”, a value. What about the empty list? It contains nothing, but it is a value — the special, empty value.
You can think of Unit
in a very similar fashion — it is the “thing” that represents “no thing”, the value that means “empty value”. Among “collection of values”, we have the empty list. Among “values”, we have Unit
. It plays the same role. It means “this function returns, but the return value contains no information” — it is “empty”.
Why is this useful? Because, among other things, it allows us to treat all function calls as expressions — all functions can be assigned, lambdas returning Unit
can be treated like any other lambda and so on. No more special behavior for void
. No more problems when we try to use generics with functions. Everything just works.
I highly recommend reading this article as well, which gives a nice cross-language overview of this issue and goes a little deeper into the theoretical aspects at the end.
The Nothing type
Jeeeeezus! Nothing, no thing, Unit, empty, psdjgolg, ARGH!
I know, I know. Just hold on, we’re almost there.
While we’ve already talked about returning “a thing” (a value) and returning “empty thing” (Unit
), there is one last situation we haven't talked about, which is not returning at all.
Take a look at the following:
class Example { public ??? iDontReturnAtAll() { return iDontReturnAtAll(); } public ??? iDontReturnAtAll2() { throw new Exception(); } }
Java will let you write anything in the place of the first ???
except for void
, and anything including void
in the place of the second ???
. Excluding cases when you use void
, the compiler will allow you to assign the result of both functions. That's not really a problem - the function will never return anything, because it will never actually return at all, but we can probably agree that it's still a little strange.
What’s even stranger is that this will compile:
class Example2 { public static <A> A test() { throw new IllegalStateException(); } public static main(String[] args) { int x = test(); String y = test(); Object z = test(); } }
This is code that promises to produce a value of type A
for every type A
. It compiles fine, even though a method cannot possibly return an instance of any type A
without accepting an A
as well.
Now, naturally, we know that this code is fine, precisely because those functions never will return. So it’s not a problem that the compiler doesn’t report an error, it’s just that we would expect the type of functions (and expression) that don’t return to be a little more rigorous than the WhateverFloatsYourBoat
type.
In Java, we can just leave it at that, because excluding the weird examples above, these things never appear in situations where types are necessary — throw
is not an expression, it's a statement, so you can't assign it, if
is also a statement, not an expression, same with switch
, there is no when
, you cannot throw
in a ternary expression and so on.
However, in Kotlin, you can do this:
//sampleStart fun throwIfNegativeOrReturn(a: Int): Int { val test = if(a < 0) throw Exception() else a return a; } //sampleEnd
Since if
's are expressions, they can be assigned. The variable test
must have a precisely defined type, which means that, in turn, the types of both branches must be precisely defined - the one with the throw
can't just be random. The same goes for when
expressions, or defining functions with expression bodies.
But we can see that Java’s type system is lacking in this — it has no type that denotes “does not return at all”. Kotlin fixes with the Nothing
type.
Let’s look at a couple of properties of the Nothing
type.
Nothing cannot be instantiated and no value can have type Nothing
This is a logical consequence of the fact that Nothing
denotes things that don't return. Therefore, we can never instantiate it, because by definition, the instantiation never finishes.
//sampleStart // x will never actually get assigned. // A value of type Nothing can never be created val x: Nothing = throw Exception() // // We will never get here //sampleEnd
Nothing is a subtype of every type
When the return type of a function is X
, we can either return X
or any subtype of X
. However, a function can always throw
and as we saw above, we can use throw
in an expression. Since we must be able to use throw
in any expression, it must be assignable to any type, which by definition makes it a subtype of every type. It is no coincidence that Nothing
is often called the bottom type in type theory.
//sampleStart fun nothingFun(): Nothing = throw NotImplementedError() // // This compiles fine, Nothing is a subtype of Int, so the assignment is valid val test1: Int = nothingFun() // // This compiles fine, Nothing is also a subtype of String, so the assignment is valid val test2: String = nothingFun() // This also compiles fine, since Nothing is a subtype of String, so the assignment is valid fun nothingFun2(): String = throw NotImplementedError() // This also compiles fine, nothingFun2 returns a String, so the assignment is valid val test3: String = nothingFun2() // This doesn't compile, since nothingFun2 return a String, so the assignment is invalid // val test4: Int = nothingFun2() // The only way this can work is if Nothing is a subtype of Int (and everything else) fun divide(a: Int, b: Int): Int = when (b) { 0 -> throw IllegalArgumentException() // Type of this is Nothing else -> a / b // The type of this is Int } // This returns either an Int or Nothing - Nothing is a subtype of Int. fun firstString(list: List<String>) = if (list.isNotEmpty()) { list.first() } else { throw IllegalArgumentException("List is empty") //sampleEnd
That last example actually hints at a nice consequence of having an explicit bottom type (the technical term for Nothing
) in a language with generics. Let’s use lists as an example.
Sometimes, when we are working with a List<Supertype>
, we want to be able to pass in a List<Subtype>
as well - for example, if we have an algorithm that calculates the average of a List<Number>
, we should be able to apply it to List<Float>
, List<Int>
and List<Double>
with no problems. The technical term for this is that List
is covariant in T
, but if you ever say that out loud people will just run off screaming, so instead we're going to say that List
has a unicorn-lollipop-rainbow
relationship with T
.
Just to make things clear, let’s state this one more time: if we say that Container<T>
has a unicorn-lollipop-rainbow
relationship with T
, that means that when SUBTYPE
is a subtype of SUPERTYPE
, then Container<SUBTYPE>
is a subtype of Container<SUPERTYPE>
. In our example, List<T>
has a unicorn-lollipop-rainbow
relationship with T
and Int
is a subtype of Number
, so List<Int>
is a subtype of List<Number>
.
Okay, that’s dope and all, but why are we even talking about this? I hear you say, exhaustedly.
Here’s why: what about the empty list? If we have an empty list of Int
s or Float
s, there's no problem, we can just pass it in — it might cause a runtime error depending on the way the algorithm is implemented, but it will certainly compile. But what about an empty list of Strings
? Well, we can't do that, you say. The types don't match.
But we should be able to do it. There is no difference between an empty list of Int
s, an empty list of Floats
or an empty list of any other type. It's all just an empty list — an empty list of strings, floats, numbers, whatever, should always have the same type.
To achieve that, the empty list has to have a type List<X>
such that List<X>
is a subtype of every List<T>
. Since List
has a unicorn-lollipop-rainbow
relationship with its type parameter, this means that X
must be a subtype of every T
, which means that, you guessed it, X
must be Nothing
. And, lo and behold, the type of emptyList()
is indeed List<Nothing>
. Notice that if you read it out loud, it kinda has a nice ring to it — a List<Int>
is a list containing Int
s, and a List<Nothing>
is a list containing nothing.
This all come together rather nicely when you consider the generic function fun <T> firstElementOf(list: List<T>): T
.
This function returns the first element of the List<T>
, which is a value of type T
— in other words, it always has to return whichever type is between the <
and >
. Does this still hold when we pass in an empty list?
Well, what happens when we pass in an empty list? The function throws — it has to, because the return type is not nullable, so there is nothing it could possibly return.
Pause for a second and think about what just happened — we deduced an implementation detail just by looking at the type signature. This is another demonstration of how useful the Kotlin type system is.
And what is the type of the empty list? List<Nothing>
. What is the type of a throw
? Nothing
! It all just works. No surprises, no special treatment for edge cases, just a single set of consistent rules. Awesome!
I highly recommend reading this excellent article, which gives a nice overview of all this as well as mentioning other interesting theoretical aspects of this issue.
Let’s close with an interesting exercise. What is the type of x
in the following code snippet?
//sampleStart val x = null //sampleEnd
Make a guess, and then use the shortcut mentioned in the previous article to check your answer.