🚀 · Types

13 min read · Updated on by

Read on to discover a detailed explanation of the problems in the Java type system, and how Kotlin fixes them using AnyUnit 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, only Int. This does not affect performance, as these types are automatically compiled to primitive values whenever possible.
  • The equivalent of the Java Object type is called Any in Kotlin
  • Instead of void, use Unit
  • 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:

  1. There is no type that represents “no value returned”. Java has void, but that is not a type - you cannot write x instanceof voidvoid.class, etc.
  2. There is no type that represents “null value returned”. Try writing public ??? 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.
  3. 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 Ints is List<Int> and a list of Strings is List<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 any List<T>, which means being a subtype of List<T> for any T, which is impossible in Java.
  • You cannot write Function<T, void> or Function<void, T>. Lambdas which return no value (or accept no value) need completely separate types (SupplierConsumer)
  • 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 (mapflatMapfilter 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 voidUnit 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 Ints or Floats, 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 StringsWell, 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 Ints, 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 Ints, 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 throwNothing! 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.

Leave a Comment

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

The Kotlin Primer