Статьи

Kotlin для разработчиков Java: 10 функций, которые вам понравятся в Kotlin

Kotlin — это язык JVM со статической типизацией, созданный компанией Jetbrains, создателем IntelliJ IDE. Kotlin построен на Java и предоставляет полезные функции, такие как нулевая безопасность, классы данных, расширения, функциональные концепции, интеллектуальные преобразования, перегрузка операторов и многое другое.

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

Зачем мне заботиться о Котлине?

Что особенно заинтересовало меня в Kotlin, так это тот факт, что он полностью совместим с Java и поддерживается Jetbrains и их популярной Java IDE IntelliJ. Вы спросили, почему меня это так заинтересовало?

Что ж, функциональная совместимость с Java очень важна, потому что уже довольно давно Java является одним из наиболее широко используемых языков программирования . С точки зрения бизнеса это означает, что большая часть реального кода написана на Java и что компании хотят поддерживать свою базу кода Java как можно дольше — обычно ценность унаследованного кода исчисляется миллионами.

С Kotlin у организаций есть возможность опробовать новый язык программирования с минимальным риском. Файлы Java могут быть преобразованы в эквивалентные файлы Kotlin, с которыми затем можно работать. Точно так же все типы, определенные в Kotlin (например, классы и перечисления), могут использоваться из Java, как любой другой тип Java. С точки зрения разработчика, здорово иметь возможность использовать библиотеки Java, к которым вы привыкли. Вы можете использовать Java IO, JavaFX, Apache Commons, Guava и все свои собственные классы прямо из Kotlin.

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

Эти два пункта также отделяют Kotlin от других языков JVM, таких как Scala, Ceylon, Clojure или Groovy.

Хорошо, хватит разговоров. Давайте прыгнем в реальные языковые особенности Kotlin.

Нулевая безопасность

1
2
3
4
5
class Person {
    val givenName: String = ""
    val familyName: String = ""
    val address: Address? = null
}

В этом примере GivenName и familyName не могут быть нулевыми — программа завершится с ошибкой во время компиляции . Вы должны явно сделать переменную nullable, чтобы иметь возможность присваивать ей значение null. Это делается через «?» после типа переменной. Таким образом, свойство адреса может быть нулевым в данном коде.

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

1
2
val givenName: String? = null
val len = givenName.length

Если вы попытаетесь скомпилировать это, компилятор Kotlin выдаст вам ошибку: «Только допустимые (?.) Или ненулевые утвержденные (!!.) Вызовы разрешены для обнуляемого получателя типа kotlin.String?». Мы увидим, как обрабатывать эти случаи, когда вы знаете, что переменная не может быть нулевой в секунду.

Пока все хорошо, но о каких безопасных и ненулевых вызовах, о которых говорит компилятор? Безопасные вызовы просто возвращают значение вызова как обычно, если вызываемый объект не равен нулю, и возвращают ноль в противном случае:

1
2
val givenName: String? = ""
val len = givenName?.length

В этом случае len будет нулевым, как и ожидалось. Если бы данное имя было нулевым, лену также было бы присвоено нулевое значение. Таким образом, возвращаемый тип данных NameName? .Length — Int ?, обнуляемое целое число.

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

1
2
val givenName: String? = "Roger"
val len = givenName!!.length

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

1
2
val text: String? = null
val len = text?.length ?: -1

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

1
2
val len = text?.length ?: -1
val len = text?.length ? text?.length : -1

Эти две строки семантически одинаковы.

Классы данных

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
class Book {
    private String title;
    private Author author;
 
    public String getTitle() {
        return title;
    }
     
    public void setTitle(String title) {
        this.title = title;
    }
 
    public Author getAuthor() {
        return author;
    }
     
    public void setAuthor(Author author) {
        this.author = author;
    }
}

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

1
data class Book(var title: String, var author: Author)

