Во время написания этой статьи произошло очень большое событие — Kotlin был признан официальным языком программирования для платформы Android. Год назад мы уже писали про этот язык, но жизнь не стоит на месте, и в этой статье я расскажу тебе, какими фичами Kotlin смог покорить меня, старого и закаленного программиста. 🙂

Уже много лет как JVM — это не просто виртуальная машина, в байт-код которой компилируется язык программирования Java, а нечто куда большее. Сегодня JVM — это платформа, для которой существует множество популярных языков программирования, таких как Scala, Groovy и Clojure. Kotlin — еще один язык в этом ряду, и он обладает целым рядом преимуществ и особенностей. Сейчас это уже не только язык для JVM, есть варианты для JavaScript и даже Kotlin Native, что очень добавляет ему привлекательности.

Меня вряд ли можно назвать оголтелым патриотом, но мне очень приятно думать, что языком программирования, который создается в России, пользуются люди по всему миру.

Почему «создается», а не «создан»? Дело в том, что языки программирования эволюционируют, и это нормально (кроме случая C++ — ну же, ребята, хватит уже). Поэтому все, что здесь описано, верно здесь и сейчас (а дальше, уверен, будет только лучше).

 

Классы и объекты

Начнем с простого — с ООП. В статье я буду акцентировать внимание на необычных с точки зрения Java моментах.

В языке Kotlin первичный конструктор не может содержать кода и код должен быть вынесен в блок инициализации.

class Customer(name: String) {
    init {
        logger.info("Customer initialized with value ${name}")
    }
}

Однако дополнительные конструкторы мало чем отличаются от аналогов в Java:

class Person {
    constructor(parent: Person) {
        parent.children.add(this)
    }
}

Если же первичный конструктор содержит параметры, то другие конструкторы должны делегировать (явно или неявно через другой, вторичный конструктор) создание объекта первичному конструктору:

class Person(val name: String) {
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

Здесь еще есть неизменяемое поле name, теперь оно часть класса. Конечно, можно сделать конструктор непубличным, например так:

class DontCreateMe private constructor () {
}

Наверное, самая интересная особенность — это отсутствие ключевого слова new в Kotlin. Чтобы создать объект, нужно просто вызвать соответствующий конструктор:

val invoice = Invoice()

val customer = Customer("Joe Smith")

В Kotlin аналог ObjectAny, базовый класс для всех классов, хотя это аналог только в смысле корня иерархии, он не имеет equals(), hashCode() и toString().

Все классы в Kotlin по умолчанию final, от них нельзя наследовать, и, чтобы сделать класс открытым для наследования, нужно объявить его как open:

open class Base(p: Int)

class Derived(p: Int) : Base(p)

Это же касается и методов, которые можно переопределять, они также должны быть помечены как open:

open class Base {
    open fun canOverride() {}
    fun cannotOverride() {}
}

class Derived() : Base() {
    override fun canOverride() {}
}

С этим связано несколько правил, в том числе следующее: только открытый класс может иметь открытые методы. Более того, методы, помеченные override, являются также open-методами. Для того чтобы закрыть override-метод, нужно использовать final:

open class AnotherDerived() : Base() {
    final override fun v() {}
}

Kotlin лаконичен и последователен, override может быть частью первичного конструктора:

interface Foo {
    val count: Int
}

class Bar1(override val count: Int) : Foo

class Bar2 : Foo {
    override var count: Int = 0
}

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

В Kotlin нет статических методов, и создатели языка рекомендуют пользоваться функциями на уровне пакета (то есть не привязанными к конкретному классу). Если мы все-таки хотим иметь что-то похожее на статические методы в Java, то есть ненаследуемые, но как-то привязанные к классу, то типичный пример здесь — это фабричный метод (factory method), когда мы можем воспользоваться возможностью создания объекта-компаньона. Для тех, кто программирует на Scala, это все очень знакомо:

interface Factory<T> {
    fun create(): T
}

class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}

val instance = MyClass.create()

Если же мы хотим получить сам объект-компаньон, это можно сделать так:

class MyClass {
    companion object {
    }
}

val x = MyClass.Companion

Более того, существует еще так называемое объектное выражение, но эту тему мы оставим до следующей статьи, а желающие могут найти все, что необходимо, в первоисточнике — KotlinLang.org.

 

Свойства

Свойства (properties) — весьма полезный инструмент программирования, и надо сказать, что в Kotlin этот вопрос решен очень интересно:

val isEmpty: Boolean
    get() = this.size == 0

Это довольно простой пример, но вполне актуальный. Здесь isEmpty — это вычислимое свойство (предполагается, что у класса, внутри которого находится функция, есть свойство size). Также для свойства можно задать и сеттер через set() = ..., и устанавливать значение можно будет через присваивание имя_свойства=значение.

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        // Здесь мы можем сделать что-нибудь с value
    }

С одной стороны, это менее прозрачно, чем использование явных get/set-методов в стиле Java, однако это здорово разгружает код.

Более того, начиная с версии Kotlin 1.1 можно не прописывать тип свойства явно, если он может быть выведен автоматически:

val isEmpty get() = this.size == 0 // Имеет тип Boolean

Если нам нужно поменять только видимость или аннотировать get или set, то это довольно легко сделать с помощью указания области видимости или аннотации перед get или set:

var setterVisibility: String = "abc"
    private set // The setter is private and has the default implementation

var setterWithAnnotation: Any? = null
    @Inject set // Annotate the setter with Inject

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

 

Интерполяция строк

Это просто, понятно и очень приятно:

val x = 10
val y = 20

