Я уже несколько лет пишу бэкенд на Kotlin. До этого писал на Java, но переход совершил практически одномоментно, потому что тогда мне казалось, что Kotlin закрывает множество минусов Java и привносит более простой и лаконичный синтаксис. Но при этом я стараюсь следить за тем, как развивается Java.
В 2018 я впервые познакомился с Kotlin, и он мне практически сразу понравился. Да и как можно не полюбить язык, названный в честь тотемного животного всех программистов? Шучу, на самом деле в честь острова.
В JetBrains поступили очень мудро, сделав Kotlin полностью совместимым c Java на уровне байт-кода. Это позволяет запускать Kotlin на той же Java-машине и использовать все библиотеки, которые уже были разработаны на Java. Чем-то мне это напомнило подход Бьёрна Страуструпа, который в свое время сделал C++ расширением языка C. Да ещё вспомним, что именно JetBrains сделала лучшую интегрированную среду разработки для Java, поэтому IDEA сразу получила поддержку Kotlin. А это ещё одно конкурентное преимущество.
Давайте проведем небольшую ретроспективу и сравним эти два языка между собой по состоянию на 2025 год. Я сгруппировал основные фичи Kotlin'a по трём группам: преимущества, нейтральные фичи и недостатки. Это далеко не полный список, и это мое субъективное деление. Поэтому предлагайте в комментах свой вариант.
Преимущества Kotlin перед Java
Здесь собраны фичи, которые на текущий момент остаются конкурентными преимуществами Kotlin'а по сравнению с Java.

Отказ от точки с запятой
Возможно, когда-то точка с запятой в конце инструкции позволяла писать компактные исходники в одну строку, но с появлением IDE и функции автоформатирования, это из преимущества превратилось в недостаток. Вряд ли кому-то сейчас придёт в голову писать на Java такой код:
public static void main(String[] args){String text="Hello!";System.out.println(text);}
Поэтому более строгие правила форматирования как плата за улучшение читаемости сейчас выглядят оправданно.
fun main() {
println("Hello!")
}
Null-safety
Первое, что приходит на ум, когда мы сравниваем Kotlin — это контроль за nullability типов и элвис-оператор.
Сравним метод на Java, печатающий в консоли строку текста:
public void displayNullable(String message) {
if (message != null) {
System.out.println(message);
} else {
System.out.println("Нет текста");
}
}
С эквивалентом на Kotlin:
fun displayNullable(param: String?) {
println(param ?: "Нет текста")
}
Помимо того, что код в целом выглядит компактнее, компилятор Kotlin-а будет контролировать нуллабельность типов на всём пути использования этого объекта. Причем это именно контроль за контекстом, так как на уровне Java-машины никаких специальных not null
типов нет. Под капотом это будет всё тот же nullable String
.
В Java, конечно, можно использовать Optional
, но это не синтаксис языка, а часть стандартной библиотеки. К тому же активное использование типизированного Optional
загромождает код.
public void displayNullable(Optional<String> message) {
System.out.println(message.orElse("Нет текста"));
}
Подстановка переменных в строку
В настоящий момент в Java есть несколько стандартных способов подстановки значений переменных в строку, но у каждого из них свои недостатки.
public static void main(String[] args) {
String name = "Вася";
int age = 25;
System.out.println("Меня зовут " + name + ", мне " + age + " лет.");
System.out.printf("Меня зовут %s, мне %s лет.%n", name, age); // аналогично String.format()
}
Конкатенация через плюс заметно «раздувает» строку кода и усложняет чтение, а использование printf()
или String.format()
отделяет переменные от конкретного места их использования, что тоже ухудшает чтение. Кроме того, во втором случае компилятор перестает контролировать соответствие количества плейсхолдеров в шаблоне количеству переданных переменных, что может обнаружиться только во время выполнения.
В Kotlin есть механизм интерполяции строк, когда мы можем подставлять переменную непосредственно в строку с помощью знака доллара ($). Если над переменной при этом нужно выполнить еще какое-то действие (например, прибавить 1), вся конструкция заключается в фигурные скобки. В них можно производить более сложные вычисления и даже вызывать методы.
fun main() {
val name = "Петя"
val age = 25
println("Меня зовут $name, мне $age лет.")
println("Скоро мне исполнится ${age + 1} лет.")
}
Подобный синтаксис повышает читаемость кода и используется во многих скриптовых языках, а потому в целом выглядит привычным для разработчиков.
В Java версии 21 и 22 шаблоны строк были в статусе preview с другим, более гибким синтаксисом. Однако, собрав обратную связь и взвесив все плюсы и минусы, разработчики удалили эту фичу из Java 23. То есть на текущий момент, если не считать сторонних библиотек, нам доступны все те же инструменты, которые появились еще во времена Java 1.5.
Разделение всех коллекций на два типа
В Java любой базовый тип коллекций (в широком смысле, а не только Collection
), будь то List
, Set
или Map
, обеспечивает и чтение, и изменение элементов. Kotlin под капотом использует всё те же коллекции из Java, но предложил изящное решение: отделить методы изменения элементов от методов чтения путем расширения интерфейса.

