В прошлой части я рассказывал о подводных камнях Kotlin, в этой я расскажу что их перевешивает.
Меня несколько раз спрашивали, а что же такого в Kotlin, что могло бы сподвигнуть на переход с Java, в чем его фишка? Да, Kotlin привнес Null-safety и большое количество синтакcического сахара, закрыл часть слабых мест Java. Но это не становится поводом для перехода. Что может стать? Новые возможности и новая философия.

Об этом говорится в первую очередь. Это действительно делает код более безопасным. Да, есть проблема при вызове Java. Сводится она к двум вариантам:
Сам ещё не использовал, но, судя по всему, это может сильно изменить подход к многопоточному программированию. UPD Я исправился и написал статью о корутинах.
К сожалению, тоже ещё не пробовал. На момент начала моего проекта, это было только в бете, да и с angular не был знаком. Сейчас чешутся руки попробовать — единый модуль с DTO для серверной и клиентской части.
Философия Kotlin — Concise, Safe, Interoperable, Tool-friendly (с оф. сайта и докладов).
Совместимость с Java действительно 100%-ая в обе стороны. Чтобы я ни делал, всё работало отлично.
На Eclipse не пробовал, но в Intellij всё замечательно, и продолжает улучшаться.
На мой прагматичный взгляд — это самое главное. Concise, Interoperable, Tool-friendly — это минимальные условия выживания языков на JVM, иначе они будут проигрывать Java.
К примеру, локальная переменная — list из строк. В java компилятор знает только о двух вариантах:
Второй вариант встречается в единичных случаях. Обычно, это не 8-ая Java, и этот list нужен в анонимном классе.
А вот Kotlin:
Что это дает? У каждого типа есть свои гарантии и набор разрешенных над ним операций. Так, += null можно вызывать только на var list12: List<String?>, а add(null) — на val list8: MutableList<String?> и var list16: MutableList<String?>.
При каждом объявлении писать полный тип будет накладно. Поэтому есть вывод типов:
Когда пишешь код, не хочется лишнее поле объявлять как nullable, чтобы позже не писать? и ?:, а большая часть операций приводит к immutable коллекциям. В итоге, в коде объявляются самые узкие состояния, что дает более строгие контракты и снижает сложность программы.
Надо было хранить последовательности изменения некоторых параметров, для этого написал интерфейс History для хранения такого параметра и extension SortedMap<Date, out History> для выравнивания содержимого после изменений:
Есть filterNot (чаще можно использовать method reference), отдельные first и firstOrNull и т.д… А если чего-то не хватает — дописываешь свой extension (я, к примеру, дописывал sum для листа BigDecimal).
При работе с jsf это просто спасение. Jsf часто дергает одно и то же поле (а я за ним в базу хожу), а в случае таблицы с сортировкой, ожидает, что вернется ровно тот же объект, что и в прошлый раз. А главное, lazy очень легко убрать/вставить.
Разница небольшая, но шансов ошибиться намного меньше, а ещё можно из инета копипастить без головной боли.
Кроме того, очень приятные шаблоны.
У меня только один проект на Kotlin, не считая мелких поделок. Так что могу с уверенностью сказать одно: Kotlin, Spring и элементы DDD отлично друг друга поддерживают. Если писать на Kotlin как на Java, почувствуется только синтаксический сахар (что уже приятно), но если отказаться от классических бинов, в которые кто угодно может что угодно вставить (а значит, практически нет ограничений на состояния), то Kotlin расцветет.
UPD
Послевкусие от Kotlin, часть 1
Послевкусие от Kotlin, часть 3. Корутины — делим процессорное время
Меня несколько раз спрашивали, а что же такого в Kotlin, что могло бы сподвигнуть на переход с Java, в чем его фишка? Да, Kotlin привнес Null-safety и большое количество синтакcического сахара, закрыл часть слабых мест Java. Но это не становится поводом для перехода. Что может стать? Новые возможности и новая философия.

