Послевкусие от Kotlin, часть 2

    В прошлой части я рассказывал о подводных камнях Kotlin, в этой я расскажу что их перевешивает.

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



    Новые возможности


    1. Null safety


    Об этом говорится в первую очередь. Это действительно делает код более безопасным. Да, есть проблема при вызове Java. Сводится она к двум вариантам:

    1. Вызов сторонних библиотек. Лечится либо явным объявлением типа переменной, к которой присваивается результат, либо написанием extension, что и так часто хочется делать для выпрямления цепочки вызовов (пример будет в конце).
    2. Получение сообщения из внешнего мира (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 другими возможностями языка


    1. Естественным образом хочется уходить от конструктора без параметров и сеттеров для полей, так как конструктор без параметров будет создавать некомпилируемое состояние объекта — null логин пользователя, к примеру.
    2. Язык поощряет отсутствие локальных переменных — нет промежуточных состояний.
    3. Удешевление DTO — data class. Так мы можем ослабить контроль над состояниями, когда передаем объект в UI, не ослабляя контракты модели.
    4. Удешевление перегрузки методов — параметры по умолчанию — нет соблазна писать кучу 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")
          }
      }
      


    То что может появиться


    1. Inline classes/Value classes. Позволит делать классы-обертки вокруг примитивов, при этом компилировать без этого класса. Можно будет, к примеру, сделать два типа строк: login и email, которые не будут друг к другу каститься. Слышал об этом на Jpoint.
    2. 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

    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

    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

    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

    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

    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. Корутины — делим процессорное время

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 42

      0

      Насчёт работы с датами — можно было чуть большим количеством кода получить даже более красивые результаты


      data class IntervalInfo(amount: Long, type: ChronoUnit)
      operator fun Date.minus(interval: IntervalInfo):Date{…}
      operator fun Date.plus(interval: IntervalInfo):Date{…}
      val Int.months = IntervalInfo(this, ChronoUnit.MONTHS)
      val Int.days = IntervalInfo(this, ChronoUnit.DAYS)

      и использовать как-то вроде


      val before = date - 3.days
      val after = date + 2.months

      Ну а если вы не на 8й джаве пока — тогда можно свой аналогийчный энам изобрести.

        –1
        Мне не очень понравилась мысль перегружать примитивы. Это хорошо для DSL, но в продакшен коде будет приводить к проблемам, так как на days и months не остановишься, и скоро у простого Int будет api не меньше чем у list
          +2
          Ну они не сами же вам в код попадают. Где надо — там заимпортировали, где не надо — не импортируете. То есть в том месте где у вас будет работа с датами — там и будете красивый код писать.
            +2
            Да, только IDE будет подсказывать все. Проверил
              +2
              Мне кажется что если оно к месту — то почему бы не писать красиво там где можно писать красиво?
                +1
                Может, это чопорно, но по мне Int не должен знать о ChronoUnit, Date и прочем. Написав такой extension, мы расширяем api Int такими знаниями.
                Вариант ниже решает эту проблему: DAYS знает о Int, но не наоборот.
                  0

                  Красиво — это не пихать мульйон методов в один тип. Любой нубас может наговнокодить божественных и несолидных объектов, когда это красивым стало?

                    0
                    Ну не знаю. Должен ли Int знать про Long? А про BigInteger? В котлине знает. По мне — это просто удобные эксплицитные конвертеры.
              +1

              Но в общем можно и не использовать свой data-класс, а писать как-то вроде


              val before = date - Pair(3, DAYS)
                +1
                Тогда возвращаясь к локаничности, но без перегрузки примитивов. Я, навреное, на это перепишу свой код
                val before = date - DAYS*3
                
                  +1

                  "Математичнее" будет DAY*3 или 3*DAY

                  –1
                  before = date - 3.days

                  выглядит вполне в духе Ruby.

              0
              а чем отличаются
              val list1: List?
              и
              var list9: List?
                0
                Тем, что первый нельзя заменять после инициализации. В случае с локальной переменной var нужен не часто, а вот в случае поля — вполне.
                  +2

                  Возможно, rfq не заметил, что там разные ключевые слова

                    +2

                    Я тоже не сразу заметил это, за это можно поставить маленький минус языку.
                    При беглом осмотре кода отличить будет сложно.

                      0

                      Видимо, упор идёт на то, что IDEA заметно выделяет локальные переменные с var.

                +7

                Отвратительные ключевые слова — сливаются воедино, я тоже не сразу заметил, что первое слово в примере то разное! Очень неприятное решение от языка, который стремится уменьшить количество ошибок. Лучше бы какое-нибудь "var/let" заюзали

                  –1

                  Сорри, это было в предыдущую ветку сообщение

                    0
                    возможно, взяли пример со scala
                      0
                      Ага, в Скала это настолько же ужасно.
                    +1

                    А чем "невозможный на Java" пример с assertFail отличается от использования @Test(expected = ArrayIndexOutOfBoundsException.class)?

                      +1

                      Я бы спросил по-другому. Чем с точки зрения красоты отличаются вот эти две строчки:


                      Kotlin: assertFail<ArrayIndexOutOfBoundsException> { arrayOf(1, 2)[3] }
                      Java: assertFail(ArrayIndexOutOfBoundsException.class, () -> arrayOf(1, 2)[3])

                        0

                        наверное тем, что конструкция на Java будет жить только в JVM начиная с 8 версии, а на Kotlin и в JVM 7 и даже 6?

                          0
                          Согласен, не слишком удачный пример. В куске кода чуть выше (про репозиотрии) был более инетересный вариант (сейчас продублирую в статье). Это в случае, если класс писал не ты и T просто нет.
                          inline fun <reified T, ID: Serializable> CrudRepository<T, ID>.find(id: ID): T {
                              return findOne(id) ?: throw ObjectNotFound(id, T::class.qualifiedName)
                          }
                          
                            0

                            Я бы разделил немного по-другому. Важно не то, писал ли ты класс — а то, может ли параметр шаблона быть выведен из параметра функции. Если может — то Kotlin дает более красивый код чем Java. Если нет — то и разницы нет.


                            В примере с репозиторием параметр шаблона T выводится из скрытого параметра метода this (вроде бы).

                              –1
                              Это inline функция, она не существует в runtime, вместо этого она разворачивается при компиляции в месте использования. Поэтому, T доступно — в compile time она ещё не стерта
                                0

                                Причем тут это?

                                  0
                                  В примере с репозиторием параметр шаблона T выводится из скрытого параметра метода this (вроде бы).

                                  Решил объяснить как работает эта магия. Из this T в общем случае не выведешь, насколько я помню. Я когда-то пробовал, решил что легче каким-то образом передавать Class T. Источник
                                    0

                                    Если this имеет тип CrudRepository<T, ID> — то для компилятора нет проблем "достать" из него T.

                                      0
                                      В случае assertFail это не сработает, так как нет объекта, из которого можно вытащить T. А механизм один.
                                        +1

                                        Так я про это и говорю! Именно поэтому при вызове assertFail нужно явно писать тип в угловых скобках, а при вызове find — не нужно.


                                        А это, в свою очередь, приводит к тому, что assertFail на Kotlin выходит ничуть не красивее чем аналог на Java. Функция же find на Kotlin получается красивее чем на Java, потому что у нее "исчезает" один параметр.

                        –2

                        Groovy ни чем не хуже… Даже Веселей...

                          +2
                          Кроме отсутсвия статической типизации, что не делает его Safe, что для меня является основным приемуществом языка
                          0

                          Ожидал более практических результатов в этой статье.
                          Я, например, пробовал компиляцию в js — как proof-of-concept интересно, но тулинг и поддержка студии (основы "философии") — отвратительные, их практически нет.


                          Некоторые идеи, представленные здесь — далеко не новые, а уже давно "витают" в сообществе. В Kotlin'е просто использованы свежие идеи. Но от влияния Java он никуда не денется: на некоторые issue JetBrains так и говорит: мы сделали это так, потому что это так в Java.

                            –1
                            Практический результат — написанный проект мне нравиться, язык действительно помогает сделать код безопаснее, код хорошо читается. К сожалению, влияние в мелочах, поэтому ужать всё это в одну статью не получилось.
                          • UFO just landed and posted this here
                              –1
                              Я полгода пишу на Kotlin, и всё что смог припомнить:
                              1. Extension написанный на Kotlin нельзя вызвать из Java. Было бы странно, если бы можно было.
                              2. Если написать на Kotlin класс и отнаследоваться от него на Java, могут возникнуть некоторые сложности.
                              • UFO just landed and posted this here
                                  0
                                  У меня проект на Spring Boot, Hibernate и JSF, final решается на раз плагином
                                  С интерфесами вряд ли будут проблемы, я говорил про наследование классов.
                                  Насчет 100% — да, я не стал это упоминать в статье и не считаю это «нарушением интеропа», так как причиной этому то, что в Kotlin немного больший функционал (в основном, за счет компилятора), чем в Java, и, естественно, к нему нет прямого доступа из Java.
                                  а еще косяки с дженериками
                                  Это вы о чем?
                                    +1

                                    метод с inline + reified из java не вызвать. Интерфейс определенный в java это SAM, но, внезапно, интерфейс определенный в kotlin нет. В интерфейсах @JvmStatic/@JvmField не работает и тд. В какой-то степени это должно радовать, потому что в котлине есть фичи, которые невозможно повторить в джаве, но тогда нужно быть скромнее про 100% интероп.

                              0
                              Сам ещё не использовал, но, судя по всему, это может сильно изменить подход к многопоточному программированию.

                              Не совсем ясно причем тут многопоточное программирование…

                              Only users with full accounts can post comments. Log in, please.