Во время написания этой статьи произошло очень большое событие — 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. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее

Вариант 2. Открой один материал

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


Check Also

DDoS на Bluetooth. Разбираем трюк, который поможет отключить чужую колонку

На свете существует не так много вещей, которые бесят практически всех без исключения. Это…

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

  1. Аватар

    Nick

    29.06.2017 at 16:55

    хорошо рассказано, аж захотелось самому поиграться с этим

Оставить мнение