Найти тему

Введение в язык Kotlin

1. Обзор

В этом уроке мы рассмотрим Kotlin, новый язык в мире JVM, и некоторые его основные функции, включая классы, наследование, условные операторы и конструкции циклов.

Затем мы рассмотрим некоторые основные функции, которые делают Kotlin привлекательным языком, включая нулевую безопасность, классы данных, функции расширения и шаблоны String.

2. Зависимости Maven

Чтобы использовать Kotlin в вашем проекте Maven, вам необходимо добавить стандартную библиотеку Kotlin в ваш pom.xml:

<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>1.0.4</version>
</dependency>

Чтобы добавить поддержку JUnit для Kotlin, вам также необходимо включить следующие зависимости:

<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit</artifactId>
<version>1.0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

Наконец, вам нужно будет настроить исходные каталоги и плагин Kotlin для выполнения сборки Maven:

<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>1.0.4</version>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

3. Основной синтаксис

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

Есть некоторое сходство с Java (например, определение пакетов происходит таким же образом). Давайте посмотрим на различия.

3.1. Определение функций

Давайте определим функцию, имеющую два параметра Int с типом возврата Int:

fun sum(a: Int, b: Int): Int {
return a + b
}

3.2. Определение локальных переменных

Локальная переменная с однократным назначением (только для чтения):

val a: Int = 1
val b = 1
val c: Int
c = 1

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

var x = 5
x += 1

4. Необязательные поля

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

val email: String?

Когда вы определили поле, допускающее значение NULL, вполне допустимо присвоить ему значение NULL:

val email: String? = null

Это означает, что поле электронной почты может быть пустым. Если мы напишем:

val email: String = "value"

Затем нам нужно присвоить значение полю электронной почты в том же операторе, в котором мы объявляем электронную почту. Оно не может иметь нулевое значение. Мы вернемся к нулевой безопасности Kotlin в следующем разделе.

5. Занятия

Давайте продемонстрируем, как создать простой класс для управления определенной категорией продукта. Наш класс ItemManager ниже имеет конструктор по умолчанию, который заполняет два поля — CategoryId и dbConnection — и необязательное поле электронной почты:

class ItemManager(val categoryId: String, val dbConnection: String) {
var email = ""
// ...
}

Эта конструкция ItemManager(…) создает конструктор и два поля в нашем классе: CategoryId и dbConnection.

Обратите внимание, что наш конструктор использует для своих аргументов ключевое слово val — это означает, что соответствующие поля будут окончательными и неизменяемыми. Если бы мы использовали ключевое слово var (как мы это делали при определении поля электронной почты ), то эти поля были бы изменяемыми.

Давайте создадим экземпляр ItemManager, используя конструктор по умолчанию:

ItemManager("cat_id", "db://connection")

Мы могли бы создать ItemManager , используя именованные параметры. Это очень полезно, когда у вас есть функция, подобная приведенной в этом примере, которая принимает два параметра одного и того же типа, например String , и вы не хотите путать их порядок. Используя параметры именования, вы можете явно указать, какой параметр назначен. В классе ItemManager есть два поля: CategoryId и dbConnection , поэтому на оба можно ссылаться с помощью именованных параметров:

ItemManager(categoryId = "catId", dbConnection = "db://Connection")

Это очень полезно, когда нам нужно передать функции больше аргументов.

Если вам нужны дополнительные конструкторы, вы должны определить их с помощью ключевого слова конструктора . Давайте определим еще один конструктор, который также устанавливает поле электронной почты:

constructor(categoryId: String, dbConnection: String, email: String)
: this(categoryId, dbConnection) {
this.email = email
}

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

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

ItemManager("cat_id", "db://connection", "foo@bar.com")

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

fun isFromSpecificCategory(catId: String): Boolean {
return categoryId == catId
}

6. Наследование

По умолчанию классы Kotlin закрыты для расширения — эквивалент класса, отмеченного как Final в Java.

Чтобы указать, что класс открыт для расширения, вы должны использовать ключевое слово open при определении класса.

Давайте определим класс Item , который открыт для расширения:

open class Item(val id: String, val name: String = "unknown_name") {
open fun getIdOfItem(): String {
return id
}
}

Обратите внимание, что мы также обозначили метод getIdOfItem() как открытый. Это позволяет его переопределить.

Теперь давайте расширим класс Item и переопределим метод getIdOfItem():