Поэтому в Kotlin интерфейс List
позволяет только читать элементы, а MutableList
расширяет его и добавляет методы изменения. Такие же пары есть для множества (Set
и MutableSet
) и мапы (Map
и MutableMap
). Поэтому теперь, если метод принимает в качестве параметра read-only List
, я могу быть уверен, что внутри он ее точно не изменит.
fun main() {
val digits = mutableListOf(1, 2, 3) // MutableList<Int>
processDigits(digits)
}
// метод своей сигнатурой декларирует,
// что не меняет состояние исходного списка
fun processDigits(digits: List<Int>) {
}
Уход от extends и implements
Java в свое время сделала правильно, что ушла от множественного наследования в стиле C++. В качестве альтернативы она предложила явное разделение на интерфейс и класс на уровне ключевых слов. Kotlin в этом смысле полностью копирует Java.
Однако когда в Java мы хотим унаследовать базовый класс или реализовать интерфейс, необходимо произвести некоторую когнитивную нагрузку. Если класс наследуется от класса, то он его «расширяет» (extends). Если класс реализует интерфейс, то он его имплементирует (implements). Но при этом если мы наследуем интерфейс от интерфейса, то также его «расширяем» (extends).
public class MyThread extends Thread {
// ...
}
public class MyRunnable implements Runnable {
// ...
}
public interface MyRunnable extends Runnable {
// ...
}
К чему вся эта игра слов? Если вспомнить, что интерфейсы могут иметь реализацию методов по умолчанию, то по сути это превращает их в подобие обычных классов. И поскольку мы можем наследоваться от любого количества интерфейсов, то получаем множественное наследование. А если так, то я во всех трёх случаях «наследую» от нескольких источников одновременно. Немного чересчур, не правда ли?
Kotlin предлагает не заморачиваться и всегда обозначать такое «наследование» через двоеточие.
class MyThread: Thread() {
// ...
}
class MyRunnable: Runnable {
// ...
}
interface MyRunnable: Runnable {
// ...
}
Операторы
В Kotlin меня радует, что я могу определить привычные операторы для собственных классов. Это повышает читаемость кода.
Допустим, я создал класс, представляющий собой матрицу, и хочу сложить две таких матрицы. Тогда мне достаточно определить функцию с именем plus()
и ключевым словом operator
.
class Matrix {
operator fun plus(right: Matrix): Matrix {
// тут логика по складыванию двух матриц
return resultMatrix
}
}
Теперь при работе с экземплярами этого класса я могу использовать стандартные операторы:
fun main() {
val a = Matrix()
val b = Matrix()
val c = a + b
}
В Java ничего подобного до сих пор нет. Хотя даже в C и C++ есть возможность определять пользовательские операторы.
Кстати, благодаря операторам в Kotlin можно сравнивать две строки по значению через двойное равно, в отличие от Java, где для этих целей нужно вызывать метод equals()
.
fun displayEquals(a: String, b: String) {
// здесь вызывается equals() - сравниваем по значению
println(if (a == b) "одинаковые" else "разные")
}
Также можно сравнивать два BigDecimal
, используя обычные неравенства, как с примитивными типами.
fun isLeftGreaterThanRight(left: BigDecimal, right: BigDecimal): Boolean {
return left > right // вызывается compareTo()
}
Все исключения — unchecked
В Java все исключения делятся на checked (проверяемые) и unchecked (непроверяемые). Checked наследуются от Exception, а unchecked от RuntimeException. Все checked-исключения, которые могут возникнуть внутри метода, должны быть перечислены в его сигнатуре. Казалось бы, видеть список возможных ошибок — это удобно.
private String readFile(String filename) throws FileNotFoundException, EOFException {
// читаем файл
}
Однако это очень быстро приводит к загромождению сигнатуры метода, и разработчики часто заменяют несколько исключений на их общего предка в иерархии наследования.
private String readFile(String filename) throws IOException {
// бизнес-логика не меняется
}
В сигнатуре мы теперь объявляем, что метод может возвращать вообще любые исключения, связанные с вводом-выводом (IOException). Однако, учитывая, что саму логику метода мы не меняем, это не совсем верно. На практике checked-исключения скорее мешают, чем помогают. Зачем тогда тратить время на постоянное уточнение списка исключений в сигнатуре метода?
Поэтому в Kotlin приняли волевое решение и вообще отказались от checked exceptions. Все исключения являются unchecked. Тот же метод на Kotlin выглядит так:
fun readFile(filename: String): String {
// чтение файла
}
Методы расширения
Одна из удобных фичей Kotlin — это написание методов расширения, позволяющих легко дополнять набор имеющегося API. Это особенно полезно, когда у нас нет возможности физически добавить метод в исходный класс. Такой метод может быть определен в любом месте вашего приложения.
Например, мы можем в BigDecimal
добавить метод, отображающий десятичную дробь в виде процентов.
fun main() {
val a = BigDecimal("0.10")
println(a.toPercentString()) // "10 %"
}
fun BigDecimal.toPercentString(): String {
// здесь this - ссылка на объект BigDecimal
return "${this.movePointRight(2).toInt()} %"
}
Однако эту фичу нужно использовать с осторожностью. Злоупотребления методами расширения, особенно в общих библиотечных компонентах, приводят к замусориванию скоупа во всех проектах, где будет использоваться ваша библиотека.
Нейтральные фичи
Теперь рассмотрим фичи, которые не дают заметного преимущества перед Java. Либо аналогичные возможности уже появились в Java, либо эти фичи изначально были спорными.

