Привет, Хабр! Надеемся в обозримом будущем и до Kotlin добраться. Мимо этой статьи (февральская) пройти не смогли.

Читаем и комментируем!

Я только что дочитал книгу Брюса Тейта "Семь языков за семь недель" — и с тех пор ее обожаю! Хотя, у меня и есть опыт работы с большинством языков, описанных в книге, мне очень понравилось, как именно автор характеризует языки, и как эти характеристики в итоге отражаются на практическом использовании языка.

Поэтому я и решил написать для этой книги еще одну главу, дополнительную. Она будет посвящена языку, который я знаю достаточно хорошо, понимаю его достоинства и недостатки: это Kotlin.

Цель этой статьи – не научить вас языку Kotlin, а показать его характер. Не пытайтесь понять ее досконально. Лучше уделите внимание тому, как описанные здесь возможности могли бы повлиять на ваш стиль программирования.

Типичнейшая черта Kotlin заключается в том, что этот язык, в сущности, не вводит ничего такого, что бы еще не встречалось в других языках программирования. Дело в том, что Kotlin использует все эти старые находки самым шедевральным образом. Вспомните супергероя Железного Человека. Тони Старк собрал Железного Человека из простейших электронных комплектующих. У Железного человека нет таких характерных сверхвозможностей, как у Супермена или Флэша. Это может показаться слабостью, однако, в долгосрочной перспективе это огромное преимущество. Поговорим об этом ниже, а пока начнем с азов.



Основы


Считается, что Kotlin предназначен для программирования в той или иной IDE – например, в IDEA IntelliJ, Android Studio или CLion (Kotlin/Native). Здесь мы начнем с командной строки, чтобы продемонстрировать Kotlin в более простом контексте. После того, как установите Kotlin, запустите REPL (интерактивную оболочку) вот так:



Попробуем с числами:

>>> 1
1
>>> 1 + 2
3
>>> 1.0 + 2
3.0

Простая математика работает. Это Kotlin/JVM, работающий на виртуальной машине Java. Целые числа в Java – это примитивы. А в Kotlin? Давайте проверим тип числа:

>>> 1::class
class kotlin.Int
>>> 1.0::class
class kotlin.Double

Оба – объекты! Вообще, в Kotlin все сущности – это объекты. Что насчет Java? Kotlin полностью интероперабелен с Java, но мы видим, что вышеприведенные типы – это типы Kotlin. Дело в том, что некоторые встроенные типы Kotlin покрывают типы Java. Можно посмотреть, каков в данном случае будет тип Java, воспользовавшись свойством java от Class или свойством javaClass любого объекта:

>>> 1.0::class.java
double
>>> 1.0.javaClass
double

Это double! Числа двойной точности (double) в Java — примитивы. Как это возможно? Kotlin задействует оптимизацию, позволяющую применять примитивы вместо объектов, когда никакие объектно-ориентированные возможности не используются. Это происходит под капотом, совершенно не касается разработчика. Если нам требуется использовать double как объект, то вместо него пойдет в ход Double. С точки зрения разработчика по-прежнему можно сказать, что «все является объектом». Определим несколько свойств:

>>> val a = 1

Это свойство только для чтения. Также можно определить свойство для чтения и для записи при помощи var:

>>> var a = 1

Обратите внимание: тип здесь не указан. Однако, пусть это не вводит вас в заблуждение: Kotlin – язык с сильной статической типизацией. Тип свойства просто выведен из типа присвоенного значения:

>>> ::a.returnType
kotlin.Int

Довольно математики, перейдем к более интересным возможностям.

Безопасность


Железного человека сконструировали потому, что ни полиция, ни армия не могли вызволить Тони Старка из плена террористов. Тони сделал Железного Человека, чтобы позаботиться о собственной безопасности, а также расширить свои возможности. Еще он этим прославился.

Точно так же JetBrains создали Kotlin. Эта же компания создает самые популярные интегрированные среды разработки. Все их инструменты изначально создавались на Java, но команда JetBrains на практике ощутила все недостатки этого языка. Тогда компания стала экспериментировать с другими языками, например, Scala или Groovy, но и они их не удовлетворили. Поэтому, в конце концов JetBrains решили создать собственный язык, с прицелом на то, что этот новый язык должен обеспечивать максимальную безопасность (чтобы в продуктах не возникало ошибок) и масштабируемость. Кроме того, Kotlin отлично пропиарил JetBrains. Они и так были известны во всем мире, а когда появилось сообщество специалистов, решивших пользоваться их обалденным языком, JetBrains стала для них еще круче. (Здесь я лишь в самых общих чертах пересказал эту историю. Если интересуют детали – послушайте этот подкаст).