Kotlin также сгенерирует полезные реализации hashCode (), equals () и toString () . Печать книги приведет к выводу, подобному книге (название = эффективная Java, автор = автор (имя = Джошуа Блох)).

Задача : написать класс Author , который приведет к такому выводу!

Мало того, это также позволит вам легко делать копии классов данных:

1
2
3
4
val book = Book("Effective Java", Author("Joshua Bloch"))
val copy = book.copy()
val puzzlers = book.copy(title = "Java Puzzlers")
val gof = book.copy(title = "Design Patterns", author = Author("Gang of Four"))

Вы можете изменить произвольные свойства копируемого объекта, добавив именованные параметры в метод copy ().

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

1
2
val book = Book("The Phoenix Project", Author("Kevin Behr"))
val (title, author) = book

3) Функции расширения

Kotlin позволяет нам расширять функциональность существующих классов, не наследуя их . Это включается функциями расширения и свойствами расширения. Допустим, вы хотите расширить класс GridPane из среды JavaFX GUI с помощью метода для извлечения элементов в строке i и столбце j:

1
2
3
4
5
6
7
8
9
fun GridPane.getElementAt(rowIndex: Int, columnIndex: Int): Node? {
    this.children.forEach {
        if (GridPane.getColumnIndex(it) == columnIndex && GridPane.getRowIndex(it) == rowIndex) {
            return it;
        }
    }
 
    return null;
}

Здесь нужно упомянуть несколько вещей. Во-первых, внутри функции расширения вы можете обратиться к объекту, для которого она была вызвана, используя «this». Во-вторых, вы используете функцию Котлина forEach () в списке дочерних узлов. Это эквивалентно методу forEach (), включенному в Java 8, и допускает некоторое программирование в функциональном стиле. В-третьих, внутри forEach () вы можете ссылаться на текущий элемент, используя неявную переменную цикла «it».

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

Есть одна вещь, о которой нужно знать: если вы попытаетесь вызвать функцию расширения с аргументами, которые также применимы для существующей функции-члена внутри класса, этот член всегда будет «выигрывать» — это означает, что он будет иметь приоритет и будет затенять ваше расширение функция.

4) Smart Casts

Как часто вы уже разыгрывали объекты там, где это было фактически избыточно? Ставлю чаще, чем вы можете посчитать, вот так:

1
2
3
if (node instanceof Leaf) {
    return ((Leaf) node).symbol;
}

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

Эквивалентный код Kotlin для приведенного выше фрагмента кода выглядит следующим образом:

1
2
3
if (node is Leaf) {
    return node.symbol;
}

Оператор instanceof в Kotlin называется «is». И, что более важно, нет необходимости загромождать ваш код приведениями, о которых может позаботиться компилятор.

Теперь это идет намного дальше, чем просто этот простой случай:

1
2
3
4
if (person !is Student)
    return
 
person.immatriculationNumber

В этом случае, если человек не был студентом, поток управления никогда не достиг бы строки 4. Соответственно, компилятор Kotlin знает, что этот человек должен быть объектом Student, и выполняет интеллектуальное приведение.

Давайте посмотрим на некоторые лениво оцененные условные выражения :

1
2
3
if (document is Payable && document.pay()) { // Smart cast
println("Payable document ${document.title} was payed for.")
}

Подобные условия используют ленивые вычисления в Kotlin, как и в Java. Таким образом, если бы документ не был подлежащим оплате, вторая часть не была бы оценена в первую очередь. Следовательно, при оценке Котлин знает, что документ является подлежащим оплате, и использует интеллектуальный состав.

То же самое касается дизъюнкции:

1
2
3
if (document !is Payable || document.pay() == false) {  // Smart cast
    println("Cannot pay document ${document.title}.")
}

Когда выражения являются другим местом, где Kotlin будет применять умные приведения, где это возможно:

