По Kotlin написана гора документации и туториалов, но целостной картинки «как устроена система типов» в них обычно нет: null safety объясняют отдельно, Unit — отдельно, Nothing вообще упоминают вскользь, как экзотику.
А картинка того стоит, потому что она маленькая. Правил буквально несколько, они не конфликтуют друг с другом и из них уже выводится всё остальное: и то, почему String нельзя присвоить null, и то, почему IDE подсвечивает недостижимый код, и то, почему ?: throw ... вообще компилируется.
Ниже — вся иерархия по слоям, снизу вверх, с примерами, где это всплывает в реальном коде.
Диаграммы в статье — из заметки Ната Прайса A Whirlwind Tour of the Kotlin Type Hierarchy, она же и подтолкнула написать этот разбор.
Наверху — Any
Все типы Kotlin выстроены в иерархию отношений «подтип — супертип». На вершине стоит абстрактный класс Any. String и Int — оба его подтипы.

Any — это аналог явского Object, но с важной разницей. В Java есть примитивы (int, double, boolean), которые живут вне иерархии классов, и есть обёртки над ними. В Kotlin такого разделения на уровне языка нет: Int — обычный тип, подтип Any, и стоит в иерархии ровно там же, где ваш собственный класс. Во что это скомпилируется на JVM — примитив или объект — дело компилятора, а не вашей модели типов.
Если объявить класс без указания базового, его непосредственным супертипом станет Any:
class Fruit(val ripeness: Double)

Если базовый класс указан — он и станет непосредственным супертипом, а Any останется общим предком всей ветки:
abstract class Fruit(val ripeness: Double)
class Banana(ripeness: Double, val bendiness: Double) : Fruit(ripeness)
class Peach(ripeness: Double, val fuzziness: Double) : Fruit(ripeness)

Интерфейсы дают несколько непосредственных супертипов сразу — но корень всё тот же:
interface ICanGoInASalad
interface ICanBeSunDried
class Tomato(ripeness: Double) :
Fruit(ripeness),
ICanGoInASalad,
ICanBeSunDried

Дальше работает единственное правило проверки типов: значение подтипа можно положить в переменную супертипа, наоборот — нельзя.
var f: Fruit = Banana(ripeness = 0.4, bendiness = 0.5)
f = Peach(ripeness = 0.6, fuzziness = 0.8) // ок
val b = Banana(ripeness = 0.4, bendiness = 0.5)
val f2: Fruit = b // ок
val b2: Banana = f2 // ошибка компиляции
// Type mismatch: inferred type is Fruit but Banana was expected
Пока ничего нового по сравнению с Java. Интересное начинается дальше.
Вторая иерархия — nullable-типы
В отличие от Java, Kotlin различает «ненулевые» и «обнуляемые» типы. Всё, что было выше, — ненулевое. Положить null в такую переменную нельзя, и разыменование ссылки такого типа гарантированно не бросит NullPointerException.
var s: String = null
// Error: Null can not be a value of a non-null type String
Если значение может отсутствовать, берётся nullable-эквивалент — тот же тип с суффиксом ?. String? — это все значения String плюс null:
var s: String? = null
s = "foo"
s = null
Ключевой момент: nullable-типы — не пометка на типе и не аннотация. Это полноценная параллельная иерархия, устроенная по тем же правилам. Если Banana — подтип Fruit, то Banana? — подтип Fruit?. И раз Any — корень ненулевой иерархии, то Any? — корень обнуляемой.

Дальше добавляется вторая половина правила, которая и связывает две иерархии в одну: ненулевой тип является подтипом своего nullable-эквивалента. String — подтип и Any, и String?.

Вот отсюда и растёт вся null safety. Никакого отдельного механизма нет: String можно положить в String?, потому что это подтип; String? нельзя положить в String, потому что это супертип. То самое правило, что и с Banana и Fruit.
Для пользовательских иерархий всё точно так же:

И раз Any? — супертип Any, то именно Any? находится на самой вершине всей системы типов Kotlin. Это единственный тип, в переменную которого можно положить вообще что угодно.
Unit — вместо void
Kotlin — язык выражений: почти все конструкции управления потоком (кроме присваивания, что как раз необычно) являются выражениями и имеют значение. Функций, не возвращающих ничего, здесь нет — вместо void есть тип Unit с единственным значением, тоже Unit.
Писать его руками почти никогда не нужно. У функции с блочным телом без указанного типа результата тип выводится в Unit; у функции-выражения — так же, как и любой другой тип:
fun example1() {
println("блочное тело без типа результата — значит Unit")
}
val u1: Unit = example1()
fun example2() =
println("функция-выражение, компилятор сам выводит Unit")
val u2: Unit = example2()
Ничего особенного в Unit нет — обычный тип. Подтип Any, имеет nullable-эквивалент Unit?, который, в свою очередь, подтип Any?.

Unit? — забавный пограничный случай, который существует просто потому, что система типов последовательна. В нём ровно два значения: Unit и null. На практике писать его руками не приходится, но именно отсутствие спецслучая для «void» позволяет обобщённо работать с любыми функциями: () -> Unit — такой же нормальный тип функции, как () -> String, и Unit спокойно подставляется в generic-параметр.
Nothing — на самом дне
В самом низу иерархии находится Nothing.