data и record
Еще во время учебы в вузе, лет 15 назад, я успел познакомиться с C#, и уже тогда у него были «свойства», к которым автоматически генерились get- и set-методы. Потом мне пришлось перейти на Java, и каково же было мое разочарование, когда я понял, что в Java ничего такого не было, и все приходилось писать вручную (благо IntelliJ IDEA позволяла делать это в два клика).
Например, вот так раньше выглядели все классы для хранения данных:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
// а также equals(), hashCode(), toString()...
}
Характерной чертой таких классов является их иммутабельность, то есть значения устанавливаются только на этапе создания. Это снижает количество возможных багов и даже позволяет использовать такие сложные объекты в качестве ключей в мапе, например.
В Kotlin же изначально были предусмотрены специальные data class
, в которых достаточно определить лишь сами поля, а все геттеры, equals()
, hashCode()
и toString()
— компилятор создавал автоматически.
data class Person(
val name: String,
val age: Int,
)
Согласитесь, ничего лишнего?
В Java также активно используется Lombok, который решает эти проблемы.
@Getter
@AllArgsConstructor
public class Person {
private final String name;
private final int age;
}
Но Lombok все же не часть языка, поэтому оставим его за скобками.
И вот, видимо, глядя на Kotlin, разработчики Java решили, что data-классы — это хорошо и сделали практически то же самое, только назвали record
.
public record Person(
String name,
int age
) {
}
На мой взгляд, это одна из киллер-фичей Java 16. Поэтому тут с Kotlin полный паритет.
Тип переменной после двоеточия
Сначала я считал, что писать тип переменной после имени разумно. Как известно, одна из самых больших проблем в программировании — придумывание имен переменных. Поэтому мне казалось логичным, что сначала нужно придумать имя. А когда попытаюсь описать в имени суть переменной — выбор типа уже станет механической работой.
В теории выглядит логично, но на практике не очень удобно, каждый раз приходится писать двоеточие, когда мы хотим указать тип. Учитывая, что компилятор умеет выводить типы автоматически, чаще всего тип мы не указываем. Но если я сохраняю в переменной результат работы метода и при этом хочу типизировать переменную более базовым типом, мне нужно его указать явно, вставив двоеточие и тип перед присваиванием.
fun getResult(): ArrayList<String> {
return arrayListOf()
}
fun main() {
// list1 имеет тип ArrayList<String>
val list1 = getResult()
// list2 имеет тип List<String>
val list2: List<String> = getResult()
}
В Java тоже добавили автоматический вывод типов (правда, через var). Но там мы бы просто заменили var на явный тип в начале строки и не добавляли двоеточие.
Уж если Kotlin разрабатывался как совместимый с Java, не стоило менять местами имя и тип переменной. В тех проектах, где пишут одновременно на Java и Kotlin — это вообще сплошная путаница, так как приходится постоянно перестраиваться с одной схемы объявления переменных на другую.
Недостатки Kotlin
Чтобы не выглядеть абсолютным фанатом Kotlin, приведу примеры фичей, которые я считаю скорее его недостатками.

Тип возвращаемого значения
Я склоняюсь к мысли, что излишняя вариативность синтаксиса — это скорее минус, чем плюс. Чем больше в языке вариантов сделать одно и то же, тем больше усилий нужно потратить на то, чтобы выработать в команде разработки единый стиль.
В Kotlin тип возвращаемого значения указывается в конце сигнатуры метода. Но его можно и не указывать явно. Тогда компилятор выведет тип на основе возвращаемого значения. Также можно использовать return
, а можно не использовать.
fun getMagicNumber() = 42
// то же самое
fun getMagicNumber(): Int = 42
// то же самое
fun getMagicNumber(): Int {
return 42
}
В итоге у нас есть 3 абсолютно эквивалентных записи одного и того же метода. Вроде бы, с одной стороны, удобно писать маленькие функции. Однако по мере усложнения логики мы, скорее всего, придем от первого варианта к третьему. Поэтому лучше сразу договориться в команде, чтобы везде использовать только третий вариант.
Тернарный оператор
В Java есть тернарный оператор, представляющий собой однострочный вариант конструкции if-else
.
private static void displayOddEven(int n) {
System.out.println(n % 2 == 0 ? "чёт" : "нечет");
}
В Kotlin его нет и не предвидится. Отчасти потому, что есть элвис-оператор. Но иногда тернарного оператора все-таки не хватает. Приходится использовать обычный if-else
, записывая его в одну строку, что лично мне не нравится.
fun displayOddEven(n: Int) {
println(if (n % 2 == 0) "чёт" else "нечет")
}
Мы рассмотрели основные недостатки Kotlin, и осталось понять, насколько они критичны в реальной практике и как соотносятся с преимуществами языка.