class ItemWithCategory(id: String, name: String, val categoryId: String) : Item(id, name) {
override fun getIdOfItem(): String {
return id + name
}
}

7. Условные операторы

В Котлине условный оператор if является эквивалентом функции, возвращающей некоторое значение. Давайте посмотрим на пример:

fun makeAnalyisOfCategory(catId: String): Unit {
val result = if (catId == "100") "Yes" else "No"
println(result)
}

В этом примере мы видим, что если catId равен «100», условный блок возвращает «Да», в противном случае — «Нет» . Возвращаемое значение присваивается результату.

Вы можете создать обычный блок ifelse:

val number = 2
if (number < 10) {
println("number less that 10")
} else if (number > 10) {
println("number is greater that 10")
}

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

val name = "John"
when (name) {
"John" -> println("Hi man")
"Alice" -> println("Hi lady")
}

8. Коллекции

В Котлине есть два типа коллекций: изменяемые и неизменяемые. Когда мы создаем неизменяемую коллекцию, это означает, что она доступна только для чтения:

val items = listOf(1, 2, 3, 4)

В этом списке нет элемента функции добавления.

Когда мы хотим создать изменяемый список, который можно изменить, нам нужно использовать метод mutableListOf():

val rwList = mutableListOf(1, 2, 3)
rwList.add(5)

Изменяемый список имеет метод add() , поэтому мы можем добавить к нему элемент. Существуют также методы, эквивалентные другим типам коллекций: mutableMapOf(), mapOf(), setOf(), mutableSetOf().

9. Исключения

Механизм обработки исключений очень похож на механизм в Java.

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

Чтобы создать объект исключения, нам нужно использовать выражение throw:

throw Exception("msg")

Обработка исключений осуществляется с помощью блока try…catch (наконец, необязательно):

try {

}
catch (e: SomeException) {

}
finally {

}

10. Лямбды

В Kotlin мы могли определять лямбда-функции и передавать их в качестве аргументов другим функциям.

Давайте посмотрим, как определить простую лямбду:

val sumLambda = { a: Int, b: Int -> a + b }

Мы определили функцию sumLambda , которая принимает в качестве аргумента два аргумента типа Int и возвращает Int.

Мы могли бы передать лямбду:

@Test
fun givenListOfNumber_whenDoingOperationsUsingLambda_shouldReturnProperResult() {
// given
val listOfNumbers = listOf(1, 2, 3)

// when
val sum = listOfNumbers.reduce { a, b -> a + b }

// then
assertEquals(6, sum)
}

11. Циклические конструкции

В Kotlin цикл по коллекциям можно выполнить с помощью стандартной конструкции for..in:

val numbers = arrayOf("first", "second", "third", "fourth")
for (n in numbers) {
println(n)
}

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

for (i in 2..9 step 2) {
println(i)
}

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

  • 2
    4
    6
    8

Мы могли бы использовать функцию rangeTo() , определенную в классе Int , следующим образом:

1.rangeTo(10).map{ it * 2 }

Результат будет содержать (обратите внимание, что rangeTo() также включает):