println("x=$x y=$y")
 

Значения и вывод типов

Вместо модификатора final в языке Kotlin явным образом различаются константы и переменные через различные ключевые слова. Для объявления констант (значений) используется ключевое слово val, а для переменных — ключевое слово var. И снова это должно быть хорошо знакомо тем, кто программирует на Scala. Вообще, определение в Kotlin синтаксически решено иначе, чем в Java:

val x: Int = 10

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

val result = sequenceOf(10).map { it.toString() }.flatMap { it.toCharArray().asSequence() }.toList()

В данном примере компилятор самостоятельно выведет тип как List<Char>. Несмотря на все радости вывода типов, все-таки в сложных случаях я рекомендую прописывать тип явно, просто для того, чтобы получить ошибку компиляции как можно ближе к ее реальному источнику. Потому что, например, если в условном выражении типы для разных ветвей различаются, то компилятор унифицирует тип до какого-то общего, в худшем случае до Any. В результате ошибка компиляции будет там, где ты передаешь соответствующее значение, а не там, где оно было описано. Возможность быстрой локализации ошибок очень важна, особенно в больших системах, что приводит нас к одной из самых интересных фишек языка Kotlin.

 

Null Safety

Вот правда, когда кто-то говорит про Kotlin, то сразу думает про Null Safety, а тот, кто знает, что такое Null Safety, вспоминает про Kotlin. Для начала разберемся, что это вообще такое. Те, кто программирует на Java, очень не любят получать NullPointerException (NPE) где-нибудь на сервере, просто потому, что stack trace часто недостаточно информативен для выявления ошибки. Это в первую очередь связано с императивной природой языка Java: место, где определена (или объявлена, но не определена) переменная, и то место, где она используется, могут находиться очень далеко друг от друга как в пространственном (в коде), так и во временном (по времени выполнения) отношении.

В языке Kotlin этот вопрос решен довольно интересно, так как ты на уровне языка контролируешь, можно ли присваивать null-значение соответствующей переменной. Причем по умолчанию это запрещено:

fun f(s: String): Int {
    return s.indexOf('=')
}
// f(null)       // Ошибка!
f("Hello World") // Все хорошо

Если же мы хотим, чтобы функция (переменная) принимала нулевые значения, то надо бы написать так:

fun f(s: String?): Int {
    return s?.indexOf('=') ?: -1
}

Выражение в return — это специальный вариант оператора «элвис» (elvis — он так называется, потому что ?: похоже на emoji в стиле Элвиса Пресли), который представляет собой не что иное, как синтаксический сахар для конструкции

if (s != null) s.indexOf('=') else -1

Последнее условное выражение — это пример так называемого smart cast в Kotlin, и если в условии была проверка на null, то вызов s.indexOf() считается абсолютно легитимным. Этот же механизм работает, если мы будем передавать аргументы, которые могут быть null, в качестве ненулевого формального параметра в функции.

fun g(cannotBeNull: String) {
   println(cannotBeNull)  
}

fun f(canBeNull: String?) {
    //  g(canBeNull) ошибка!
    if (canBeNull != null) {
        // Теперь все абсолютно корректно
        g(canBeNull)    
    }
}

А что, если мы в каком-то месте точно знаем, что переменная будет проинициализирована, но вначале она равна null (к этому вопросу в более общем случае мы вернемся чуть позднее)?

var canBeNull: String? = null
// ...
canBeNull = "Hello"
// ...
val cannotBeNull: String = canBeNull!!

Здесь мы явным образом берем на себя ответственность за небезопасное преобразование. Если все-таки переменная canBeNull не будет определена, как предполагается, то мы получим KotlinNullPointerException. И в чем, собственно, плюсы такого подхода по сравнению с обычной Java? Такой подход позволяет локализовать ошибку. Даже если мы будем писать canBeNull!!.length, то сам синтаксис помогает нам понять, где именно проблема. Строго говоря, возможна сложная цепочка, a!!.b.c!!.d к примеру, и в этом случае также понятно, где источник потенциальных проблем. Правильное использование такого подхода позволяет изолировать крупные участки кода от потенциального NPE! Можно использовать конструкцию canBeNull?.length, но в отличие от предыдущего случая тип этого выражения будет Int?, а не Int. Это означает, что если canBeNull равен null, то и само значение будет равно null. Таким образом, a?.b?.c?.d будет null, если любое из значений в цепочке равно null. Тем, кто знаком с функциональным программированием, это напоминает аппликативные функторы и монаду Maybe в Haskell.

Еще одна интересная особенность Kotlin в работе с null — это использование let. Если мы хотим, чтобы для ненулевых значений была выполнена какая-то операция, то можно сделать так:

val listWithNulls: List<String?> = listOf("A", null)
for (item in listWithNulls) {
    item?.let { println(it) } // Печатаем A и игнорируем null
}

Для приведения типов в Kotlin используется оператор as, однако в случае преобразования из типа, который не допускает null, можно получить kotlin.TypeCastException: null cannot be cast to non-null type kotlin.Int (если мы преобразуем в Int). Есть и более «умный» вариант:

Продолжение статьи доступно только подписчикам

Вариант 1. Оформи подписку на «Хакер», чтобы читать все статьи на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта, включая эту статью. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

Вариант 2. Купи одну статью

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


2 комментария

Подпишитесь на ][, чтобы участвовать в обсуждении

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

Check Also

Windows 10 против шифровальщиков. Как устроена защита в обновленной Windows 10

Этой осенью Windows 10 обновилась до версии 1709 с кодовым названием Fall Creators Update …