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