跪拜 Guibai
← Back to the summary

Nothing Is a Type: How Kotlin's Bottom Type Eliminates '!!' in Android Code


theme: juejin highlight: xcode

cover.png

A few days ago I was writing code when my leader suddenly messaged me.

Leader: Nothing Me: is impossible? Leader: What does this mean in Kotlin? Me: withdraws a message Me: Wait, leader, the project is urgent right now. I'll write an article for you when I get a chance.

But when I actually started writing, I realized the three types Nothing, Any, and Unit can be explained together, because compared to Java, these three types are indeed special.

In short:

Any is the common supertype of all non-null types and can accept any non-null value. Unit means "the task is done, no result to return." Nothing means "this code won't finish normally," for example it always throws an exception.

If you find a more complex Android project on GitHub, you'll likely see all three of these types.

The button callback is onClick: () -> Unit, the loading state is UiState<Nothing>, and the value of the analytics parameter is Any?.

These usages might seem arbitrary, but once you're familiar with them, you'll know they provide useful information both to developers and to the compiler.

Now you're probably asking: what do these three types mean respectively? And what's the difference from Java?

OK, let's start with some common Android code.

Where do they sit in Kotlin's type system

Think of Kotlin's type system as a ladder.

The higher the type on the ladder, the more values it can accept, but the less you know about that value.

Nothing is the opposite extreme: it tells the compiler "you will never get any value from here."

What is Any

All Kotlin classes ultimately inherit from Any, even if you don't write it explicitly. So every object has equals(), hashCode(), and toString().

A parameter of type Any can accept any non-null value.

A common scenario in Android is analytics: the same event may contain numbers, text, or objects.

// Sends one analytics event with any mix of values
fun logEvent(name: String, vararg params: Pair<String, Any?>) { /* ... */ }

logEvent("checkout", "items" to 3, "coupon" to "SAVE10", "note" to null)

Here each parameter value uses Any? because the note might be null. If you want to use the value according to its actual type, you can check with is first. For example, inside an if (value is String) branch, the compiler lets you directly write value.length. This is a smart cast.

On the JVM, Kotlin maps Java's Object to Any.

Of course, there are differences: Any only defines the three member functions above, and you can't directly call wait() or notify() on it.

When writing logs or analytics and dealing with Bundle, Any is convenient.

But inside an application, it loses specific type information. If you write a function that accepts Any and then uses is checks internally for various types, I'd advise you that sealed types or generics are usually more appropriate.

Unit vs Java void

Suppose a function does something but has no result to hand back to the caller. Then it can return Unit.

Of course, in Kotlin you usually don't need to write it explicitly:

fun clearCart() { cartItems.clear() }   // return type is Unit

Java's void means no return value.

Kotlin's Unit is a type whose only value is also called Unit.

Here is the JVM version of Unit in Kotlin 2.4.0's standard library.

public actual object Unit {
    override fun toString(): String = "kotlin.Unit"
}

It uses object to declare a singleton.

Since Unit is a value, you can even store it: val result = clearCart() compiles, and printing result gives kotlin.Unit.

Does having an extra value introduce overhead? For a direct call like clearCart() where you don't receive the result, there's no extra return value overhead. It compiles on the JVM to a method returning void, without creating a Unit object or returning one.

So why do we need a type? Because Unit can be used in generics, while void cannot.

In Java's generics to express "no result," you can use Void and write return null. In Kotlin you just use Unit.

OK, now comes the point my leader cared about!

What is Nothing

Nothing means code will not end normally. It has no values, so a function declared to return Nothing won't return normally; it might throw an exception or loop forever.

Actually, you've already used it.

throw is an expression of type Nothing, and the standard library's error() and TODO() are both declared to return Nothing.

nothing-control-flow.png

Nothing is at the very bottom of the ladder and is a subtype of every type. So this code compiles:

data class Order(val id: String, val total: Int)
fun findOrder(id: String): Order? = orders[id]   // null when not found