Заключение
Несмотря на то что одни фичи Kotlin уже перестали быть конкурентными, а другие являются делом вкуса, большая их часть до сих пор актуальна. А из-за необходимости сохранения обратной совместимости они вряд ли появятся в Java в обозримом будущем.
Kotlin, как молодой любовник, придал старушке Java новые силы и оказал заметное влияние на ее фичи и скорость их внедрения. Возможно, поэтому львиная доля бэкенда в корпоративном сегменте до сих пор пишется именно на Java. Но в последние годы в отдельных компаниях наблюдается тренд на плавное увеличение кодовой базы на Kotlin. Поэтому, если вы еще не пробовали писать на нем, надеюсь, моя статья будет вам полезна и даже подтолкнет к эксперименту с новым языком.
Комментарии (54)
JajaComp
08.08.2025 09:43А как же sealed и корутины как минимум?
Krokochik
08.08.2025 09:43Автор же сразу оговорился, что не добавлял то, что есть в Java. В Java уже есть как sealed интерфейсы, так и Project Loom, полностью заменяющий корутины (понятно, что какой-нибудь андроид сидит на старых jdk, но это не меняет факта, что в языке это уже есть)
treap
08.08.2025 09:43С недостатков конечно в голос
devmark Автор
08.08.2025 09:43Да, существенных недостатков по большому счету нет)
IvanVakhrushev
08.08.2025 09:43Вы забыли про vendor lock и санкции
FluffyArt
08.08.2025 09:43Вот вы знаете, для тех, кто не работал с джавой десятилетиями (сужу по себе и не только), это одна из причин, почему выбрал го, в итоге, для сайд проекта.
Не смотря на то, что мне нравится Котлин и он мне интуитивно понятен и приятен, на Ktor+ jooq сделал тестово сервис небольшой, все же свобода выбора той же ИДЕ и ресурсов перевесил не в пользу этой экосистемы. Хотя язык мне по своей сути очень зашёл.
Кстати, материалов чисто для бэкенда на мой взгляд меньше, чем на го, но это может и субъективно
Ну и, если честно, смущает, что java и kotlin такие конкурирующие языки, то есть кому-то из них надо доверить свой ценный ресурс - время
leni8ec
08.08.2025 09:43Я бы выделил ещё scope функции в Котлин, очень удобно и хорошая вариативность в использовании.
scopes: let, run, with, apply, also
Lewigh
08.08.2025 09:43У Kotlin есть и плюсы и минусы перед Java.
Но главная, я бы даже сказал капитальная проблема языка - это то что у него отсутствуют какие бы то ни было весомые киллер фичи отвечающие на вопрос разработчика или компании - зачем на него переходить. Улучшенный синтаксис и встроенные проверки - это вообще не аргумент.
Точнее, не так. У Kotlin была киллер-фича - быть мульти-языком для решения большинства проблем. Другими словами - это должен бы быть единый язык на котором бы писали и фронт и бэк и мобилку. Но идею не вывезли. KotlinJs особо никому не нужен. KMM оказался не лучшей идеей и не взлетел. Остался только KotlinJvm который по сути better java. Раньше хотя бы корутины были преимуществом, но с появление виртуальных потоков и они были нивелированы. Kotlin к сожалению сейчас ничего не может предоставить весомого чтобы било Java и было поводом перейти на этот язык.
Как показало время, как мне кажется, это и стало причиной того что язык взлетел только на андроиде.
devmark Автор
08.08.2025 09:43На бекенде он тоже используется. Когда только Котлин появился, он закрыл прям заметные проблемы java. Сейчас в java появились аналогичные фичи и в этом немалая заслуга Котлин) а вообще наряду с контролем за null я бы отметил ещё разделение коллекций на изменяемые и неизменяемые.
ermadmi78
08.08.2025 09:43Я на Java писал 18 лет. На Kotlin пишу последние 5 лет. К мобильной разработке никакого отношения не имею, занимаюсь сугубо бекендом. На Kotlin за 5 лет запустил разработку 4х крупных проектов в качестве техлида. С поиском работы никаких проблем никогда не было. Что я делаю не так?
ermadmi78
08.08.2025 09:43Kotlin к сожалению сейчас ничего не может предоставить весомого чтобы било Java и было поводом перейти на этот язык.
Не хочу участвовать в холиваре. Но слово "перейти" на мой взгляд это некоторое преувеличение. Kotlin эта та же Java, но просто удобнее. В свои проекты я обычно нанимаю джавистов. Через пару недель после выхода они на Kotlin начинают писать как на Java. Через 3 месяца они на Kotlin пишут как на Kotlin.
Lewigh
08.08.2025 09:43Я не говорю что на Kotlin нет проектов или вакансий. Я о том что язык смысл которого был подвинуть Java со своей задачей не справился. Он зашел только на андроиде потому что там бесперспективняк. На бэкенде все очень скромно, особенно учитывая насколько дешево перейти на Kotlin - вся экосистема тажа самая, нужно заменить только язык и все. Но даже при таких благоприятных обстоятельствах и за столько лет Kotlin занимает очень скромную долю рынка. Ладно можно сказать много легаси, но новые проекты тоже продолжают массово стартовать на Java.
А почему так я уже написал - нет каких то серьезных позиций где Kotlin бы мог то что не может Java. Рантайм такой же, библиотеки такие же в большинстве. Корутины уже не преимущество. По производительности Kotlin даже чуть проигрывает Java. Остаеться чуть лучше синтаксис, закрытие какие то проблем да, чуть понадежнее да. Кому то норм, а кому то из разрабов будет влом забивать себе голову еще одним языком который чуть лучше. Многим компаниям, как выясняется, тоже влом заморачиваться с поиском котлинистов или переучиванием джавистов. Оно бы можно было, если была киллер фича, которая бы это оправдывала, но ее нет.
Хороший пример Go. У языка огромное количество проблем, но есть несколько киллерфич, за счет которых он взлетел, которые выделяют его на фоне других и которые являются аргументом для команды и компании, даже не смотря на то что там вообще отдельная молодая экосистема.
Поэтому можно конечно рассказывать что на нем пишут бэкенды - пишут, и что вакансии есть - есть, но по факту все это очень скромно для такого задела и стольких лет.
ermadmi78
08.08.2025 09:43Ну, подвинуть Java - это изначально не реалистичная цель была. А вот создать весьма и весьма достойную альтернативу Java - ИМХО, получилось. Я искренне люблю и уважаю Java, но пишу на Kotlin. Так как он мне нравится ещё больше, чем Java :)
Lewigh
08.08.2025 09:43Ну, подвинуть Java - это изначально не реалистичная цель была.
Вполне реалистичная. На том же андроиде Kotlin вообще вытеснил Java. Другой пример, как я уже писал - Go. Язык, у которого куча детских болячек, у которого не было ни экосистемы нормальной ни даже пакетного менеджера, плюс который откровенно недолюбливали многие за его примитивность. Т.е. по сравнению с Kotlin у которого на старте была уже вся экосистема и инструменты, там вообще все плохо. И тем не менее, он взлетел и откуси не то что у Java а у всего пула backend языков не слабый кусок рынка.
И вот получается, есть красивый милый приятный современный Kotlin с весьма скромными результатами на уровне приза зрительских симпатий. А есть кривой косой морально устаревший Go который при этом подвинул рынок. И еще раз повторю - все потому что Go было что предложить рынку кроме своей приятности а Kotlin - нет.
Это я не к тому что не люблю Kotlin а к тому - почему для этого языка дела сейчас обстоят так как обстоят.
devmark Автор
08.08.2025 09:43В целом понимаю вашу точку зрения. И хочу дополнить, что Kotlin на Android взлетел благодаря поддержке со стороны Google.
valery1707
08.08.2025 09:43Поэтому теперь, если метод принимает в качестве параметра read-only
List
, я могу быть уверен, что внутри он ее точно не изменит.Всё же уверенным быть нельзя - можно только надеяться:
fun processDigits(digits: List<Int>) { if (digits is MutableList<Int>) { digits.add(0) } } fun main() { val digits = mutableListOf(1, 2, 3) // MutableList<Int> processDigits(digits) println(digits) // [1, 2, 3, 0] }
devmark Автор
08.08.2025 09:43Если мы хотим выстрелить себе в ногу, то никто нам не может помешать)
Такие конструкции это всё-таки плохой код (мягко говоря), потому что мы нарушаем инкапсуляцию и полиморфизм. Тут речь больше про контракт: если я вижу, что метод принимает read-only list, значит метод декларирует, что он не собирается менять этот список.
JVyacheslav
08.08.2025 09:43Вряд ли кому-то сейчас придёт в голову писать на Java такой код:
public static void main(String[] args){String text="Hello!";System.out.println(text);}
А вот и нет! Мне пришло. Сижу периодически на платформе codingame когда делать нечего. Иногда там встречается соревнование "кто короче напишет" - подсчёт идёт в символах. Удаление всех пробелов очень спасает, иногда даже короче получается, чем у некоторых питонистов. Так что это определённо важная фича, а вы её назвали бесполезной...
if (message != null) {
System.out.println(message);
} else {
System.out.println("Нет текста");
}
Это не эквивалент решения, которое вы предоставили на котлине. В джаве есть тернарные операторы и всё это записывается также в одну строчку.
К чему вся эта игра слов?
А почему нет? Это не плюс на самом деле... Это даже скорее минус, что ты не можешь видеть, от чего ты наследуешься: от класса или метода.
В Kotlin есть механизм интерполяции строк, когда мы можем подставлять переменную непосредственно в строку с помощью знака доллара ($). Если над переменной при этом нужно выполнить еще какое-то действие (например, прибавить 1), вся конструкция заключается в фигурные скобки
thymeleaf на минималках какой-то... Польза от того сомнительная, но окэй.
На практике checked-исключения скорее мешают, чем помогают
Товарищ, вы боритесь менее, чем за 1 строку кода! Остановитесь! Разработчику полезно знать, где вообще подобный эксепшон в перспективе может появиться.
В конце статьи вы таки вспомнили о наличии тернарного оператора. Но уже слишком поздно. Итого получается, что весомых аргументов тупо нет. Кроме, возможно, защиты от NPE. А ещё вы, по моему мнению, несколько пренебрегаете преимуществами джавы.
XViivi
08.08.2025 09:43Товарищ, вы боритесь менее, чем за 1 строку кода! Остановитесь! Разработчику полезно знать, где вообще подобный эксепшон в перспективе может появиться.
Я сам не джавист, но всё же условно в вузе с джавой мы работали и за это время я заметил маленький минус у такого подхода. Условно я не могу написать так:
Function<A, B> f = this::someMethodThatThrows;
По этому мне придётся оборачивать:
Function<A, B> f = a -> { try { someMethodThatThrows(a); } catch (Exception e) { throw RuntimeException(e); } };
Я всё же считаю, что такое оборачивание является некоторой проблемой и не так далеко ушло от какого-то
err != nil
в go — хоть и не нужно так часто.Yami-no-Ryuu
08.08.2025 09:43Не согласен, чекед исключение - это аналог Result<B, BaseError>.
Function<A, B> f = a -> { var res = someMethodThatErrs(a); if (res.hasError()) throw new... return res.value; }
В лучшем случае, в худшем if (res.error instanceof ParseError)
Те же яйца, вид в профиль.
А для if(res.hasError()).... В Котлин добавили библиотеку с Error монадой, криво косо, но добавили.
Без синтаксического сахара больно смотреть правда...
XViivi
08.08.2025 09:43Может быть, всё же дело в том, что я не джавист и джавовские практики мне не знакомы. Если так рассудить, явные исключения действительно схожи с
Result
, а неявные сpanic
. Но, в какой-то мере, мне кажется, что проблема исключений в некоторой степени и в том, что ты с ними работать будешь как с исключениями. Возможно, было бы удобно всё же сделать какой-тоResult
типа такого, чисто чтобы что-то делать локально внутри функции и локально с результатом конкретной функции — хотя это, наверное, и грозит потерей стектрейса (или нет, я не помню, как это работает).public sealed interface Result<T, E extends Exception> { record Ok<T, E extends Exception>(T value) implements Result<T, E> { public T unwrap(){ return value; } public T rethrow() { return value; } public <T0> Ok<T0, E> map(Function<T, T0> fn) { return new Ok<>(fn.apply(value)); } //and other } record Err<T, E extends Exception>(@NotNull E err) implements Result<T, E> { public T unwrap(){ throw new RuntimeException(err); } public T rethrow() throws E { throw err; } @SuppressWarnings("unchecked") public <T0> Err<T0, E> map(Function<T, T0> fn) { return (Err<T0, E>)this; } //and other } @FunctionalInterface interface ThrowingFn<T, E extends Exception>{ T run() throws E; } @SuppressWarnings("unchecked") @NotNull static<T, E extends Exception> Result<T, E> catch_result(ThrowingFn<T, E> fn) { try { return new Ok<>(fn.run()); } catch (Exception e) { return new Err<>((E)e); } } static<T> T unwrap_result(ThrowingFn<T, ? extends Exception> fn) { try { return fn.run(); } catch (Exception e) { throw new RuntimeException(e); } } T unwrap(); T rethrow() throws E; <T0> Result<T0, E> map(Function<T, T0> fn); //and other }
Во всяком случае, я думаю, что есть пару вещей, которые изменить бы хотелось, чтобы всё работало приятнее:
-
Синтаксис
Условный
try{ } catch(ExceptionType e) {}
лично для меня имеет пару проблем в дизайне:1) Фигурные скобки вокруг try и catch обязательные — рассчитаны лишь на то, чтобы оборачивать всю функцию — при необязательности для остальных конструкций.
2) Тип исключения — если throws функции лишь одного типа, тип в catch можно бы и вывести из контекста
3) Блок try всегда возвращает void — нужно заводить переменную вне блока для результата.
4) Сахар на
try{ } catch(ExceptionType e) {throw new RuntimeException(e)}
всё же был бы приятен Неявность и схожесть с неявными исключениями — поставь
throws Exception
на функцию — и не заметь throw там, где ты уже хочешь его поймать.Не помешали бы существующие отдельно методы, кидающие явные и неявные исключение в тех случаях, когда принято кидать лишь неявные.
Неудобность с точки зрения типизации. Опять же, условный
Function<A, B>
уже запрещает кидать явные эксепшены. Если бы явные исключения становились частью возвращаемого типа, было бы удобнее. Хотя и можно сделать условныйThrowingFunction<ArgT, ResT, ExT extends Exception>
, навроде того, что я написал сверху, но так-то большинству библиотек и фреймворков всё же будет всё равно.
Но может быть и такое, что это всё вообще не нужно, всё и так работает замечательно, я не прав и у меня мозг просто немного оrustенел, так сказать.
-
devmark Автор
08.08.2025 09:43А вот и нет! Мне пришло. Сижу периодически на платформе codingame когда делать нечего. Иногда там встречается соревнование "кто короче напишет" - подсчёт идёт в символах. Удаление всех пробелов очень спасает, иногда даже короче получается, чем у некоторых питонистов. Так что это определённо важная фича, а вы её назвали бесполезной...
Данную статью я писал точки зрения промышленной разработки больших корпоративных проектов (основная ниша java и kotlin). Там всё-таки код в одну строку не пишут.
А почему нет? Это не плюс на самом деле... Это даже скорее минус, что ты не можешь видеть, от чего ты наследуешься: от класса или метода.
По поводу наследования, в процессе agile-разработки очень часто возникает ситуация, что интерфейс превращается в абстрактный класс или наоборот. Вот эти все extends и implements в таком случае особой ценности не несут.
thymeleaf на минималках какой-то... Польза от того сомнительная, но окэй.
Как человек, активно использующий thymeleaf в некоторых проектах, могу вас заверить, что котлиновская интерполяция строк поприятнее, т.к. она мощнее.
Товарищ, вы боритесь менее, чем за 1 строку кода! Остановитесь! Разработчику полезно знать, где вообще подобный эксепшон в перспективе может появиться.
В том-то и дело, что не одну) Очень часто вереница checked-эксепшенов занимает несколько строк. Особенно при использовании автоформатирования.
lampa_torsherov
08.08.2025 09:43Что касается checked-исключений, это конечно древний костыль, но костыль легко обходимый без смены языка. В новом коде можно просто их не писать, а для вызова методов с throws существует zero-cost (ну, почти zero) хак:
@SuppressWarnings("unchecked") static <T extends Throwable> void throwAny(Throwable t) throws T { throw (T) t; }
И теперь можно откуда угодно делать
try { foo(); // throws smt } catch (Throwable e) { throwAny(e); }
Вуаля, проблема решена. Ну, или можно пользоваться checked-фичей, как задумано. Тоже как вариант.
P.S. В случае с InterruptedException я бы даже сказал, что это плюс - такая себе жесткая напоминалка не забыть флаг прерывания.
dimaludi
08.08.2025 09:43Сравним метод на Java, печатающий в консоли строку текста: ...
Можно также лаконично написать на java:public
void
displayNullable(
String
message) {
System.out.println(message != null ? message : "Нет текста");
}
Krokochik
08.08.2025 09:43В Kotlin меня радует, что я могу определить привычные операторы для собственных классов. Это повышает читаемость кода.
Очень смело, конечно, заявлять, что переопределение операторов повышает читаемость кода :)
devmark Автор
08.08.2025 09:43Операторы очень выручают при работе со стандартной библиотекой. Я могу использовать математические операторы для работы с числами BigDecimal:
val a = BigDecimal("100.00") val b = BigDecimal("200.00") val c = BigDecimal("300.00") val sum = a * b + c // против Java-варианта: BigDecimal sum = a.multiply(b).plus(c);
Согласитесь, что мне не нужно каждый раз лезть в реализацию стандартных математических действий. Но при этом возможность записи всяких формул в естественном виде - это удобно.
То же самое я могу сделать для своего собственного класса, например, для сложения матриц.
При этом в котлине я не могу определять вообще произвольные комбинации символов как операторы. А только фиксированный набор стандартных операторов.
denis_iii
08.08.2025 09:43После появления виртуальных потоков в JVM, Java развивает идею Scoped Values и Structured Concurrency JEP`s.
Сложно представить, что условный Netflix будет использовать корутины. Значит Kotlin будет подстраивать свое API и нужно ждать интеграции пока StructuredTaskScope завернут в CoroutineScope с исключениями, завершением задач и пр. А Java еще на шаг вперед уйдет.
Плюс вопрос усилий, нужно ли это авторам Kotlin, или они вполне логично сосредоточат силы на Android/JS в будущем.
ofigenn
08.08.2025 09:43Чтобы заоаботать , нужно создать спрос. "Делаем Kotlin, оставляем его нормальную поддержку только в IDEA, profit".
Anarchist
08.08.2025 09:43null-контроль во всех JVM-языках работает ровно для момента, когда требуется интероперабельность с Java. Вызываете метод Java, и вы уже точно не знаете, вернется null или нет. С одной стороны, null нужно подозревать всегда, с другой даже в Java есть методы, которые никогда null не возвращают. Тони Хоар, человек, придумавший null, очень раскаивался в этом.
Исключения (и checked, и unchecked) - ещё одна родовая травма Java. И все JVM-языки вынуждены как-то иметь с ними дело. Показательно, что в изначально таком чисто Java-проекте, как Spring, от checked отказались.
По поводу if ... else против тернарного оператора и return имею противоположную точку зрения: if ... else читабельнее, а return лучше вообще не использовать. Return лишь немногим лучше goto, как и break, и прочие "прерыватели" логики функций.
devmark Автор
08.08.2025 09:43По поводу интеропа с java, мы стараемся у себя в проектах не смешивать java и kotlin. То есть если пишем на Kotlin, то все исходники должны быть на Kotlin (в том числе юзаем build.gradle.kts вместо грувишного buld.gradle). Но при этом мы активно используем Spring и уже знаем, где могут возникать null при вызове стандартных компонентов, а где не могут. На самом деле, если конфиг правильный, то в большинстве случаев у нас будет создан какой-то инстанс. А если нет - мы узнаем об этом ещё при старте приложения.
r_Rain
08.08.2025 09:43Забыли сказать про то, что в котлине куча удобных методов для работы с коллекциями, котлин уменьшает кол-во скобок благодаря удобному объявлению и использованию лямбд ( list.forEach { printlin("item: $it") }; map.forEach { k, v -> println("$k: $v")} ), у котлина человеческий when (хотя в новых джавах что-то подобное уже в switch добавили), не надо писать этот дурацкий new, можно ещё свойства-расширания делать, что как и функции-расширения позволяет чейнить вызовы, а не оборачивать в скобки в обратном порядке, есть корутины, есть object Obj { }, вызов функции с именованными параметрами myFun(cnt = 10, name = "n"), inline fun, ranges (1..10), спасибо, что не надо париться между примитивами и их обёртками (Int для int & Integer), всякие if-else, try-catch, when могут возвращать значение, when { a -> {}, b -> {}, else -> {} } можно использовать как замену if-else. И тут я наверняка ещё в 2 раза больше могу написать, но тут уже всего не упомнишь. Так что вам ещё предстоят открытия :).
Для меня из недостатков - это выпиленный for (;;). Отсутствие тернарников скорее хорошо, чем плохо, потому что они читабельны, если однострочны, чаще же всего все эти тернарники впоследствии разрастаются и лучше писать if-else или when.devmark Автор
08.08.2025 09:43Спасибо за дополнение! В Kotlin работа с лямбдами действительно удобна. Однако ими тоже не следует злоупотреблять: большое количество вложенных друг в друга лямбд порождают "лесенку" в коде.
Цикл for очень удобен при "низкоуровневом", алгоритмическом программировании. Особенно когда у нас кастомный алгоритм обхода коллекции, а не банальный инкремент. Но в типовых приложениях чаще всего мы работаем с коллекциями и чаще всего перебираем элементы по очереди от начала до конца. Поэтому видимо авторы и решили оставить только for-each.
empty-sky
08.08.2025 09:43Странно, что забыли намного более удобно организованную работу с методами/функциями
Именованные параметры позволяют не вспоминать, в каком же порядке и что значат эти 5 строковых параметров в методе стандартной либы, плюс страхуют от ошибок формата "поменял местами аргументы метода", так как от их порядка ничего зависеть не будет в отличие от Java
Ну и приятная мелочь в виде значений по умолчанию тоже делает написание кода приятнее и удобнее
devmark Автор
08.08.2025 09:43Полностью согласен! Я хотел про это написать и действительно забыл) Однако тут есть и обратная сторона медали, как и в случае с возвращаемым значением: с точки зрения единообразия в команде надо договориться, в каких случаях делаем именованные параметры, в каких - не делаем. Или всегда делаем.
iliks84
Как вы на практике с ним работаете-то, в ситуации когда идея недоступна для скачки. Обычным маленьким компаниям условно «можно» скачать, подхимичить, но вы банк… Джетбрейнз прямым текстом говорит мы в России ничего не будем разрешать, даже бесплатный продукт, нам санкции всего мира запрещают (а вот микрософту что-то н чего не запрещает…).
Ставите openide что ли? В vs code работать пока с котлином невозможно, уровень плагина не тот.
ermadmi78
Я GigaIDE использую. Можно взять OpenIDE. Обе среды поддерживают Kotlin
iliks84
Хорошо, а приятно пользоваться языком, чьи авторы на багрепорты от русских отвечают в стиле «вы из России, мы вас не обслуживаем»?
ris58h
Посмотрел: я 72 issue создал в их YouTrack - ни на один так не ответили. Что я делаю не так?
iliks84
Ну а людям именно так отвечают:
https://youtrack.jetbrains.com/issue/WEB-59910/Instantiation-Expressions-doesnt-work-with-comma#focus=Comments-27-6966043.0-0
По их отписке стандартной можно погуглить и увидеть, что они многим так отвечают
iliks84
По слову sanctions в youtrack около 500 проблем таким образом закрытых находится. То что у вас не закрыли это скорее их недосмотр, а не то что этого нет.
Вот еще эпичный диалог, как раз про котлин:
https://youtrack.jetbrains.com/issue/KT-56783/kotlinc-js-empty-output-IR
sdramare
Jetbrains просто соблюдает законодательство своей юрисдикции и на это указывают. Претензии должны быть не к ним.
devmark Автор
Так же как и с Java. GigaIDE, openIDE, community edition.
iliks84
Community edition точно так же лицензия их запрещает использовать в России. Остаются только пересборки наши.
sdramare
Майкрософту не мешает?