1
2
3
4
5
6
val result = when (expr) {
    is Expr.Number      -> expr.value
    is Expr.Sum         -> expr.first + expr.second
    is Expr.Difference  -> expr.first - expr.second
    is Expr.Exp         -> Math.pow(expr.base, expr.exponent)
}

В зависимости от типа объекта, вы можете просто использовать соответствующие свойства в каждом блоке when.

Примечание. В приведенном выше примере класс Expr должен быть запечатанным классом, содержащим только эти четыре подкласса.

5) Вывод типа

В Kotlin вам не нужно явно указывать тип каждой переменной, даже если Kotlin строго типизирован . Вы можете явно определить тип данных, например, если вы не хотите, чтобы маленькое целое число хранилось в переменных Int, а было бы Short или даже Byte. Вы можете сделать это, используя двоеточие, где тип данных стоит за именем переменной:

1
2
3
val list: Iterable = arrayListOf(1.0, 0.0, 3.1415, 2.718// Only need Iterable interface
 
val arrayList = arrayListOf("Kotlin", "Scala", "Groovy"// Type is ArrayList

Вы можете использовать явные типы, как в Java, но вы также можете писать более краткие описания переменных, похожих на Python. Явные типы полезны для ссылки на самый общий интерфейс (что вы всегда должны делать).

6) Функциональное программирование

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

Сочетание лямбда-выражений и библиотеки Kotlin действительно облегчает ваш день при работе с коллекциями:

1
2
3
val numbers = arrayListOf(-42, 17, 13, -9, 12)
val nonNegative = numbers.filter { it >= 0 }
println(nonNegative)

Обратите внимание, что при использовании лямбда-выражений с одним аргументом Kotlin создает неявную переменную с именем «it», которая ссылается на единственный аргумент лямбда-выражения. Таким образом, вторая строка выше эквивалентна:

1
val nonNegative = numbers.filter { it -> it >= 0 }

Kotlin предоставляет все основные функциональные возможности, такие как фильтр, карта и flatMap, взятие и падение, первое и последнее, сложение и сворачивание, forEach, уменьшение и все остальное, чего жаждет прагматичный функциональный программист:

01
02
03
04
05
06
07
08
09
10
11
12
13
println(numbers.take(2))  // First two elements: [-42, 17]
 
println(numbers.drop(2))  // List without first two elements: [13, -9, 12]
 
println(numbers.foldRight(0, { a, b -> a + b }))  // Sum of all elements: -9
 
numbers.forEach { print("${it * 2} ") }  // -84 34 26 -18 24
 
---
 
val genres = listOf("Action", "Comedy", "Thriller")
val myKindOfMovies: Iterable<String> = genres.filter { it.length <= 6 }.map { it + " Movie" }
println(myKindOfMovies)  // [Action Movie, Comedy Movie]

7) Объекты (иначе легко создавать синглтоны)

У Kotlin есть ключевое слово object, которое позволяет нам определять объект, похожий на класс. Но, конечно, тогда этот объект существует только как один экземпляр . Это полезный способ создания синглетонов, но эта функция не ограничивается только синглетонами.

Создать объект так же просто, как это:

1
2
3
4
5
6
object CardFactory {
     
    fun getCard(): Card {
        // ...
    }
}

И затем вы можете использовать этот объект как класс со статическими членами:

1
2
3
fun main(args: Array<String>) {
    val card = CardFactory.getCard()
}

Вы даже можете позволить вашим объектам иметь суперклассы :

1
2
3
4
5
6
object SubmitButtonListener : ActionListener {
     
    override fun actionPerformed(e: ActionEvent?) {
        // Submit form...
    }
}

Эта концепция является мощным расширением классов, интерфейсов и перечислений, доступных в Java, потому что часто элементы модели предметной области являются объектами по своей природе (они существуют только один раз и, следовательно, всегда должны иметь самое большее экземпляр во время выполнения).