Новые возможности
1. Null safety
Об этом говорится в первую очередь. Это действительно делает код более безопасным. Да, есть проблема при вызове Java. Сводится она к двум вариантам:
- Вызов сторонних библиотек. Лечится либо явным объявлением типа переменной, к которой присваивается результат, либо написанием extension, что и так часто хочется делать для выпрямления цепочки вызовов (пример будет в конце).
- Получение сообщения из внешнего мира (REST, MQ и т.д.). Тут на помощь приходит Spring Validate.
2. Корутины
Сам ещё не использовал, но, судя по всему, это может сильно изменить подход к многопоточному программированию. UPD Я исправился и написал статью о корутинах.
3. Компиляция в JavaScript
К сожалению, тоже ещё не пробовал. На момент начала моего проекта, это было только в бете, да и с angular не был знаком. Сейчас чешутся руки попробовать — единый модуль с DTO для серверной и клиентской части.
Новая философия
Философия Kotlin — Concise, Safe, Interoperable, Tool-friendly (с оф. сайта и докладов).
1. Interoperable
Совместимость с Java действительно 100%-ая в обе стороны. Чтобы я ни делал, всё работало отлично.
2. Tool-friendly
На Eclipse не пробовал, но в Intellij всё замечательно, и продолжает улучшаться.
3. Safe
На мой прагматичный взгляд — это самое главное. Concise, Interoperable, Tool-friendly — это минимальные условия выживания языков на JVM, иначе они будут проигрывать Java.
Null-safety + mutability
К примеру, локальная переменная — list из строк. В java компилятор знает только о двух вариантах:
List<String> list1; final List<String> list2;
Второй вариант встречается в единичных случаях. Обычно, это не 8-ая Java, и этот list нужен в анонимном классе.
А вот Kotlin:
val list1: List<String>? val list2: List<String?>? val list3: List<String> val list4: List<String?> val list5: MutableList<String>? val list6: MutableList<String?>? val list7: MutableList<String> val list8: MutableList<String?> var list9: List<String>? var list10: List<String?>? var list11: List<String> var list12: List<String?> var list13: MutableList<String>? var list14: MutableList<String?>? var list15: MutableList<String> var list16: MutableList<String?>
Что это дает? У каждого типа есть свои гарантии и набор разрешенных над ним операций. Так, += null можно вызывать только на var list12: List<String?>, а add(null) — на val list8: MutableList<String?> и var list16: MutableList<String?>.
При каждом объявлении писать полный тип будет накладно. Поэтому есть вывод типов:
val test = Random().nextBoolean() val list1 = if (test) null else listOf("") val list2 = if (test) null else listOf(null, "") val list3 = listOf("") val list4 = listOf(null, "") val list5 = if (test) null else mutableListOf("") val list6 = if (test) null else mutableListOf(null, "") val list7 = mutableListOf("") val list8 = mutableListOf(null, "") var list9 = list2?.filterNotNull() var list10 = list2 var list11 = list2?.filterNotNull() ?: emptyList() var list12 = list2 ?: emptyList() var list13 = list2?.filterNotNull()?.toMutableList() var list14 = list2?.toMutableList() var list15 = list2?.filterNotNull()?.toMutableList() ?: mutableListOf() var list16 = list2?.toMutableList() ?: mutableListOf()
Когда пишешь код, не хочется лишнее поле объявлять как nullable, чтобы позже не писать? и ?:, а большая часть операций приводит к immutable коллекциям. В итоге, в коде объявляются самые узкие состояния, что дает более строгие контракты и снижает сложность программы.
Поддержка Safe другими возможностями языка
- Естественным образом хочется уходить от конструктора без параметров и сеттеров для полей, так как конструктор без параметров будет создавать некомпилируемое состояние объекта — null логин пользователя, к примеру.
- Язык поощряет отсутствие локальных переменных — нет промежуточных состояний.
- Удешевление DTO — data class. Так мы можем ослабить контроль над состояниями, когда передаем объект в UI, не ослабляя контракты модели.
- Удешевление перегрузки методов — параметры по умолчанию — нет соблазна писать кучу setters.
Примерdata class Schedule( val delay: Int, val delayTimeUnit: TimeUnit = TimeUnit.SECONDS, val rate: Int? = null, val rateTimeUnit: TimeUnit = TimeUnit.SECONDS, val run: () -> Unit ) fun usage() { Schedule(1) { println("Delay for second") } Schedule(100, TimeUnit.MILLISECONDS) { println("Delay for 100 milliseconds") } Schedule(1, rate = 1) { println("Delay for second, repeat every second") } }
То что может появиться
- Inline classes/Value classes. Позволит делать классы-обертки вокруг примитивов, при этом компилировать без этого класса. Можно будет, к примеру, сделать два типа строк: login и email, которые не будут друг к другу каститься. Слышал об этом на Jpoint.
- Truly immutable data. Поддержка на уровне синтаксиса изменяемости объекта. Immutable объект не сможет содержать ссылки на не immutable и как-либо их изменять. Заняло третье место при голосовании за новые возможности языка.
4. Concise (примеры из моего проекта, практически как есть)
Нет ограничения «один файл — один класс»
Работа со spring data у меня выглядит так (всё в одном файле):
@Repository interface PayerRepository : CrudRepository<Payer, Int> { fun findByApprenticeId(id: Int): List<Payer> } @Repository interface AttendanceRepository : CrudRepository<LessonAttendance, LessonAttendance.ID> { fun findByDateBetween(from: Date, to: Date): List<LessonAttendance> } fun AttendanceRepository.byMonth(month: Date): List<LessonAttendance> { val from = month.truncateToMonth() val to = month.addMonths(1).subtractDays(1) return findByDateBetween(from, to) } //ещё 10 репозиторий inline fun <reified T, ID: Serializable> CrudRepository<T, ID>.find(id: ID): T { return findOne(id) ?: throw ObjectNotFound(id, T::class.qualifiedName) }
Extensions.
Выпрямляем обращение к DateUtils
Kotlin
Java
fun isJournalBlocked(date: Date, forMonth: Date) = forMonth <= date.subtractMonths(1).subtractDays(10) //используется ещё в 20 местах fun Date.subtractMonths(amount: Int): Date = DateUtils.addMonths(this, -amount) //используется ещё в 8 местах fun Date.subtractDays(amount: Int): Date = DateUtils.addDays(this, -amount)
Java
public boolean isJournalBlocked(Date date, Date forMonth) { return date.compareTo(DateUtils.addDays(DateUtils.addMonths(forMonth, -1), -1)) <= 0; }
Надо было хранить последовательности изменения некоторых параметров, для этого написал интерфейс History для хранения такого параметра и extension SortedMap<Date, out History> для выравнивания содержимого после изменений:
Реализация
interface History<out T> { val begin: Date var end: Date? fun historyOf(): T fun containsMonth(date: Date): Boolean { val month = date.truncateToMonth() return begin <= month && (end == null || month < end) } } fun <T> SortedMap<Date, out History<T>>.fix() { removeRepeatedNeighbors() val navigableMap = TreeMap<Date, History<T>>(this) values.forEach { it.end = navigableMap.higherEntry(it.begin)?.value?.begin } } private fun <T> SortedMap<Date, out History<T>>.removeRepeatedNeighbors() { var previousHistory: T? = null for (history in values.toList()) { if (history.historyOf() == previousHistory) { remove(history.begin) } else { previousHistory = history.historyOf() } } } //usage: fun setGroup(from: Date, group: ClassGroup) { val history = GroupHistory( this, group, from.truncateToMonth(), null ) groupsHistory[history.begin] = history groupsHistory.fix() this.group = groupsHistory.getValue(groupsHistory.lastKey()).group }
Операции с коллекциями
Пример 1.
Kotlin
Java
val apprentices: List<ApprenticeDTO> = apprenticeRepository.findAll() .map(::ApprenticeDTO) .sortedWith(compareBy({ it.lastName }, { it.firstName }))
Java
List<ApprenticeDTO> apprentices = StreamSupport.stream( apprenticeRepository.findAll().spliterator(), false ).map(ApprenticeDTO::new) .sorted(Comparator.comparing(ApprenticeDTO::getLastName) .thenComparing(Comparator.comparing(ApprenticeDTO::getFirstName))) .collect(Collectors.toList());
Пример 2.
Kotlin
Java
val attendances: Map<Pair<Date, Int>, Int> attendances = attendanceRepository .byMonth(month) .groupBy { it.date to it.group.id } .mapValues { it.value.count() } .toMap()
Java
Map<Pair<Date, Integer>, Integer> attendances = attendanceRepository .byMonth(month) .stream() .collect(Collectors.groupingBy((it) -> new Pair<>(it.getDate(), it.getGroup().getId()))) .entrySet() .stream() .map(entry -> new Pair<>(entry.getKey(), entry.getValue().size())) .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
Есть filterNot (чаще можно использовать method reference), отдельные first и firstOrNull и т.д… А если чего-то не хватает — дописываешь свой extension (я, к примеру, дописывал sum для листа BigDecimal).
Lazy
При работе с jsf это просто спасение. Jsf часто дергает одно и то же поле (а я за ним в базу хожу), а в случае таблицы с сортировкой, ожидает, что вернется ровно тот же объект, что и в прошлый раз. А главное, lazy очень легко убрать/вставить.
Smart cast + sealed class
Пример
Kotlin
Java
fun rentForGroup(month: Date, group: ClassGroup): Int { val hall = group.hall val hallRent = hall.rent(month) return when (hallRent) { is Monthly -> hallRent.priceForMonth() / hall.groups(month).size is PercentOfRevenue -> hallRent.priceForMonth(creditForGroup(month, group)) is Hourly -> hallRent.priceForLessons(group.monthLessons(month)) } }
Java
public int rentForGroup(Date month, ClassGroup group) { Hall hall = group.getHall(); Rent hallRent = hall.rent(month); if (hallRent instanceof Monthly) { return ((Monthly) hallRent).priceForMonth() / hall.groups(month).size(); } else if (hallRent instanceof PercentOfRevenue) { return ((PercentOfRevenue) hallRent).priceForMonth(creditForGroup(month, group)); } else if (hallRent instanceof Hourly) { return ((Hourly) hallRent).priceForLessons(group.monthLessons(month)); } else { throw new UnsupportedOperationException(); } }
Inline functions
На Java это просто невозможно сделать (если не добавлять явный парметр с классом).
inline fun <reified E : Throwable> assertFail(expression: () -> Unit) { try { expression() Assert.fail("expression must fail with ${E::class.qualifiedName}") } catch (e: Throwable) { if (e !is E) { throw e } } } @Test fun greenTest() { assertFail<ArrayIndexOutOfBoundsException> { arrayOf(1, 2)[3] } }
inline fun <reified T, ID: Serializable> CrudRepository<T, ID>.find(id: ID): T { return findOne(id) ?: throw ObjectNotFound(id, T::class.qualifiedName) }
String literals.
Разница небольшая, но шансов ошибиться намного меньше, а ещё можно из инета копипастить без головной боли.
regexp
Kotlin
Java
val email = """^([_A-Za-z0-9-+]+(\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\.[A-Za-z0-9]+)*(\.[A-Za-z]{2,}))?$"""
Java
String email = "^([_A-Za-z0-9-+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,}))?$"
Кроме того, очень приятные шаблоны.
Послевкусие
У меня только один проект на Kotlin, не считая мелких поделок. Так что могу с уверенностью сказать одно: Kotlin, Spring и элементы DDD отлично друг друга поддерживают. Если писать на Kotlin как на Java, почувствуется только синтаксический сахар (что уже приятно), но если отказаться от классических бинов, в которые кто угодно может что угодно вставить (а значит, практически нет ограничений на состояния), то Kotlin расцветет.
UPD
Послевкусие от Kotlin, часть 1
Послевкусие от Kotlin, часть 3. Корутины — делим процессорное время