[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

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

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

class ItemService {
fun findItemNameForId(id: String): Item? {
val itemId = UUID.randomUUID().toString()
return Item(itemId, "name-$itemId");
}
}

Важно отметить возвращаемый тип этого метода. Это объект, за которым стоит вопросительный знак. Это конструкция языка Kotlin, а это означает, что Item, возвращаемый этим методом, может иметь значение null. Нам нужно обработать этот случай во время компиляции, решая, что мы хотим делать с этим объектом (это более или менее эквивалентно типу необязательного<T> в Java 8 ).

Если сигнатура метода имеет тип без вопросительного знака:

fun findItemNameForId(id: String): Item

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

В противном случае, если методу передан объект, допускающий значение NULL, и этот случай не обрабатывается, он не будет скомпилирован.

Давайте напишем тестовый пример для типобезопасности Kotlin:

val id = "item_id"
val itemService = ItemService()

val result = itemService.findItemNameForId(id)

assertNotNull(result?.let { it -> it.id })
assertNotNull(result!!.id)

Здесь мы видим, что после выполнения метода findItemNameForId() возвращаемый тип имеет Kotlin Nullable . Чтобы получить доступ к полю этого объекта ( id ), нам нужно обработать этот случай во время компиляции. Метод let() будет выполняться только в том случае, если результат не имеет значения NULL. Доступ к полю I d можно получить внутри лямбда-функции, поскольку оно безопасно для нулевых значений.

Другой способ получить доступ к этому полю объекта, допускающему значение NULL, — использовать оператор Kotlin !!. Это эквивалентно:

if (result == null){
throwNpe();
}
return result;

Kotlin проверит, является ли этот объект нулевым , и если да, то выдаст исключение NullPointerException, в противном случае вернет правильный объект. Функция throwNpe() — внутренняя функция Kotlin.

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

Очень хорошая языковая конструкция, которую можно найти в Kotlin, — это классы данных (это эквивалент «класса случая» из языка Scala). Целью таких классов является только хранение данных. В нашем примере у нас был класс Item , который содержит только данные:

data class Item(val id: String, val name: String)

Компилятор создаст для нас методы hashCode() , Equals() и toString() . Хорошей практикой является сделать классы данных неизменяемыми, используя ключевое слово val . Классы данных могут иметь значения полей по умолчанию:

data class Item(val id: String, val name: String = "unknown_name")

Мы видим, что поле имени имеет значение по умолчанию «unknown_name».

14. Функции расширения

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

Давайте рассмотрим пример, в котором у нас есть список элементов, и мы хотим взять случайный элемент из этого списка. Мы хотим добавить новую функцию random() в сторонний класс List .

Вот как это выглядит в Котлине:

fun <T> List<T>.random(): T? {
if (this.isEmpty()) return null
return get(ThreadLocalRandom.current().nextInt(count()))
}

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

Внутри метода расширения мы работаем с областью списка, поэтому его использование дает доступ к методам экземпляра списка, таким как isEmpty() или count(). Затем мы можем вызвать метод random() для любого списка, находящегося в этой области:

fun <T> getRandomElementOfList(list: List<T>): T? {
return list.random()
}

Мы создали метод, который принимает список, а затем выполняет пользовательскую функцию расширения random() , которая была определена ранее. Давайте напишем тестовый пример для нашей новой функции:

val elements = listOf("a", "b", "c")

val result = ListExtension().getRandomElementOfList(elements)

assertTrue(elements.contains(result))

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

15. Строковые шаблоны

Очень приятная особенность языка Kotlin — возможность использовать шаблоны для строк . Это очень полезно, поскольку нам не нужно объединять String вручную:

val firstName = "Tom"
val secondName = "Mary"
val concatOfNames = "$firstName + $secondName"
val sum = "four: ${2 + 2}"

Мы также можем вычислить выражение внутри блока ${}:

val itemManager = ItemManager("cat_id", "db://connection")
val result = "function result: ${itemManager.isFromSpecificCategory("1")}"

16. Совместимость Котлина и Java

Kotlin – совместимость с Java очень проста. Предположим, у нас есть класс Java с методом, который работает со строкой:

class StringUtils{
public static String toUpperCase(String name) {
return name.toUpperCase();
}
}

Теперь мы хотим выполнить этот код из нашего класса Kotlin. Нам нужно только импортировать этот класс, и мы сможем без проблем выполнить Java-метод из Kotlin:

val name = "tom"

val res = StringUtils.toUpperCase(name)

assertEquals(res, "TOM")

Как мы видим, мы использовали Java-метод из кода Kotlin.

Вызов кода Kotlin из Java также очень прост. Давайте определим простую функцию Kotlin:

class MathematicsOperations {
fun addTwoNumbers(a: Int, b: Int): Int {
return a + b
}
}

Выполнить addTwoNumbers() из кода Java очень просто:

int res = new MathematicsOperations().addTwoNumbers(2, 4);

assertEquals(6, res);

Мы видим, что вызов кода Kotlin был для нас прозрачен.

Когда мы определяем метод в Java, тип возвращаемого значения которого — void , в Котлине возвращаемое значение будет иметь тип Unit .

В языке Java есть некоторые специальные идентификаторы ( is , object , in , ..), которые при использовании в коде Kotlin необходимо экранировать. Например, мы могли бы определить метод с именем object() , но нам нужно не забыть экранировать это имя, поскольку это специальный идентификатор в Java:

fun `object`(): String {
return "this is object"
}

Тогда мы могли бы выполнить этот метод:

`object`()

17. Заключение

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

Оригинал статьи: https://www.baeldung.com/kotlin/intro