Kotlin значительно выигрывает у Java по части безопасности. Свойства необходимо инициализировать:

>>> var a: String
error: property must be initialized or be abstract

По умолчанию типы не обнуляемы:

>>> var a: String = null
error: null can not be a value of a non-null type String

Если мы хотим показать, что данный тип обнуляем, это делается при помощи ?:

>>> var a: String? = null

Правда, обнуляемый тип нельзя явно использовать:

>>> a.length
error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

Обнуляемые типы похожи на другие, например, на опциональные типы, используемые в Scala. Перед использованием такой тип нужно распаковать. Можно воспользоваться безопасным вызовом, действующим точно как нормальный вызов, если свойство не равно null. Если свойство равно null, то мы не вызываем метод, а просто возвращаем null:

>>> a = null
>>> a?.length
null
>>> a = "AAA"
>>> a?.length
3

Также можем использовать небезопасный вызов, который выбросит исключение, если свойство будет равно нулю;

>>> a = null
>>> a!!.length
kotlin.KotlinNullPointerException

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

Интеллектуальность


Железный человек так крут, поскольку его боевой костюм по-настоящему интеллектуален. Он просчитывает ситуацию и предупреждает Тони об опасностях. Kotlin также по-настоящему интеллектуален, и тем самым очень помогает разработчикам.

Один из образцов такой интеллектуальности – умное приведение типов. Когда мы проверяем, не равно ли свойство нулю, мы можем использовать его так, словно оно действительно ненулевое. Чтобы продемонстрировать некоторые более продвинутые возможности, мы будем работать с файлами. Предлагаю воспользоваться IDEA IntelliJ (здесь рассказано, как приступить к работе с ней). В качестве альтернативы можете попробовать все эти возможности в онлайновой REPL. Рассмотрим пример:

fun smartCastingExample(str: String?) {
    if(str != null)
        print("Length is " + str.length)
}

Как видите, str используется явно (без небезопасных или безопасных вызовов). Вот почему в области проверки if(str != null) тип приводится от String? к String. Также все сработает, если мы выйдем из функции при противоположной проверке:

fun smartCastingExample(str: String?) {
    if(str == null)
        return
    
    print("Length is " + str.length)
}

Принцип работает не только с обнуляемостью. Также возможно умное приведение к типу:

fun smartCastingExample(any: Any?) {
    if(any is String)
        print("String with length " + any.length)
}

Kotlin отлично поддерживается в IDEA IntelliJ, Android Studio или CLion. В этих IDE вы получите массу советов, подсказок и поддержки. Вот пример, где императивная обработка коллекции, типичная для Java, заменяется на декларативную, свойственную Kotlin. Обратите внимание, что именно среда разработки предлагает и выполняет весь переход:



Минимализм


Тони Старк не облачается в Железного человека целиком, если ему это не требуется. Обычно он пользуется только машиной или некоторыми небольшими компонентами.



Философия Kotlin, в частности, постулирует, что простое должно оставаться простым. Вот код Hello World на Kotlin:

fun main(args: Array<String>) {
    print("Hello, World")
}

Это всего лишь одна функция, выводящая текст. Другие типичные операции на Kotlin также просты. Когда все тело функции нам не требуется, можно обойтись единственным выражением:

fun add(a: Int, b: Int) = a + b

Ниже вы еще не раз увидите точно такой же минималистичный стиль.

Гибкость


Железный Человек проигрывает Супермену по некоторым важным показателям. Например, лазерные глаза даны Супермену от рождения, он может испепелять врага взглядом, когда ему вздумается. Тони Старк не оснащал Железного Человека глазными лазерами – возможно, потому, что не видел в этом острой необходимости. Здесь важно отметить, что он легко мог бы добавить Железному Человеку такую фичу. На самом деле, это под силу и любому пользователю Железного Человека. Но, при этом, на Железного человека можно навешивать и иные апгрейды, которые вполне могут быть не менее эффективны, зато обойдутся дешевле. В этом – огромный потенциал гибкости. Перейдем к практике. В большинстве языков программирования в той или иной форме предоставляются литералы коллекций. В Python, Ruby или Haskell список можно определить вот так: [1,2,3]. В Kotlin таких литералов коллекций нет, зато он предусматривает функции верхнего уровня (те, что могут использоваться везде), а в стандартной библиотеке Kotlin имеются такие функции верхнего уровня, при помощи которых можно создавать коллекции:

>>> listOf(1,2,3)
[1, 2, 3]
>>> setOf(1,2,3)
[1, 2, 3]
>>> mapOf(1 to "A", 2 to "B", 3 to "C")
{1=A, 2=B, 3=C}

Почему это так важно? Когда в языке предоставляется литерал коллекций, он же и определяет, как пользователь будет применять коллекции. У всех коллекций имеются некоторые характеристики. Идет обширная дискуссия о том, какие списки лучше – изменяемые или неизменяемые? Изменяемые эффективнее, но неизменяемые гораздо потокобезопаснее. По этому поводу есть множество мнений и доводов. С учетом этого, хотели бы вы, чтобы литерал списка порождал именно изменяемый, либо именно неизменяемый список? В любом случае, вы будете так или иначе навязывать программисту варианты использования языка, поскольку программист предпочтет воспользоваться литералом коллекций. Kotlin в данном случае оставляет свободу выбора. listOf, setOf и mapOf дают неизменяемые коллекции:

>>> var list = listOf(1,2,3)
>>> list.add(4)
error: unresolved reference: add
list.add(4)
     ^
>>> list + 4
[1, 2, 3, 4]

Хотя, не составляет труда создать и изменяемую коллекцию при помощи mutableListOf, mutableSetOf и mutableMapOf:

>>> mutableListOf(1,2,3)
[1, 2, 3]
>>> mutableSetOf(1,2,3)
[1, 2, 3]
>>> mutableMapOf(1 to "A", 2 to "B", 3 to "C")
{1=A, 2=B, 3=C}

Обратите внимание: кто угодно может определить собственную коллекцию, а затем – функцию верхнего уровня, которая будет ее создавать:

fun <T> specialListOf(vararg a: T): SpecialList<T> {
    // Код
}

Выше легко заметить, что я воспользовался обобщенным параметром типа T. Не переживайте. Это означает, что необходимо передать набор однотипных элементов, чтобы создать коллекцию данного типа.

Благодаря тому, что в Kotlin используются базовые возможности, а не встроенные литералы, сторонние библиотеки при работе с Kotlin не уступают по силе его стандартной библиотеке. Еще одна великолепная фича, значительно демократизирующая библиотеки и развязывающая руки разработчикам – это так называемая функция-расширение (extension function). Суть в следующем: можно определить такую функцию, чтобы она действовала как метод:

>>> fun Int.double() = this * 2
>>> 2.double()
4

Обратите внимание: на практике при этом никаких методов к классу не добавляется. Функции-расширения – это просто функции, которые вызываются вот таким особым образом. Данная возможность может показаться простой, но она реально мощная. Например, в Kotlin, как и в других современных языках, предоставляются функции для обработки коллекций:

class Person(val name: String, val surname: String)

val avengers = listOf(
        Person("Tony", "Stark"),
        Person("Steve", "Rogers"),
        Person("Bruce", "Banner"),
        Person("Thor", "")
)
val list = avengers
        .filter { it.surname.isNotBlank() }
        .sortedWith(compareBy({ it.surname }, { it.name }))
        .joinToString { "${it.name} ${it.surname}" }
print(list) // Выводит: Брюс Баннер, Стив Роджерс, Тони Старк

Огромное преимущество Kotlin заключается в том, что такие и подобные функции определяются как функции-расширения. Например, взгляните на реализацию filter:

inline fun <T> Iterable<T>.filter(
    predicate: (T) -> Boolean
): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}
inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(
    destination: C, predicate: (T) -> Boolean
): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}

Если ее не стоит реализовывать, то можете определить эту функцию сами. Если вам нужна какая-либо иная функция для обработки коллекций, то можете без труда ее определить. Зачастую разработчики так и делают. Например, можно найти массу библиотек, в которых определяются функции-расширения для Android. Они упрощают разработку под Android. Еще один результат такого решения – к вашим услугам есть просто уйма методов для обработки коллекций.

Здесь также немаловажно отметить, что filter – это функция, расширяющая не тип List, а интерфейс Iterable:

public interface Iterable<out T> {
    public operator fun iterator(): Iterator<T>
}

Можете без труда определить свой класс коллекций, реализующий Iterable, и язык за вас добавит к нему эти методы для обработки коллекций. Даже String реализует его. Вот почему можно задействовать все методы обработки коллекций и со String:

>>> "I like cake!".map { it.toLowerCase() }.filter { it in 'a'..'z' }.joinToString(separator = "")
ilikecake



Краткие итоги


