Перейти к основному содержимому
Мобилка44ГЛАВА 44Kotlin и Java: в чём разницаJavaKotlinJVM

Kotlin и Java: в чём разница и почему Android пишут на Kotlin

Прочитано0%
Квизы0/6
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Ты уже написал не одну сотню строк на 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 появилась в 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-варианте нет — их пришлось бы дописать руками ещё строк на двадцать.

ОДНА ПРОГРАММА · ДВА ЯЗЫКАJavapublic class User { private final String name; public User(String name) { this.name = name; } public String getName() { return name; }}Kotlindata class User(val name: String)конструктор, геттер, equals,hashCode и toString —компилятор пишет их самодна строка вместо девятивсё описано рукамиописано только новоеповедение у обоих классов одинаковое: разница в объёме кода, а не в возможностях
Одна и та же сущность на двух языках: слева всё описано вручную — поля, конструктор, геттеры, toString; справа компилятор пишет то же самое сам по одной строке объявления.

Разберём Java-строки, чтобы они перестали быть шумом:

  • public class User — объявление класса. Слово public — модификатор видимости: класс виден отовсюду. В Kotlin public подразумевается по умолчанию и его просто не пишут.
  • 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, происходит две отдельные вещи:

  1. Компиляция. Компилятор (javac для Java, kotlinc для Kotlin) читает твои исходники и выдаёт файлы .class — это байткод (bytecode): промежуточные инструкции, не понятные процессору напрямую, но одинаковые на всех машинах.
  2. Исполнение. Байткод скармливается JVM (Java Virtual Machine, виртуальная машина Java) — программе, которая читает эти инструкции и превращает их в реальные действия процессора уже конкретно на этой машине.

Фокус в том, что байткод у обоих компиляторов один и тот же формат. JVM не имеет ни малейшего понятия, на каком языке был написан исходник: она видит классы, методы, поля и инструкции. Файл .class, полученный из User.kt, для неё не отличается от .class, полученного из User.java.

ИСХОДНИК · БАЙТКОД · JVMUser.javaUser.ktjavackotlincбайткод .classодин формат для обоихJVMисполняетJVM не знает, на каком языке это писалиона видит только классы и методы байткода — поэтому библиотекана Java подключается к Kotlin-проекту как своя, без обёртокна Android байткод потом переводится в формат DEX для ART
Два разных компилятора приводят исходники к одному формату байткода, и дальше JVM исполняет его, не зная и не спрашивая, на каком языке всё было написано.

Отсюда следуют все практические выводы главы:

  • Библиотека на 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 нет.

Отвечено вопросов: 0 из 4
Что такое байткод и кто его исполняет?
Почему Java-библиотеку можно подключить к Kotlin-проекту без обёрток?
Почему в Kotlin-приложении падает исключение с именем java.lang.NullPointerException?
Что происходит с байткодом при сборке Android-приложения?
Отвечено верно: 0 из 4

Главное отличие: 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.

NULL · КОГДА ОШИБКА НАХОДИТСЯJava: любая ссылка может быть nullString name = user.getName();int n = name.length();компилируется молчаNullPointerExceptionпадает у пользователя, в рантаймеKotlin: null разрешён только с вопросомval name: String? = user.nameval n = name?.length ?: 0без вопросительного знакакод просто не собираетсяошибка найдена на твоей машинеодна и та же ошибка: слева её находит пользователь, справа — компилятортип String и тип String? в Kotlin — разные типы, и компилятор их не путает
Одна и та же ошибка в двух языках: в Java она компилируется молча и падает у пользователя, в Kotlin компилятор отказывается собирать проект и показывает её тебе.

Три оператора, которыми это лечится​

С типом 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.

Отвечено вопросов: 0 из 5
Чем тип String отличается от String? в Kotlin?
Что делает выражение user?.name ?: "гость"?
Когда обнаруживается ошибка обращения к null в Java и когда — в Kotlin?
Почему злоупотребление оператором !! считается плохим признаком?
Что такое smart cast (умное приведение)?
Отвечено верно: 0 из 5

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 нужно набрать явно, поэтому в реальном коде его часто нет — даже там, где он был бы уместен.

Сводная таблица слов, чтобы не путаться при чтении:

KotlinJavaЧто означает
val x = 5final int x = 5;присвоить один раз
var x = 5int x = 5;можно перезаписывать
val x: Stringfinal String x;тип пишется после имени против «перед именем»
нет аналога; в конце строкиKotlin не требует точку с запятой
x?.lengthx != null ? x.length() : nullбезопасный вызов
override fun@Override перед методомпереопределение метода

Важная тонкость, которая ловит всех: val — про ссылку, а не про содержимое. Список, объявленный через val, нельзя заменить другим списком, но менять его элементы, если сам список изменяемый, никто не запрещает:

val items = mutableListOf("а", "б")
items.add("в") // можно: меняем содержимое, а не ссылку
// items = mutableListOf() // нельзя: переприсваивание ссылки

Ровно так же ведёт себя final в Java. Это не особенность Kotlin — это устройство платформы.

Отвечено вопросов: 0 из 4
Какое ключевое слово Java соответствует kotlin-овскому val?
В Kotlin написано var title: String. Что увидит Java-код, подключивший этот класс?
Что даёт ключевое слово data перед class?
val items = mutableListOf("а"). Можно ли выполнить items.add("б")?
Отвечено верно: 0 из 4

Лямбды: функция как значение​

Лямбды ты уже писал — во второй главе трека и в каждом обработчике нажатия на 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 умеет это из коробки.

ОДИН ПРОЕКТ · ДВА ЯЗЫКА РЯДОМстарый код на JavaLegacyParser.javaработает, трогать незачемпереписывать не требуетсяновый код на KotlinProfileScreen.ktвызывает Java напрямуюпо обычному importKotlin вызывает JavaJava вызывает Kotlinграница с Java — платформенный тип String!: про null компилятор ничего не знаетмиграция идёт по файлу за раз: смешанный модуль собирается так же, как чистыйзначение из Java лучше сразу класть в явный тип String или String?
Старый код на Java и новый на Kotlin в одном модуле: вызовы идут в обе стороны, а опасное место одно — граница, где Kotlin получает значение от Java и ничего не знает про его null.

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. Конвертер — хорошая отправная точка и плохой финал; после него код нужно прочитать и вычистить.

Отвечено вопросов: 0 из 5
Можно ли держать .java и .kt файлы в одном модуле Android-проекта?
Что такое платформенный тип String! в Kotlin?
Как безопасно принять значение из Java-метода?
Зачем в Kotlin-коде встречается аннотация @JvmStatic?
Что не так с результатом автоконвертации Java-файла в Kotlin?
Отвечено верно: 0 из 5

Что читать в чужом 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 осваивается за вечер. Прибыль — доступ к тридцати годам кода, примеров и ответов, которые иначе выглядят как чужой алфавит.

Отвечено вопросов: 0 из 4
Почему в Kotlin нет ключевого слова new?
Где Java в Android-разработке остаётся неизбежной?
Что означает static в Java и что ему соответствует в Kotlin?
Функция верхнего уровня format объявлена в файле Price.kt. Как её вызвать из Java?
Отвечено верно: 0 из 4

Практика: набери строки обоих языков​

Пальцы запоминают синтаксис быстрее головы. Здесь два пула: строки Java, которые ты будешь встречать в чужом коде, и их Kotlin-эквиваленты. Разница в объёме набора чувствуется сразу — это и есть самый честный аргумент главы.

Цель: точность ≥ 90%

public class User {

Шпаргалка: перевод с Java на Kotlin​

ЗадачаJavaKotlin
Переменная, которую можно менятьint x = 5;var x = 5
Переменная на один разfinal int x = 5;val x = 5
Может быть пустоString s (всегда может)String? (только если написан вопрос)
Безопасное обращениеs != null ? s.length() : nulls?.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) objobj 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 выигрывал сам по себе, но независимость языка от одного владельца тоже была аргументом.

Дальше по теме:

Сводная проверка по всей главе: 12 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоЧто учить новичку в Android: Java vs Kotlin? Мобильный разработчикАлексей Гладков
ВидеоKotlin на практике / Отличия от Java и первая программа на KotlinТимофей Коваленко
ВидеоJava vs Kotlin: какой язык учить начинающему Android-разработчику?Nordic IT School
ВидеоСравнение Kotlin и Java в back-end разработкеSerhii Nemchynskyi
ВидеоСможет ли Kotlin вытеснить Java? Мое мнениеSerhii Nemchynskyi

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.