val order: Order = findOrder(orderId) ?: error("Order $orderId not loaded")

The ?: has two paths. When an order is found, the left side gives an Order; when not found, the error() on the right throws an exception and gives no value, and the expression's type is Nothing. Therefore the whole ?: expression is of type Order, and order doesn't need to be declared as Order?.

When writing your own helper functions, the return type also affects the compiler's reasoning. Here are two versions of stopOrder, where the second is meant to replace the first:

fun orderTotal(id: String): Int {
    val order = findOrder(id)
    if (order == null) stopOrder(id)
    return order.total   // error: order is still Order?
}

// ! Almost right: the helper always throws, but the compiler doesn't know
fun stopOrder(id: String) {
    throw IllegalStateException("Order $id not loaded")
}

Replace the first stopOrder with the following version, then compile orderTotal again.

// ✓ Right: Nothing tells the compiler this call never comes back
fun stopOrder(id: String): Nothing {
    throw IllegalStateException("Order $id not loaded")
}
// Now order.total compiles, because order can't be null after that line

The first version of stopOrder is a block-body function, and when the return type is omitted, it defaults to Unit.

From the caller's perspective, execution can still continue after stopOrder(id), and at that point order might still be null. So directly accessing order.total is an error, requiring a safe call ?. or a non-null assertion !!; a quick fix is to write order!!.total.

But once stopOrder(id) has : Nothing, the compiler knows: if order is null, the code won't reach return order.total. Therefore, when accessing total, order has already been smart-cast to Order, so you don't need !!.

Why do you see UiState

This is a common pattern in Android code.

Many apps name the type that represents UI state UiState; here we use OrderState.

sealed interface OrderState<out T> {
    data object Loading : OrderState<Nothing>
    data class Ready<T>(val data: T) : OrderState<T>
    data class Failed(val message: String) : OrderState<Nothing>
}

val state: OrderState<Order> = OrderState.Loading   // compiles, it's amazing

Loading carries no data, so its type parameter can be Nothing.

Nothing is a subtype of Order, and T is declared as covariant with out. Therefore, OrderState<Nothing> is also a subtype of OrderState<Order>. No matter what T is, you can use the same Loading object.

Of course, without out, the last line would report an error: the initializer type doesn't match.

Quick comparison

Type What it can represent Example in Android
Any? any value, including null Analytics parameters, Map<String, Any?>, equals(other: Any?)
Any any non-null value A log function that accepts a non-null value
Unit the only value is Unit, meaning no result to return () -> Unit, Result<Unit>, Flow<Unit>
Nothing no values, meaning this path doesn't end normally throw, error(), TODO(), UiState<Nothing>

Common questions

Is Any the same as Java's Object?

On the JVM, the two correspond. If a Java method's Object parameter has no nullability annotation, in Kotlin you'll usually see a platform type Any!. Any itself is a Kotlin type, only defines three member functions, and also exists on Kotlin platforms other than the JVM.

What's the difference between Unit and Nothing?

A function with return type Unit returns normally, with no result to hand to the caller. A function with return type Nothing doesn't return normally. In JVM bytecode, ordinary Unit functions like clearCart() usually compile to methods that return void, while ordinary methods declared to return Nothing have JVM return type java.lang.Void (which really is a type).

What type is null?

When you write val x = null, the compiler infers x as Nothing?. Nothing? has only one value, which is null. It is a subtype of all nullable types, so null can be assigned to any T?.

What to remember when writing code?

Any? is at the top of the ladder and accepts any value; Any is right below it, accepting any non-null value, but it won't tell you what it specifically is.

Unit is a type whose only value is also Unit, meaning there's no result to return.

Nothing is at the bottom, telling the compiler that this path won't return normally, so the compiler can reason more precisely about types on other paths.

When writing a helper that always throws an exception, remember to declare its return type as : Nothing. That lets the compiler smart-cast in subsequent code, and you'll write fewer !!.