Введение в котлинский язык

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-stdlib , kotlin-test-junit и junit в Maven Central ,

Наконец, вам необходимо настроить исходные каталоги и плагин 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>

Вы можете найти последнюю версию https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.jetbrains.kotlin%22%20AND%20a%3A%22kotlin-maven-plugin% 22[kotlin-maven-plugin]в Центральном Maven.

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 имеет базовый синтаксис для определения поля, которое может быть обнуляемым (необязательно). Когда мы хотим объявить этот тип поля обнуляемым, нам нужно использовать тип с суффиксом со знаком вопроса:

val email: String?

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

val email: String? = null

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

val email: String = "value"

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

5. Классы

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

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

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

Обратите внимание, что наш конструктор использует ключевое слово val в качестве аргументов - это означает, что соответствующие поля будут final и неизменными.

Если бы мы использовали ключевое слово var (как мы это делали при определении поля email ), эти поля были бы изменяемыми.

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

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

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

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

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

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

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

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

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

ItemManager("cat__id", "db://connection", "[email protected]")

Если вы хотите определить метод экземпляра в 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. Условные заявления

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

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

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

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

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

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

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 (finally необязательно):

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)
}

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

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 , возвращаемый этим методом, может быть нулевым.

Мы должны обработать этот случай во время компиляции, решая, что мы хотим сделать с этим объектом (он более или менее эквивалентен типу Java 8 Optional <T> ).

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

fun findItemNameForId(id: String): Item

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

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

Давайте напишем контрольный пример для безопасности типов 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 () будет выполняться только в том случае, если результат не обнуляемый. Доступ к полю Id внутри лямбда-функции возможен, потому что он является нулевым.

Другой способ получить доступ к этому полю объекта, который может быть пустым - использовать оператор Котлина !! . Это эквивалентно:

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

Kotlin проверит, является ли этот объект null , если это так, он выдаст исключение NullPointerException, в противном случае он вернет правильный объект.

Функция throwNpe () является внутренней функцией Котлина.

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

Очень хорошая языковая конструкция, которую можно найти в Kotlin, это классы данных (это эквивалентно «case class» из языка 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")

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

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

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

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

Вот как это выглядит в Kotlin:

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

Здесь важно отметить сигнатуру метода.

Методу предшествует имя класса, к которому мы добавляем этот дополнительный метод.

Внутри метода расширения мы работаем с областью списка, поэтому с помощью this предоставил доступ к методам экземпляра списка, таким как 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 s. Это очень полезно, потому что нам не нужно объединять String __s вручную:

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. Совместимость Kotlin/Java

Kotlin - взаимодействие с Java легко и просто. Давайте предположим, что у нас есть класс Java с методом, который работает с String:

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 , в Kotlin возвращаемое значение будет иметь тип Unit .

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

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

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

`object`()

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

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

Реализация всех этих примеров и фрагментов кода может быть найдена в the проект GitHub - это проект Maven, поэтому его должно быть легко импортировать и запустить как есть.