Тони Старк не родился супергероем, и никакой радиоактивный паук его не кусал. Он сконструировал Железного Человека благодаря своим невероятным знаниям и опыту. Аналогично, JetBrains – это компания, создающая великолепные IDE для разных языков; ее люди многому научились за этой работой и воплотили свои знания, написав невероятный язык программирования. Они не привнесли в мир программирования ничего нового, а просто предложили идеально сработанный язык, в котором используются плюсы множества других языков программирования – поэтому на нем и достигается максимальная продуктивность разработчика и высочайшее качество проектов.

Отличный пример такого умного проектирования – кортежи Kotlin. Когда язык еще существовал в бета-версии, в нем уже поддерживались кортежи. Хотя, когда команда Kotlin проанализировала, как именно работа с кортежами сказывается на разработке программ, выяснилось, что их влияние не всегда положительно. Вот почему кортежи в релиз не попали. Вместо них в Kotlin можно применять аннотацию data. Она универсальнее, и разработчики Kotlin не сомневались, что в целом она положительно повлияет на работу с языком. Кортежи по-прежнему обсуждаются и, возможно, когда-нибудь в Kotlin появится нативная поддержка для них. Однако, чтобы это произошло, команда Kotlin и сообщество разработчиков должны убедиться в целесообразности такого решения.

Это красноречиво характеризует Kotlin. Это не сборная солянка из идей, а по-настоящему грамотно сделанный язык. В нем собран минимальный набор самых мощных фич, которые в сумме дают вам потрясающие возможности. В целом, Kotlin оставляет вам гораздо больше свободы – в том числе, для совместного творчества.

Домашнее чтение


Если вы хотите подробнее разобраться с Kotlin, рекомендую почитать документацию языка, а также познакомиться с ресурсом Kotlin Koans. Если вас интересует Kotlin для Android, посмотрите эту книгу.

Комментарии (11)


  1. smer44
    21.04.2018 05:14

    for (element in this) if (predicate(element)) destination.add(element)
    return destination

    когда создаётся цепь фильтров, в каждом звене конструируется новый массив и потом даётся дальше?


    1. rjhdby
      21.04.2018 13:43

      Да, сейчас это реализовано именно таким образом, без потоков. Насколько я понимаю, так было сделано для обратной совместимости с Java6 (Android же)


    1. lany
      21.04.2018 15:39

      Есть вариант с asSequence, если вас это не устраивает. Впрочем, он особо не отличается от Java Stream API.


      1. smer44
        21.04.2018 17:14

        минимизация пайплайна (из функций map, foreach, filter, и т.п.) и применение его последовательно или чуть паралельно к каждому значению без нескольких проходов, то есть нечто в направлении генераторов можно сделать в этом котлине?


        1. kalininmr
          21.04.2018 22:57

          так в котлине генераторы есть же.
          ну или почти.
          через сопрограммы и в таком духе


        1. rjhdby
          21.04.2018 23:52

          Там есть корутины, с помощью которых, в том числе, реализованы генераторы


        1. lany
          22.04.2018 06:33

          Конкретно это и в джаве работает. Корутины для этого не нужны.


    1. mitgard
      23.04.2018 09:09

      Не отвечу за котлин на 100%, но почти уверен что да.


  1. Optik
    21.04.2018 11:12
    +1

    предложили идеально сработанный язык

    По доллару бы за каждый идеал. По два за идеал от рождения.

    Это не сборная солянка из идей, а по-настоящему грамотно сделанный язык. В нем собран минимальный набор самых мощных фич, которые в сумме дают вам потрясающие возможности.

    Солянка и есть набор из разных мяс.

    В общем опять оценка преисполненная поверхностных суждений с базой на эмоциональном восприятии.
    Не бывает идеальных языков. Всегда очень много нюансов, когда приходится жертвовать одним против другого. В одних ситуациях это плюс, в других минус. [mode=troll] Даже разработчики go отказались от слова «идеальный» и стали слушать (ну как смогли) фидбэк пользователей. [/mode]


    1. rjhdby
      21.04.2018 13:33
      +1

      Стоит заметить, что определение «идеальный» — полностью на совести автора статьи. Сами ребята из JetBrains его так не называют(я, по крайней мере не помню такого) и работа с фидбеком у них поставлена достаточно хорошо ;)


  1. rjhdby
    21.04.2018 13:48
    +1

    Можете без труда определить свой класс коллекций, реализующий Iterable, и язык за вас добавит к нему эти методы для обработки коллекций. Даже String реализует его. Вот почему можно задействовать все методы обработки коллекций и со String:

    А вот это совсем не правда. String в Kotlin не реализует интерфейс Iterable и функции расширения, такие как map, filter и т.д. для него совершенно отдельные, привязанные либо к самому String, либо к суперклассу CharSequence (который тоже не реализует Iterable кстати)