Kotlin и Java: в чём разница и почему Android пишут на Kotlin
Зачем это нужно
Ты уже написал не одну сотню строк на Kotlin: переменные, функции, классы, коллекции, экраны на Compose. И всё это время рядом жило слово, которое никто не объяснял. Оно вылезает в путях к файлам (src/main/java/), в названиях библиотек (java.util.List), в тексте ошибок (java.lang.NullPointerException), в настройках Android Studio (JDK location, Java compatibility 17). Иногда прямо в твоём проекте лежит файл с расширением .java — и он собирается вместе со всем остальным, будто так и надо.
Это не остатки чужого проекта и не мусор. Kotlin вырос внутри мира Java и работает на её платформе. Пока ты этого не знаешь, половина сообщений компилятора выглядит как случайный набор слов: почему Unresolved reference рядом с java.lang? Почему Stack Overflow отвечает на твой вопрос кодом, который выглядит вообще не так? Почему официальная документация Android половину примеров показывает на двух языках сразу?
Эта глава закрывает пробел. Никакого нового синтаксиса Kotlin здесь нет — только объяснение того, что уже вокруг тебя. К концу ты сможешь: прочитать чужой Java-код, не паникуя; объяснить, что такое JVM и байткод; сказать, почему null — главное отличие двух языков; понять, как .java и .kt уживаются в одном проекте, и решить, стоит ли лезть в Java-файл или можно спокойно оставить его как есть.
- Зачем это нужно
- Откуда взялась Java и при чём тут Kotlin
- Одна и та же программа: Java слева, Kotlin справа
- JVM и байткод: почему они вообще совместимы
- Главное отличие: null и NullPointerException
- Boilerplate: геттеры, сеттеры и data class
- var, val и final
- Лямбды: функция как значение
- Функции верхнего уровня и extension-функции
- Корутины против потоков: обзорно
- Interop: как .java и .kt живут в одном проекте
- Что читать в чужом Java-коде, не паникуя
- Где Java по-прежнему нужна
- Практика: набери строки обоих языков
- Шпаргалка: перевод с Java на Kotlin
- Типичные заблуждения
- Что должен уметь
- Куда дальше: проверенные ресурсы
Откуда взялась Java и при чём тут Kotlin
Java появилась в 1995 году и была придумана вокруг одной идеи, которая тогда звучала революционно: написал один раз — запускается везде. Программа на Си компилировалась под конкретный процессор и конкретную операционную систему; чтобы она работала на другой машине, её нужно было пересобирать заново. Java предложила прослойку — виртуальную машину, которая есть на каждой платформе и одинаково исполняет один и тот же скомпилированный файл.
Идея сработала настолько хорошо, что за тридцать лет на Java написали банковские системы, серверы, промышленный софт и — с 2008 года — Android. Первые десять лет Android был языком Java целиком: приложения, документация, примеры, книги, вопросы на форумах — всё на ней.
У языка при этом были известные всем проблемы: он многословный (простейший класс занимает десяток строк), он старый, и менять его сложно — за совместимостью с миллиардами строк существующего кода следят очень строго. Новые возможности в Java появляются, но медленно и осторожно.
Kotlin придумали в петербургском офисе компании JetBrains — той самой, что делает IntelliJ IDEA, на которой основана Android Studio. Задача была не «сделать новый язык вообще», а конкретная: писать короче и безопаснее, но остаться внутри платформы Java, чтобы весь накопленный код и все библиотеки продолжали работать. Публично проект анонсировали в 2011 году, версия 1.0 вышла в 2016-м, а в 2017-м Google объявила Kotlin официально поддерживаемым языком Android. С 2019 года политика Google — Kotlin-first: новая документация, новые библиотеки и новые примеры пишутся на Kotlin в первую очередь.
Ключевое, что нужно унести из этого абзаца: Kotlin не заменил Java и не отменил её. Он встал рядом, на ту же платформу. Именно поэтому Java продолжает попадаться тебе на глаза — она никуда не делась, на ней написано всё, что было раньше, включая половину библиотек, которые ты подключаешь одной строкой в Gradle.
Имя Kotlin выбрано по образцу Java: Java названа в честь острова Ява, Kotlin — в честь острова Котлин в Финском заливе, того самого, на котором стоит Кронштадт. Шутка про «остров рядом с офисом» стала официальной версией: команда сама рассказывала, что искали короткое имя и решили не ломать традицию островов.
Одна и та же программа: Java слева, Kotlin справа
Проще всего разница видна на коде, который делает ровно одно и то же. Возьмём класс, хранящий пользователя с именем и возрастом. Вот он на Java:
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "User(name=" + name + ", age=" + age + ")";
}
}
И он же на Kotlin:
data class User(val name: String, val age: Int)
Двадцать четыре строки против одной. При этом Kotlin-версия умеет больше: data class бесплатно даёт ещё и equals, hashCode и copy, которых в Java-варианте нет — их пришлось бы дописать руками ещё строк на двадцать.
Разберём Java-строки, чтобы они перестали быть шумом:
public class User— объявление класса. Словоpublic— модификатор видимости: класс виден отовсюду. В Kotlinpublicподразумевается по умолчанию и его просто не пишут.private final String name;— поле класса.private— видно только внутри класса,final— присвоить можно один раз. Тип пишется перед именем:String name, а неname: String, как в Kotlin. Точка с запятой в конце обязательна.public User(String name, int age)— конструктор: метод без типа возврата, названный как класс. В Kotlin он въезжает прямо в скобки после имени класса.this.name = name;— присваивание полю. Словоthisнужно, потому что параметр и поле называются одинаково, и без него компилятор возьмёт параметр.public String getName()— геттер: метод, который отдаёт значение поля. В Java к полям обращаются через такие методы, а не напрямую. В Kotlin геттер генерируется автоматически, а обращение выглядит какuser.name— но под капотом вызывается ровно такой жеgetName().@Override— аннотация: пометка компилятору «этот метод переопределяет унаследованный». В Kotlin её роль играет ключевое словоoverride, и там оно обязательно.
Обрати внимание, чего в Java-коде нет и быть не может: val, var, fun, вывода типов из значения, строковых шаблонов с $. Вместо шаблона — склейка через +. Это не «Java хуже написана» — это просто другой набор слов для того же смысла.
JVM и байткод: почему они вообще совместимы
Теперь главный вопрос: как код на двух разных языках оказывается в одном проекте и работает вместе. Ответ — в том, что ни один из них не исполняется напрямую.
Когда ты нажимаешь Run, происходит две отдельные вещи:
- Компиляция. Компилятор (
javacдля Java,kotlincдля Kotlin) читает твои исходники и выдаёт файлы.class— это байткод (bytecode): промежуточные инструкции, не понятные процессору напрямую, но одинаковые на всех машинах. - Исполнение. Байткод скармливается JVM (Java Virtual Machine, виртуальная машина Java) — программе, которая читает эти инструкции и превращает их в реальные действия процессора уже конкретно на этой машине.
Фокус в том, что байткод у обоих компиляторов один и тот же формат. JVM не имеет ни малейшего понятия, на каком языке был написан исходник: она видит классы, методы, поля и инструкции. Файл .class, полученный из User.kt, для неё не отличается от .class, полученного из User.java.
Отсюда следуют все практические выводы главы:
- Библиотека на Java подключается к Kotlin-проекту как своя. Никаких обёрток, мостов и конвертеров: ты пишешь
importи вызываешь методы. Ретрофит, OkHttp, Gson, половина Android SDK — написаны на Java, и ты уже ими пользовался, не заметив этого. - Java-код может вызывать Kotlin-код. Обратное направление тоже работает: Kotlin-класс виден из Java как обычный класс с геттерами и сеттерами.
- Ошибки в рантайме приходят с Java-именами.
java.lang.NullPointerException,java.lang.IllegalStateException,java.util.ConcurrentModificationException— это классы стандартной библиотеки Java, и Kotlin переиспользует их, а не заводит свои.
На Android есть ещё один шаг, о котором полезно знать. Устройство исполняет не байткод JVM, а формат DEX на среде выполнения ART (Android Runtime): при сборке приложения инструмент d8/R8 переводит .class в .dex. Для тебя это ничего не меняет — цепочка та же, просто с дополнительным звеном в конце. Но именно поэтому в Android-проекте нельзя брать любую JVM-библиотеку подряд: некоторые опираются на классы, которых на Android нет.
Главное отличие: null и NullPointerException
Всё, что было выше, — про удобство. Дальше идёт единственное отличие, ради которого стоило заводить новый язык.
В Java любая ссылка может оказаться пустой. Переменная типа String содержит либо строку, либо специальное значение null — «здесь ничего нет». Компилятор не различает эти два случая: с его точки зрения String name — просто строка, и вызвать у неё метод можно всегда.
String name = user.getName(); // может вернуть null — компилятор не возражает
int n = name.length(); // если вернулся null — падение прямо здесь
Второй строкой программа обращается к методу у пустоты — и получает NullPointerException (NPE). Причём происходит это в момент запуска, у пользователя, а не при сборке. Тони Хоар, который придумал null в 1965 году, спустя сорок лет публично назвал это своей ошибкой на миллиард долларов — именно из-за количества падений, которые эта идея породила по всей индустрии.
В Kotlin null тоже существует — иначе не получилось бы работать с Java-библиотеками. Но типы разделены надвое:
String— строка, и только строка. Положить сюдаnullнельзя, компилятор откажется.String?— строка илиnull. Вопросительный знак — часть типа, не украшение.
val sure: String = "Соня"
val maybe: String? = null
sure.length // работает: тип гарантирует, что значение есть
maybe.length // НЕ КОМПИЛИРУЕТСЯ: компилятор видит, что тут может быть null
Вторая строка не соберётся никогда. Ошибка находится на твоей машине, за секунды, а не через неделю в отзыве в Google Play.
Три оператора, которыми это лечится
С типом String? всё равно нужно работать — и для этого есть три коротких оператора.
?. — безопасный вызов (safe call). Если слева null, весь вызов возвращает null, не падая:
val len: Int? = maybe?.length // null, если maybe == null, иначе длина
?: — элвис-оператор (Elvis operator). Подставляет запасное значение, когда слева null:
val len: Int = maybe?.length ?: 0 // если слева null — берём 0
Имя странное, но объяснимое: если посмотреть на ?: сбоку, видно причёску и глаза Элвиса Пресли. Так его и называют в официальной документации.
!! — оператор «я гарантирую». Говорит компилятору «здесь точно не null, пропусти», и если ты ошибся — падает с NPE:
val len: Int = maybe!!.length // ведёт себя ровно как Java: может упасть
Оператор !! — это выключатель всей защиты, ради которой Kotlin и придумывали. Каждое его появление в коде означает «здесь я обещаю компилятору, что понимаю больше него». Иногда так и есть, но у новичка !! чаще всего означает «код не собирался, и я убрал ошибку самым коротким способом». Правильный рефлекс: увидел !! — попробуй заменить на ?. с элвисом, и если получилось, значит, !! был лишним.
Умное приведение
Есть ещё один приём, который делает ? почти незаметным в повседневном коде. После проверки на null компилятор сам переключает тип:
fun greet(name: String?) {
if (name == null) return
// с этой строки и до конца функции name имеет тип String, а не String?
println("Привет, " + name.uppercase())
}
Это называется smart cast — умное приведение типа. Компилятор проследил ход выполнения и понял: раз до сюда добрались, значит null был отсеян. В Java такого нет: там после проверки if (name != null) тип не меняется, потому что менять нечего — типы и так не различают null.
Boilerplate: геттеры, сеттеры и data class
Второе по заметности отличие — количество кода, которое приходится писать руками ради ничего. У этого есть название: boilerplate (шаблонный код) — строки, которые ничего не решают, но обязаны присутствовать.
Классический пример из Java — доступ к полям. Правило хорошего тона там такое: поле делается private, а снаружи с ним работают через методы getName() и setName(...). Смысл правила разумный — можно поменять внутреннее устройство, не ломая тех, кто пользуется классом. Плата — по два метода на каждое поле:
public class Product {
private String title;
private int price;
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public int getPrice() { return price; }
public void setPrice(int price) { this.price = price; }
}
Kotlin оставляет ровно ту же схему, но пишет её за тебя. Когда ты объявляешь
class Product {
var title: String = ""
var price: Int = 0
}
компилятор порождает в байткоде то же самое поле с getTitle()/setTitle() — просто в исходнике их не видно, а обращение выглядит как product.title. Это не «Kotlin отменил геттеры», это «Kotlin перестал заставлять их набирать». Проверить легко: Java-код, подключивший этот класс, увидит именно getTitle() и setTitle(...).
Для val генерируется только геттер — сеттера нет, потому что присвоить нечего.
data class
Отдельная история — классы, существующие только чтобы хранить данные: ответ сервера, элемент списка, состояние экрана. Кроме полей им нужны equals (сравнение по содержимому), hashCode (чтобы работать в множествах и словарях), toString (читаемый вывод в лог) и удобное копирование. В Java всё это пишут руками или генерируют средой — и потом забывают обновить при добавлении нового поля.
Ключевое слово data перед class в Kotlin означает «сгенерируй весь этот набор по списку свойств в конструкторе»:
data class Product(val title: String, val price: Int)
val a = Product("Кофе", 250)
val b = Product("Кофе", 250)
println(a == b) // true — сравнение по содержимому
println(a) // Product(title=Кофе, price=250)
val cheaper = a.copy(price = 199) // копия с изменённым полем
Без data строка a == b вернула бы false: обычные классы сравниваются по ссылке, то есть «это буквально один и тот же объект в памяти?». Ровно так же ведёт себя Java-класс без написанного вручную equals — и это одна из самых частых ошибок, которую ловят на код-ревью.
Хочешь увидеть, что именно генерирует компилятор, — в Android Studio есть встроенный инструмент: меню Tools → Kotlin → Show Kotlin Bytecode, а в открывшейся панели кнопка Decompile. Она покажет твой Kotlin-файл в виде Java-кода, эквивалентного полученному байткоду. Один взгляд на декомпилированный data class объясняет про геттеры и equals больше, чем страница текста.
var, val и final
Про val и var ты уже всё знаешь: val присваивается один раз, var можно перезаписывать. Интересно другое — как то же самое выглядит в Java, потому что читать её код ты будешь именно с этим знанием.
В Java изменяемость по умолчанию: любая переменная перезаписывается, пока явно не запрещено. Запрет ставится ключевым словом final:
final String name = "Соня"; // аналог val
String city = "Самара"; // аналог var
Разница не только в словах, но и в том, что считается нормой. В Kotlin val — вариант по умолчанию, к которому приучает и язык, и IDE (она подсвечивает var, который ни разу не переприсваивают). В Java final нужно набрать явно, поэтому в реальном коде его часто нет — даже там, где он был бы уместен.
Сводная таблица слов, чтобы не путаться при чтении:
| Kotlin | Java | Что означает |
|---|---|---|
val x = 5 | final int x = 5; | присвоить один раз |
var x = 5 | int x = 5; | можно перезаписывать |
val x: String | final String x; | тип пишется после имени против «перед именем» |
| нет аналога | ; в конце строки | Kotlin не требует точку с запятой |
x?.length | x != null ? x.length() : null | безопасный вызов |
override fun | @Override перед методом | переопределение метода |
Важная тонкость, которая ловит всех: val — про ссылку, а не про содержимое. Список, объявленный через val, нельзя заменить другим списком, но менять его элементы, если сам список изменяемый, никто не запрещает:
val items = mutableListOf("а", "б")
items.add("в") // можно: меняем содержимое, а не ссылку
// items = mutableListOf() // нельзя: переприсваивание ссылки
Ровно так же ведёт себя final в Java. Это не особенность Kotlin — это устройство платформы.
Лямбды: функция как значение
Лямбды ты уже писал — во второй главе трека и в каждом обработчике нажатия на Compose. Здесь важно понять, во что они превращаются и как то же самое выглядело в Java раньше.
Сравним обработчик клика. Kotlin:
button.setOnClickListener { view -> println("нажали") }
Java до восьмой версии — единственный способ был такой:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
System.out.println("нажали");
}
});
Пять строк служебного текста ради одной строки дела. Конструкция называется анонимный внутренний класс: раз метод принимает объект с методом onClick, приходится создавать такой объект прямо на месте. В Java 8 (2014) появились лямбды, и стало короче:
button.setOnClickListener(view -> System.out.println("нажали"));
Три вещи, которые полезно знать про лямбды Kotlin на фоне Java:
- Лямбда — последний аргумент — выносится за скобки. Именно поэтому в Compose так много кода вида
Button(onClick = ...) { Text("ОК") }: содержимое в фигурных скобках — это последний параметр функции, вынесенный наружу. Такого синтаксиса в Java нет вообще. - Единственный параметр можно не называть. Внутри лямбды он доступен как
it. В Java параметр называть обязательно всегда. - Функции высшего порядка — обычное дело. Функция может принимать другую функцию и возвращать функцию, и тип для этого записывается прямо в сигнатуре:
(Int) -> String. В Java такие типы приходится выражать через интерфейсы вродеFunction<Integer, String>.
Практический вывод для чтения чужого кода: если в Java-примере ты видишь new SomeListener() { ... } на пять строк — это ровно то же самое, что твоя одна строка с фигурными скобками. Не пугайся объёма, ищи внутри единственный метод — там и лежит смысл.
Функции верхнего уровня и extension-функции
В Java всё живёт внутри класса. Совсем всё: не бывает функции, которая лежит просто в файле. Если нужна утилита «отформатируй цену», её кладут в класс-контейнер и делают статической:
public class PriceUtils {
public static String format(int rubles) {
return rubles + " ₽";
}
}
// вызов
String s = PriceUtils.format(250);
Класс PriceUtils не значит ничего — объект такого класса никто никогда не создаст. Он существует, потому что синтаксис требует куда-то положить функцию. В Kotlin функцию кладут прямо в файл:
// файл Price.kt
fun format(rubles: Int): String = "$rubles ₽"
// вызов из любого места после import
val s = format(250)
Это функция верхнего уровня (top-level function). Интересно, что на уровне байткода компилятор всё равно делает класс-контейнер — он назовёт его PriceKt по имени файла. То есть Java-код увидит ровно PriceKt.format(250). Платформа не изменилась, изменилось то, что тебе нужно набирать.
Extension-функции
Приём, которого в Java нет совсем. Extension-функция (функция-расширение) позволяет дописать метод к чужому классу, не наследуясь от него и не имея доступа к его исходникам:
fun Int.toPrice(): String = "$this ₽"
val s = 250.toPrice() // "250 ₽"
Тип перед точкой в объявлении — приёмник (receiver): класс, которому мы дописываем метод. Внутри функции this указывает на само значение. Снаружи это выглядит так, будто у Int всегда был метод toPrice().
Никакой магии внутри нет: компилятор превращает это в обычную статическую функцию, куда значение передаётся первым аргументом. Из Java она видна как PriceKt.toPrice(250). Но в Kotlin-коде она читается слева направо, в порядке действий, а не наизнанку:
// цепочка расширений читается как последовательность шагов
val result = text.trim().lowercase().take(20)
Именно на расширениях построена значительная часть стандартной библиотеки Kotlin и Android KTX: функции вроде String.isNullOrBlank() или View.isVisible дописаны к классам, которые сами по себе про Kotlin ничего не знают.
Extension-функция не может обращаться к приватным потрохам класса и не «встраивается» в него по-настоящему: если у класса появится собственный метод с тем же именем и параметрами, побеждать при вызове будет он, а твоё расширение молча перестанет использоваться. Поэтому расширения хороши для удобных сокращений, а не для попытки залезть внутрь чужого класса.
Корутины против потоков: обзорно
Тема большая, ей будет посвящена отдельная глава. Здесь — только то, что нужно, чтобы понимать разницу подходов, когда встретишь их в чужом коде.
Задача одна и та же: запрос в сеть занимает секунду, а интерфейс не должен замирать на эту секунду. Значит, долгая работа выполняется не там же, где рисуется экран.
Классический ответ Java — потоки (threads). Поток — это отдельная линия исполнения, которой управляет операционная система. Создали поток, отдали ему работу, он выполнил и сообщил результат:
new Thread(() -> {
String data = loadFromNetwork(); // долго
runOnUiThread(() -> textView.setText(data)); // вернуться в поток экрана
}).start();
Проблемы известны: поток дорогой (память под стек измеряется мегабайтами, тысячу штук не создать), а возврат результата приходится писать вручную — через колбэки, которые при нескольких последовательных запросах вкладываются друг в друга и превращаются в лестницу.
Ответ Kotlin — корутины (coroutines). Корутина выглядит как обычный последовательный код, но умеет приостанавливаться, не занимая поток на время ожидания:
lifecycleScope.launch {
val data = loadFromNetwork() // suspend-функция: тут корутина «засыпает»
textView.text = data // строка выполнится, когда данные придут
}
Ключевое слово suspend помечает функцию, которая умеет приостанавливаться. Пока она ждёт ответа сети, поток свободен и занимается другими делами; когда ответ пришёл, выполнение продолжается со следующей строки. Корутины лёгкие: их можно завести тысячами, потому что это не потоки ОС, а структуры в памяти поверх небольшого пула потоков.
Три вывода, которые пригодятся уже сейчас:
- Код читается сверху вниз. Никакой вложенности колбэков: последовательные действия записаны последовательно.
- У корутин есть область (scope) и отмена.
lifecycleScopeв Android означает «жить, пока живёт экран»: ушёл пользователь с экрана — запросы отменились сами. С голыми потоками отменять нужно вручную, и об этом регулярно забывают. suspend— не «выполнить в фоне». Это «умею приостановиться». Где именно выполняться, задаётся отдельно — диспетчером.
Interop: как .java и .kt живут в одном проекте
Теперь про то, ради чего всё затевалось, — совместимость (interoperability, сокращённо interop). Правило простое: в одном модуле могут лежать и .java, и .kt, и они собираются вместе. Никакой настройки для этого не требуется — Gradle-плагин Kotlin умеет это из коробки.
Kotlin вызывает Java. Просто пишешь import и пользуешься. Единственная особенность — платформенные типы.
Java вызывает Kotlin. Тоже работает: Kotlin-класс виден как обычный класс, свойства — как геттеры и сеттеры, функции верхнего уровня — как статические методы класса, названного по имени файла.
Платформенные типы: единственное опасное место
Когда Kotlin получает значение из Java-метода, он не знает, может ли оно быть null: в Java этой информации в типе нет. Такой тип называется платформенным и записывается в подсказках IDE с восклицательным знаком — String!. Он ведёт себя гибко: его можно присвоить и в String, и в String?.
val name = legacyParser.getName() // тип String! — компилятор не проверяет
println(name.length) // соберётся, но может упасть с NPE
Это ровно та дыра в защите, о которой нужно помнить: на границе с Java гарантии Kotlin заканчиваются. Правильная привычка — сразу класть значение из Java в явно указанный тип:
val name: String? = legacyParser.getName() // честно: может быть null
val safe: String = name ?: "без имени" // и сразу разобрались, что делать
Если Java-библиотека размечена аннотациями @Nullable и @NotNull (а хорошие библиотеки размечены), компилятор Kotlin прочитает их и подставит правильный тип — с вопросом или без. Именно поэтому современные Java-библиотеки для Android так тщательно расставляют эти аннотации.
Аннотации для обратной стороны
Иногда нужно, чтобы Kotlin-код удобно вызывался из Java. Для этого есть несколько пометок, которые полезно узнавать в чужом коде:
@JvmStatic— сделать метод по-настоящему статическим, чтобы из Java звался какUtils.format(...), а неUtils.INSTANCE.format(...).@JvmOverloads— сгенерировать перегрузки для параметров со значениями по умолчанию: в Java значений по умолчанию нет, и без этой аннотации вызвать функцию с пропущенным аргументом нельзя.@JvmName— задать другое имя в байткоде, когда стандартное неудобно или конфликтует.
Знать их наизусть не нужно; нужно понимать, что они делают, когда увидишь их в коде библиотеки.
Конвертация файла в один клик
Android Studio умеет переводить Java-файл в Kotlin: Code → Convert Java File to Kotlin File. Результат почти всегда компилируется, но получается «Java, записанная словами Kotlin»: с лишними !!, var там, где просится val, и без data class. Конвертер — хорошая отправная точка и плохой финал; после него код нужно прочитать и вычистить.
Что читать в чужом Java-коде, не паникуя
Ты почти наверняка не будешь писать на Java. Но читать придётся: половина ответов на Stack Overflow, старые примеры в документации, исходники библиотек, легаси в командном проекте. Вот минимальный словарь, чтобы Java-файл выглядел понятным текстом, а не шифром.
Порядок слов в объявлении. Java пишет тип перед именем, Kotlin — после имени через двоеточие:
private static final int MAX_COUNT = 10; // Java
private const val MAX_COUNT = 10 // Kotlin
Модификаторы, которые встречаются постоянно:
| Слово в Java | Что означает | Ближайшее в Kotlin |
|---|---|---|
public | видно отовсюду | по умолчанию, писать не нужно |
private | видно только внутри класса | private |
static | принадлежит классу, а не объекту | функция верхнего уровня или companion object |
final (у переменной) | присвоить один раз | val |
final (у класса) | запрет наследования | по умолчанию, а open — разрешение |
void | функция ничего не возвращает | Unit, обычно не пишется |
abstract | без реализации, наследник обязан дописать | abstract |
implements / extends | реализует интерфейс / наследует класс | оба через двоеточие |
Что бросается в глаза при первом взгляде:
- Точка с запятой в конце каждой строки — обязательна. Её отсутствие в Kotlin — самое заметное внешнее отличие.
newперед созданием объекта:new User("Соня"). В Kotlin этоUser("Соня")без словаnew.System.out.println(...)— этоprintln(...).- Явное приведение типов в скобках:
(String) obj. В Kotlin —obj as String. - Цикл
for (int i = 0; i < n; i++)— классический счётный цикл. В Kotlin ему соответствуетfor (i in 0 until n). - Аннотации через
@перед объявлением:@Override,@Nullable,@Deprecated. Читать их можно как комментарии для компилятора.
Чего в Java нет, и не надо этого искать: строковых шаблонов с $ (вместо них склейка через +), val/var, data class, extension-функций, именованных аргументов, значений параметров по умолчанию, вывода типа для полей класса.
Рабочий приём, когда наткнулся на непонятный Java-фрагмент в ответе на Stack Overflow: создай в Android Studio временный .java-файл, вставь код туда и нажми Code → Convert Java File to Kotlin File. Ты мгновенно получишь перевод на знакомый язык. Это способ прочитать чужой код, а не способ добавить его в проект: результат конвертации почти всегда нуждается в правке.
Где Java по-прежнему нужна
Честная часть главы. «Android пишут на Kotlin» — правда, но из неё не следует «Java можно не знать вообще». Вот конкретные места, где она никуда не делась.
- Легаси-код в реальных проектах. Приложение, которому пять лет, наполовину написано на Java, и переписывать его никто не будет: работающий код не трогают без причины. Твоя задача в таком проекте — добавлять новое на Kotlin и уметь читать старое.
- Исходники библиотек и Android SDK. Когда документация не отвечает на вопрос, ответ ищут в исходниках. Значительная часть фреймворка Android — Java, и щелчок по имени класса в IDE открывает именно её.
- Серверная разработка. За пределами Android мир JVM — это в первую очередь Java и Spring. Если тебя занесёт в бэкенд на JVM, знание синтаксиса Java станет рабочим требованием, а не бонусом.
- Обучение и олимпиадные задачи. Во многих учебных курсах и на части площадок с задачами Java по-прежнему основной или единственный язык.
- Понимание того, как устроен сам Kotlin. Многие его особенности объясняются только через платформу: почему
Intиногдаint, а иногдаInteger; откуда берётсяUnit; почему у обобщённых типов есть стирание типов и что это значит на практике. Без базового знания Java эти вещи приходится просто запоминать.
Практический вывод: писать на Java с нуля тебе, скорее всего, не придётся. Читать — придётся точно. Стоимость этого умения невелика: языки родственные, и после Kotlin синтаксис Java осваивается за вечер. Прибыль — доступ к тридцати годам кода, примеров и ответов, которые иначе выглядят как чужой алфавит.
Практика: набери строки обоих языков
Пальцы запоминают синтаксис быстрее головы. Здесь два пула: строки Java, которые ты будешь встречать в чужом коде, и их Kotlin-эквиваленты. Разница в объёме набора чувствуется сразу — это и есть самый честный аргумент главы.
Цель: точность ≥ 90%
public class User {
Шпаргалка: перевод с Java на Kotlin
| Задача | Java | Kotlin |
|---|---|---|
| Переменная, которую можно менять | int x = 5; | var x = 5 |
| Переменная на один раз | final int x = 5; | val x = 5 |
| Может быть пусто | String s (всегда может) | String? (только если написан вопрос) |
| Безопасное обращение | s != null ? s.length() : null | s?.length |
| Значение по умолчанию | s != null ? s : "нет" | s ?: "нет" |
| Создание объекта | new User("Соня") | User("Соня") |
| Класс-хранилище данных | класс на 25 строк | data class User(val name: String) |
| Вывод в консоль | System.out.println(x); | println(x) |
| Склейка строк | "Привет, " + name | "Привет, $name" |
| Метод без возврата | void run() | fun run() |
| Статическая утилита | класс со static-методом | функция верхнего уровня |
| Метод чужому классу | невозможно | extension-функция |
| Фоновая работа | new Thread(...).start() | launch { ... } с suspend |
| Переопределение | @Override перед методом | override fun |
| Приведение типа | (String) obj | obj as String |
Типичные заблуждения
«Kotlin — это надстройка над Java, он медленнее». Нет. Оба языка компилируются в один и тот же байткод, и разницы в скорости исполнения на уровне обычного кода нет. Отличается время сборки — компилятор Kotlin делает больше работы, — но это про ожидание в IDE, а не про работу приложения на устройстве.
«Раз Kotlin безопаснее, NPE в нём невозможен». Возможен: через !!, через платформенные типы на границе с Java и через обращение к свойству, которое ещё не проинициализировано (lateinit). Kotlin убирает основной класс этих ошибок, а не все до единой.
«Java устарела и её пора забыть». Java выпускает новые версии каждые полгода, и в ней давно есть лямбды, var для локальных переменных, записи (records) — близкий родственник data class. Она развивается, просто медленнее и осторожнее.
«В проекте должен быть только один язык». Смешанный модуль — норма, а не признак беспорядка. Переписывать работающий Java-файл на Kotlin ради единообразия — способ внести новые ошибки в код, который до этого работал годами.
«Путь src/main/java значит, что там Java». Это просто историческое имя папки. Kotlin-файлы прекрасно живут в src/main/java, и в большинстве Android-проектов лежат именно там.
Что должен уметь
- Объяснять, откуда взялась Java, что такое JVM и байткод и почему код на двух языках работает вместе.
- Читать простой Java-класс: находить поля, конструктор, геттеры, модификаторы и понимать, что делает каждая строка.
- Формулировать главное отличие языков — обработка
nullна уровне системы типов — и объяснять, когда ошибка находится компилятором, а когда пользователем. - Пользоваться
?,?.,?:и понимать цену!!. - Объяснять, что такое boilerplate, какие методы генерирует
data classи что видит из Java Kotlin-свойство. - Сопоставлять
val/varсfinalи понимать, чтоvalзащищает ссылку, а не содержимое. - Узнавать анонимный внутренний класс в Java-коде и понимать, что в Kotlin ему соответствует лямбда.
- Объяснять, что такое функция верхнего уровня и extension-функция и во что они превращаются в байткоде.
- В общих чертах описывать разницу между потоками и корутинами и понимать, зачем нужен
suspend. - Держать в голове опасность платформенных типов на границе с Java и правильно принимать значения из Java-кода.
- Называть места, где знание Java окупается, и не бояться открывать
.java-файл в своём проекте.
Углубиться
Стирание типов (type erasure). Обобщённые типы (List<String>) существуют только на этапе компиляции: в байткоде остаётся просто List. Это наследие Java, введённое ради совместимости со старым кодом, и Kotlin живёт с тем же ограничением. Практическое следствие — нельзя во время выполнения спросить у списка, какого типа его элементы.
Int против Integer. В Java есть примитивные типы (int, boolean) и их объектные обёртки (Integer, Boolean). В Kotlin всё выглядит как объекты — тип всегда Int, — но компилятор сам подставляет примитив там, где это возможно, и обёртку там, где нужен объект (например, внутри коллекций). Отсюда неочевидное поведение при сравнении больших чисел через ссылочное равенство.
Records в Java. С Java 16 в языке есть record — краткое объявление класса-хранилища данных с автоматическими методами доступа, equals, hashCode и toString. По сути ответ на data class, хотя и с другими правилами.
Kotlin Multiplatform. Kotlin компилируется не только в байткод JVM: есть компиляция в JavaScript и в нативный код для iOS. Это делает возможным общий код между Android- и iOS-приложением — направление, которое активно развивается и уже используется в продакшене.
Почему Google выбрала Kotlin. Кроме технических причин была и организационная: многолетний судебный спор Oracle и Google вокруг использования Java API в Android. Технически Kotlin выигрывал сам по себе, но независимость языка от одного владельца тоже была аргументом.
Дальше по теме:
- Kotlin: сравнение с Java — официальный список отличий двух языков по пунктам
- Kotlin: вызов Java из Kotlin — платформенные типы, аннотации nullability и особенности границы
Сводная проверка по всей главе: 12 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
-
Kotlin: сравнение с Java — официальный перечень отличий: что в Kotlin есть сверх Java и чего в нём нет.
-
Kotlin: null-безопасность — первоисточник про
?,?.,?:,!!и умное приведение типов. -
Kotlin: вызов Java из Kotlin — платформенные типы, аннотации nullability, особенности границы двух языков.
-
Kotlin: вызов Kotlin из Java —
@JvmStatic,@JvmOverloads,@JvmNameи имена классов-контейнеров. -
Kotlin: классы данных — что именно генерирует
data classи какие есть ограничения. -
Kotlin: свойства — как объявление свойства превращается в геттер и сеттер.
-
Kotlin: функции-расширения — как устроены extension-функции и почему они разрешаются статически.
-
Kotlin: лямбды и функции высшего порядка — синтаксис лямбд,
itи вынос последнего аргумента за скобки. -
Kotlin: корутины — точка входа в тему
suspend, областей и диспетчеров. -
Kotlin: смешанный Java/Kotlin проект — как добавить Kotlin в существующий Java-проект и наоборот.
-
Android: Kotlin для Android — официальная позиция Google и стартовые материалы.
-
Android: руководство по совместимости с Java — как писать Kotlin-API, удобные для вызова из Java, и наоборот.
-
Android: корутины на Android — области, привязанные к жизненному циклу, и отмена работы.
-
Спецификация виртуальной машины Java — первоисточник про байткод и устройство JVM, для тех, кому интересно дно.
-
Oracle: класс NullPointerException — документация того самого исключения, вокруг которого построена половина главы.
-
Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.