8) Аргументы по умолчанию

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class NutritionFacts {
    private final String foodName;
    private final int calories;
    private final int protein;
    private final int carbohydrates;
    private final int fat;
    private final String description;
 
    public NutritionFacts(String foodName, int calories) {
        this.foodName = foodName;
        this.calories = calories;
        this.protein = -1;
        this.carbohydrates = -1;
        this.fat = -1;
        this.description = "";
    }
 
    public NutritionFacts(String foodName, int calories, int protein, int carbohydrates, int fat) {
        this.foodName = foodName;
        this.calories = calories;
        this.protein = protein;
        this.carbohydrates = carbohydrates;
        this.fat = fat;
        this.description = "";
    }
 
    public NutritionFacts(String foodName, int calories, int protein, int carbohydrates, int fat, String description) {
        this.foodName = foodName;
        this.calories = calories;
        this.protein = protein;
        this.carbohydrates = carbohydrates;
        this.fat = fat;
        this.description = description;
    }
}

Тьфу, довольно плохо. Но это становится еще хуже, если вы хотите предоставить больше различных конструкторов или добавить больше возможных атрибутов.

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

Но с Kotlin вы можете сделать это еще проще, используя значения по умолчанию для аргументов:

1
2
3
4
5
6
7
class NutritionFacts(val foodName: String,
                     val calories: Int,
                     val protein: Int = 0,
                     val carbohydrates: Int = 0,
                     val fat: Int = 0,
                     val description: String = "") {
}

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

1
2
3
val pizza = NutritionFacts("Pizza", 442, 12, 27, 24, "Developer's best friend")
val pasta = NutritionFacts("Pasta", 371, 14, 25, 11)
val noodleSoup = NutritionFacts("Noodle Soup", 210)

Обратите внимание, что вы также можете сделать этот класс классом данных, для которого будут сгенерированы такие методы, как equals () и toString ().

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

9) Именованные аргументы

Стандартные аргументы становятся еще более мощными в сочетании с именованными аргументами:

1
2
val burger = NutritionFacts("Hamburger", calories = 541, fat = 33, protein = 14)
val rice = NutritionFacts("Rice", 312, carbohydrates = 23, description = "Tasty, nutritious grains")

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

1
myString.transform(true, false, false, true, false)

Если вы не реализовали эту функцию 10 секунд назад, вы никак не можете знать, что здесь происходит (нет гарантии, что вы знаете, даже если вы реализовали ее 10 секунд назад).

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

1
2
3
4
5
6
7
myString.transform(
    toLowerCase = true,
    toUpperCase = false,
    toCamelCase = false,
    ellipse = true,
    normalizeSpacing = false
)

10) Бонус: применение лучших практик

Как правило, Kotlin применяет многие из лучших практик, которым вы должны следовать при использовании Java. Вы можете прочитать о них в книге Джоша Блоха «Эффективная Ява» .

Прежде всего, использование val против var способствует тому, что каждая переменная будет финальной, которая не должна изменяться, и в то же время обеспечивает более краткий синтаксис для нее. Это полезно при создании неизменяемых объектов, например.

Таким образом, начинающие, изучающие язык, также научатся следовать этой практике с самого начала, потому что вы склонны думать о том, использовать ли val или var каждый раз, и учиться предпочитать val, когда это возможно, var.

Далее, Kotlin также поддерживает принцип либо разработки наследования, либо его запрета — потому что в Kotlin вы должны явно объявить класс как открытый , чтобы наследовать его. Таким образом, вы должны помнить, чтобы разрешить наследование, а не помнить, чтобы запретить его.

Что теперь?

Если из этого обзора вам стало интересно узнать больше о Kotlin, вы можете посмотреть мои 10 обучающих видео для Kotlin или перейти к полному курсу (со скидкой 95% для читателей) .

Курс удобен для начинающих и начинается с нуля. Если вы уже знаете Java или сопоставимый язык, вы все равно найдете ценный ресурс для знакомства с Kotlin.