Название буквальное: у Nothing нет ни одного экземпляра. Выражение типа Nothing не вычисляется в значение — вообще никогда.
Разница с Unit здесь принципиальная и её стоит проговорить: вычисление выражения типа Unit даёт значение — синглтон Unit. Вычисление выражения типа Nothing не возвращает управление.
Отсюда следует, что код после выражения типа Nothing недостижим — и компилятор с IDE вас об этом предупредят.
Какие выражения имеют тип Nothing? Те, что передают управление куда-то в сторону.
throw прерывает вычисление и выбрасывает исключение наружу — значит, throw это выражение типа Nothing. И вот тут начинает работать главное свойство: Nothing — подтип любого типа. Именно поэтому throw можно поставить в любую ветку любого выражения, и тип выражения от этого не испортится:
fun formatCell(value: Double): String =
if (value.isNaN())
throw IllegalArgumentException("$value не число")
else
value.toString()
Тип if-выражения — общий супертип обеих веток. Одна ветка даёт String, другая Nothing, а раз Nothing — подтип String, общий супертип это String. Никакой магии, обычный вывод типов.
Тип Nothing есть и у return — это тоже передача управления, прерывающая вычисление выражения, частью которого она является:
fun formatCellRounded(value: Double): String {
val rounded: Long = if (value.isNaN()) return "#ERROR" else Math.round(value)
return rounded.toString()
}
Функция, уходящая в бесконечный цикл или убивающая процесс, тоже возвращает Nothing. В стандартной библиотеке, например, так объявлен exitProcess:
fun exitProcess(status: Int): Nothing
И если вы напишете свою функцию с результатом Nothing, компилятор будет проверять недостижимый код после её вызова ровно так же, как после встроенных конструкций:
inline fun forever(action: () -> Unit): Nothing {
while (true) action()
}
fun example() {
forever {
println("работаем...")
}
println("готово") // Warning: Unreachable code
}
Как и null safety, анализ недостижимого кода здесь не отдельная проверка в компиляторе и IDE, как приходится делать в Java. Это следствие системы типов.
Nothing? — тип значения null
Nothing, как и любой другой тип, можно сделать обнуляемым — получится Nothing?. В нём может лежать ровно одно значение: null. Собственно, Nothing? и есть тип литерала null.
Nothing? — предельный подтип всех обнуляемых типов, и именно поэтому null подставляется в переменную любого nullable-типа.

Проверить это можно прямо в IDE:
val x = null // выведенный тип: Nothing?
val list = listOf(null) // List<Nothing?>
Где это пригодится на практике
Всё вышесказанное — не только про красоту устройства. Несколько мест, где знание про Nothing экономит время:
Elvis с выходом из функции. Классика, которая работает именно из-за Nothing:
fun loadName(id: String): String {
val user = repository.find(id) ?: return "аноним"
return user.name
}
Правая часть ?: имеет тип Nothing, поэтому тип всего выражения — User, а не какой-нибудь Any. По той же причине после такой строки работает smart cast: компилятор знает, что дальше user не может быть null.
when, который не разъезжается в Any. Сравните две ветки-заглушки:
val level = when (code) {
1 -> "низкий"
2 -> "высокий"
else -> error("неизвестный код $code") // error(): Nothing → level: String
}
Если бы error() возвращала Unit, общим супертипом веток стал бы Any и переменная получила бы бесполезный тип. error(), TODO() и exitProcess() объявлены возвращающими Nothing как раз для этого — заглушка не портит вывод типов, и TODO() можно смело ставить в тело функции с любым типом результата.
Nothing в дженериках. Тут Nothing из курьёза превращается в рабочий инструмент. Вместе с ковариантным out он даёт тип, который подходит на место любого:
sealed interface Result<out T> {
data class Ok<out T>(val value: T) : Result<T>
data class Err(val message: String) : Result<Nothing>
}
fun parse(raw: String): Result<Int> =
raw.toIntOrNull()?.let { Result.Ok(it) } ?: Result.Err("не число: $raw")
Err не хранит значение, поэтому параметризован Nothing. А раз Nothing — подтип всего и T объявлен как out, Result<Nothing> оказывается подтипом Result<Int>, Result<User> и вообще любого Result<T>. Один объект ошибки подходит везде, дублировать его под каждый тип не нужно.
Ровно по этой же причине в стандартной библиотеке пустая коллекция — один синглтон на всю программу: emptyList() возвращает объект типа List<Nothing>, который является List<T> для любого T.
Вся картина целиком
Если посмотреть на всё сразу, выглядит внушительно:

Но пугаться нечего — правил здесь всего ничего:
- Типы образуют иерархию «подтип — супертип», на вершине ненулевой ветки —
Any. - У каждого типа есть nullable-эквивалент, и nullable-типы образуют такую же иерархию.
- Ненулевой тип — подтип своего nullable-эквивалента. Отсюда
Any?на самой вершине. Nothing— подтип вообще всех типов, аNothing?— тип значенияnull.
Ни одного спецслучая. Null safety, полиморфизм и анализ недостижимого кода — не отдельные фичи, прикрученные к компилятору сбоку, а прямые следствия этих четырёх пунктов. Потому проверка типов в Kotlin и работает предсказуемо: она не «знает про null», она просто сравнивает типы.
Оригинал, откуда взяты диаграммы: Nat Pryce, A Whirlwind Tour of the Kotlin Type Hierarchy (2016).