01-kotlin.md 37 KB

Основы Kotlin

Оглавление

Введение

Документация на Kotlin: https://kotlinlang.org/docs/reference/basic-syntax.html.

Kotlin — это относительно молодой язык от российской компании JetBrains. Появился он в 2011 году.

Для нас он интересен тем, что на конференции Google I/O 2017 команда разработчиков Android сообщила, что Kotlin получил официальную поддержку для разработки Android-приложений. Сейчас Kotlin становится основным языком для разработки под Android. Именно его поддержке уделяется наибольшее внимание.

Основные возможности и преимущества Kotlin:

  • Компилируется в байт-код JVM, работает на виртуальной машине Java.
  • Программы могут использовать все существующие Java-фреймворки и библиотеки.
  • Kotlin можно интегрировать с Maven, Gradle и другими системами сборки.
  • Язык очень прост для изучения.
  • Исходный код открыт.
  • Легко читаемый лаконичный синтаксис.
  • В популярной среде разработки IntelliJ IDEA доступна автоматическая конвертация Java-кода в Kotlin и наоборот.

IntelliJ IDEA — среда разработки, созданная компанией JetBrains. Сейчас повсеместно используется для разработки Java- и Kotlin-приложений и не только.

Основные типы

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

Числа

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

  • Double — 64 бита.
  • Float — 32 бита.
  • Long — 64 бита.
  • Int — 32 бита.
  • Short — 16 бит.
  • Byte — 8 бит.

Числовые константы описываются следующим образом. Десятичные числа: 123. Long-тип обозначаются заглавной буквой L: 123L. Шестнадцатеричные числа имеют приставку 0x: 0x0F. Двоичные числа имеют приставку 0b: 0b00001011.

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

val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010

Преобразования

Меньшие типы являются подтипами больших типов.

// Возможный код, который на самом деле не скомпилируется:
val a: Int = 1 // "Int (java.lang.Integer)
val b: Long = a // Неявное преобразование возвращает Long (java.lang.Long)
println(a == b) // Данное выражение выведет "false" т.к. метод equals() типа Long предполагает, что вторая часть выражения также имеет тип Long

Для выполнения корректного сравнения по значению необходимо выполнять преобразования типов:

a.toLong() // Приведение Int к Long
// или
b.toInt() // Приведение Long к Int

Каждый численный тип поддерживает следующие преобразования:

  • toByte(): Byte
  • toShort(): Short
  • toInt(): Int
  • toLong(): Long
  • toFloat(): Float
  • toDouble(): Double

При выполнении арифметических действий с разными типами выполняется преобразование результата к наибольшему типу.

val l = 1L + 3 // Long + Int => Long

Символы

Символы в Kotlin представлены типом Char. Они не являются числами, однако могут быть преобразованы к Int.

Символьные литералы записываются в одинарных кавычках: '1', '\n', '\uFF00'. Мы можем явно привести символ в число типа Int:

val c = 'A'
val num: Int = c.toInt() // Явное преобразование в число
println(num)

Логический тип

Тип Boolean представляет логический тип данных и принимает два значения: true и false.

Встроенные действия над логическими переменными те же, что и в Java:

  • || – логическое ИЛИ,
  • && – логическое И,
  • ! - отрицание.

Массивы

Массивы в Kotlin представлены классом Array<T>. Массивы обладают функциями get и set (которые обозначаются [] согласно соглашению о перегрузке операторов), и свойством size, а также несколькими полезными встроенными функциями.

Для создания массива можно использовать библиотечную функцию arrayOf(), которой в качестве аргумента передаются элементы массива, т.е. выполнение arrayOf(1, 2, 3) создаёт массив [1, 2, 3]. Также существует библиотечная функция arrayOfNulls(). Она может быть использована для создания массива, заполненного значениями null.

val arr: Array<Int> = arrayOf(1, 2, 3)
println(arr.contentToString()) // Вывод содержимого массива в лог

Также в Kotlin есть особые классы для представления массивов примитивных типов без дополнительных затрат на оборачивание: ByteArray, ShortArray, IntArray, Boolean и т.д. Данные классы не наследуют класс Array, хотя и обладают тем же набором методов и свойств. У каждого из них есть соответствующая фабричная функция:

val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2] // [5, 2, 3]

Строки

Строки в Kotlin представлены типом String. Строки являются неизменяемыми. Строки состоят из символов, которые могут быть получены с помощью операции индексирования: s[i].

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

Строки могут содержать шаблонные выражения, т.е. участки кода, которые выполняются, а полученный результат встраивается в строку. Шаблон начинается со знака доллара ($) и состоит либо из простого имени переменной, либо из произвольного выражения в фигурных скобках.

Примеры:

val i = 10
val s = "i = $i" // Строка "i = 10"
val s = "abc"
val str = "$s.length is ${s.length}" // Строка "abc.length is 3"

При необходимости использования символ $ может быть представлен с помощью следующего синтаксиса:

val price = "${'$'}9.99"

Пакеты

Файл с исходным кодом может начинаться с объявления пакета:

package org.example.kotlinlearn

fun someFunction() {}

class SomeClass {}

// ...

Всё содержимое файла с исходниками (например, классы и функции) располагается в объявленном пакете. Таким образом, в приведённом выше примере полное имя функции someFunction() будет org.example.kotlinlearn.someFunction, а полное имя класса SomeClass - org.example.kotlinlearn.SomeClass.

Каждый файл может содержать свои собственные объявления импорта. Можно импортировать одно имя, например:

import org.example.kotlinlearn.SomeClass   
// теперь SomeClass можно использовать без указания пакета

или доступное содержимое пространства имён (пакет, класс, объект и т.д.):

import org.example.kotlinlearn.*
// всё в 'org.example.kotlinlearn' становится доступно без указания пакета

val и var

Переменные в Kotlin бывают изменяемые и неизменяемые. Неизменяемые (только для чтения) переменные объявляются с помощью ключевого слова val:

val a: Int = 1
val b = 1   // Тип Int выведен автоматически
val c: Int  // Тип обязателен, когда значение не инициализируется
c = 1       // Последующее присвоение
c = 2       // Изменение невозможно, ошибка при компиляции

Изменяемые переменные объявляются с помощью ключевого слова var:

var x = 5 // Тип Int выведен автоматически
x += 1

Управляющие конструкции

Условное выражение if

В языке Kotlin if является выражением, т.е. оно возвращает значение. Это позволяет отказаться от тернарного оператора (условие ? условие истинно : условие ложно), поскольку выражению if может его заменить.

// обычное использование 
var max = a 
if (a < b) 
  max = b 
 
// с блоком else 
var max: Int
if (a > b) 
  max = a 
else 
  max = b 
 
// в виде выражения 
val max = if (a > b) a else b

"Ветви" выражения if могут содержать несколько строк кода, при этом последнее выражение является значением блока и может быть возвращено:

val max = if (a > b) { 
    println("возвращаем a") 
    a 
} 
else { 
    println("возвращаем b") 
    b 
}
println(max) // Напечатает "возвращаем a", если a > b, и "возвращаем b", если a <= b.

Если конструкция if используется в качестве выражения (например, возвращая его значение или присваивая его переменной), то использование ветки else обязательно.

Условное выражение when

Ключевое слово when призвано заменить оператор switch, присутствующий в C-подобных языках. В простейшем виде его использование выглядит так:

when (x) {
    1 -> println("x == 1")
    2 -> println("x == 2")
    else -> {
        println("x is neither 1 nor 2")
    }
}

Значение ветки else вычисляется в том случае, когда ни одно из условий в других ветках не удовлетворено.

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

when (x) {
    0, 1 -> print("x == 0 or x == 1")
    else -> print("otherwise")
}

Помимо констант в ветках можно использовать произвольные выражения:

when (x) {
    parseInt(s) -> println("s encodes x")
    else -> println("s does not encode x")
}

Также можно проверять вхождение аргумента в интервал in или !in или его наличие в коллекции:

when (x) {
    in 1..10 -> println("x is in the range")
    in validNumbers -> println("x is valid")
    !in 10..20 -> println("x is outside the range")
    else -> println("none of the above")
}

Иногда выражение when удобно использовать вместо цепочки условий вида if-else if. При отстутствии аргумента, условия работают как простые логические выражения, а тело ветки выполняется при его истинности. Однако злоупотреблять таким решением не стоит, цепочки if-else if чаще бывают понятнее для чтения, особенно, когда условия в них достаточно длинные.

when {
    x.isOdd() -> print("x is odd")
    x.isEven() -> print("x is even")
    else -> print("x is funny")
}

Цикл for

Цикл for обеспечивает перебор всех значений коллекции.

for (item in collection) {
    print(item)
}

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

for (i in array.indices) {
    print(array[i])
}

Также можно использовать библиотечную функцию withIndex() для получения индекса и значения по этому индексу одновременно:

for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}

Цикл while

Циклы while и do..while абсолютно аналогичны таковым же в Java:

// цикл while
while (x > 0) {
    x--
}

// цикл do..while
do {
    val y = retrieveData()
} while (y != null) // y здесь доступно!

Классы

Классы в Kotlin, как и в Java, объявляются с помощью ключевого слова class:

class Person {

}

Тело класса является необязательным: если у класса нет тела, фигурные скобки могут быть опущены:

class Customer

Конструкторы

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

class Customer constructor(name: String)

Ключевое слово constructor может быть опущено:

class Customer(name: String)

Основной конструктор не содержит в себе исполняемого кода. Код для инициализации класса принято помещать в соответствующий блок (initializers blocks), который помечается словом init. По сути блок init содержит код конструктора в привычном его виде. Параметры основного конструктора могут использоваться в инициализирующем блоке.

class Customer(name: String) {
    init {
        println("Customer initialized with value $name")
    }
}

В действительности, для объявления и инициализации свойств основного конструктора в Kotlin есть лаконичное синтаксическое решение:

class Person(val firstName: String, val lastName: String, var age: Int) {
  // Далее в теле класса свойства firstName, lastName и age доступны 
  // и проинициализированы значениями, переданными в конструктор класса Person
}

Для объявления дополнительных (secondary) конструкторов используется ключевое слово constructor:

class Customer(name: String) {

    constructor(name: String, cash: Int) : this(name) {
        println("Customer initialized with name $name and cash $cash")
    }
}

Здесь класс Customer имеет два конструктора: основной, принимающий на вход параметр name; и дополнительный, принимающий на вход параметры name и cash. Если класс имеет основной конструктор с параметром, то каждый его дополнительный конструктор должен делегировать вызов основному с передачей параметров. Именно для этого после сигнатуры конструктора после двоеточия указывается ключевое слово this с передачей параметров. Таким образом сначала будет вызван основной конструктор, а после него дополнительный.

Создание эксемпляров классов

Для создания экземпляра класса конструктор вызывается так, как если бы он был обычной функцией (без ключевого слова new, как в Java):

val customer = Customer("Joe Smith")

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

Для явного объявления суперкласса мы помещаем его имя за знаком двоеточия в объявлении класса:

open class Base(p: Int) {
    fun baseFunc() {}
}

class Derived(p: Int) : Base(p) {
    fun derivedFunc() {
        baseFunc()
    }
}

В данном примере класс Derived наследуется от класса Base и наследует его функцию baseFunc().

Ключевое слово open является противоположностью слову final в Java: оно позволяет другим классам наследоваться от данного. По умолчанию, все классы в Kotlin имеют статус final.

Если у класса нет основного (primary) конструктора, тогда каждый дополнительный (secondary) конструктор должен включать в себя инициализацию базового типа с помощью ключевого слова super. Примечательно, что любые дополнительные конструкторы могут ссылаться на разные конструкторы базового типа:

open class View(ctx: Context) {

  constructor(ctx: Context, attrs: AttributeSet) : this(ctx) {
    this.attrs = attrs
  }
}

class MyView : View {
    constructor(ctx: Context) : super(ctx) {
    }
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
    }
}

Переопределение методов класса

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

open class Base {
    open fun v() {}
    fun nv() {}
}

class Derived() : Base() {
    override fun v() {}
    override fun nv() {} // Ошибка компиляции, т.к. nv() не помечен как open в классе Base
    open fun nov() {} // Пометить можно, но это не будет иметь эффекта, т.к. сам класс Derived не помечен как open
}

Для Derived.v() необходима аннотация override для переопределения метода. Если она отсутствует, компилятор выдаст ошибку. Если у функции типа Base.nv() нет аннотации open, переопределение этого метода в производном классе невозможно. В finalклассе (классе без аннотации open), запрещено использование аннотации open для его методов, как в случае с Derived.nov().

Член класса, помеченный override, является сам по себе open, т.е. он может быть переопределён в производных классах.

Абстрактные классы

Класс и некоторые его члены могут быть объявлены как abstract. Абстрактный член не имеет реализации в своём классе. Обратите внимание, что нам не надо аннотировать абстрактный класс или функцию словом open - это подразумевается и так.

abstract class Base(p: Int) {
    fun baseFunc() {}
    abstract fun abstractFunc();
}

class Derived(p: Int) : Base(p) {
    fun derivedFunc() {
        baseFunc()
    }

    override fun abstractFunc() {
        println("Base.abstractFunc()")
    }
}

Свойства и поля

Классы в Kotlin могут иметь свойства: изменяемые (mutable) и неизменяемые (read-only) — var и val соответственно.

public class Address {
    public var name: String = ...
    public var street: String = ...
    public var city: String = ...
}

Для того, чтобы воспользоваться свойством, мы просто обращаемся к нему по имени:

fun copyAddress(address: Address): Address {
    val result = Address()
    result.name = address.name // вызов методов доступа
    result.street = address.street
    result.city = address.city
    return result
}

Методы доступа (геттеры и сеттеры) могут быть описаны самостоятельно, как и обычные функции, прямо при объявлении свойств. Например, пользовательский геттер и сеттер:

val stringRepresentation: String
    get() = "$name of $street in $city"
    set(value) {
        field = "$value new string"
    }

Классы в Kotlin не могут иметь полей. Т.е. переменные, которые объявляются внутри класса только выглядят и ведут себя как поля из Java, хотя на самом деле являются свойствами, для них неявно реализуются методы get() и set().

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

Нередко создаются классы лишь с целью хранения данных. Функционал таких классов зависит от самих данных, которые в них хранятся. В Kotlin класс может быть отмечен словом data и такой класс называют классом данных:

data class User(val name: String, val age: Int)

Компилятор автоматически формирует следующие методы для класса из свойств, объявленных в основном конструкторе:

  • пару функций equals()/hashCode() — для возможности сравнения конкретных объектов по значению и ссылке.
  • Функцию toString() в форме "User(name=John, age=42)" — для приведения объекта к строке,
  • Компонентные функции componentN(), которые соответствуют свойствам, в соответствии с порядком их объявления (name=component1(), age=component2()),
  • Функцию copy() — для создания копии объекта.

Для обеспечения согласованности и осмысленного поведения сгенерированного кода классы данных должны удовлетворять следующим требованиям:

  • Основной конструктор должен иметь как минимум один параметр.
  • Все параметры основного конструктора должны быть отмечены, как val или var.
  • Классы данных не могут быть абстрактными или "открытыми" (open).

Интерфейсы

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

Интерфейс определяется ключевым словом interface:

interface MyInterface1 {
    fun interface1()
    fun method1() {
      // тело
    }
}

interface MyInterface2 {
    fun interface2()
    fun method2() {
      // тело
    }
}

Класс или объект могут реализовать любое количество интерфейсов:

class Child : MyInterface1, MyInterface2 {
    override fun interface1() {
        // тело
    }
    override fun interface2() {
        // тело
    }
}

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

interface MyInterface {
    val prop: Int // абстрактное свойство

    var _propWithImpl: String // абстрактное свойство
    var propWithImpl: String
        get() = _propWithImpl
        set(value) {
            _propWithImpl = value
        }

    fun interface2()
    fun method2() {
      // тело
    }
}

class Child : MyInterface {
    override val prop: Int = 29
    override val _propWithImpl: String = "property with implementation"
}

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

Null-безопасность

Nullable типы и Non-Null типы

Система типов в языке Kotlin нацелена на то, чтобы избавить программиста от опасности обращения к null-значениям. Одним из самых распространённых подводных камней многих языков программирования, в том числе Java, является попытка произвести доступ к null-значению. Это приводит к ошибке. В Java такая ошибка называется NullPointerException.

Kotlin призван исключить ошибки подобного рода. NullPointerException в Kotlin могу возникать только в следующих случаях:

  • Явное указание throw NullPointerException().
  • Использование оператора !! (описано ниже).
  • Эту ошибку вызвал внешний Java-код.
  • Есть какое-то несоответствие при инициализации данных (в конструкторе использована ссылка this на данные, которые не были ещё проинициализированы).

Система типов в Kotlin различает два типа ссылок: которые могут иметь значение null (nullable ссылки) и те, которые не могут иметь null-знаечния (non-null ссылки). Для того, чтобы разрешить null значение, необходимо добавить к объявлению типа переменноq знак вопроса ?, например, String?.

Пример:

var a: String = "abc"
a = null // ошибка компиляции, a — по умолчанию non-null
var l = a.length // ошибка компиляции, переменная a имеет значение null

var b: String? = "abc"
b = null // нет ошибки, переменная b может иметь значение null
var l = b.length // ошибка: переменная b имеет значение null

Пример выше показывает, что при вызове метода с использованием переменной a, получение NullPointerException исключено, т.к. компилятор не позволит собрать такой код, где a может иметь значение null. Получить доступ к значению переменной b для вызова метода — небезопасно и компилятор предупредит об ошибке. Но если есть необходимость получить доступ к свойству, то есть несколько способов:

  • Проверка на null.
  • Безопасные вызовы.
  • Элвис-оператор (elvis operator или оператор ?:).
  • Оператор !!.

Проверка на null

Первый способ. Можно явно проверить переменную на null значение и обработать два варианта по отдельности:

val l = if (a != null) a.length else -1

Компилятор отслеживает информацию о проведённой проверке и позволяет вызывать length внутри блока if.

Безопасные вызовы

Вторым способом является оператор безопасного вызова ?.:

var l = a?.length

Этот код возвращает a.length в том, случае, если a не null. Иначе он возвращает null, и тогда типом этого выражения (переменной l) будет Int?.

Элвис-оператор

Он же elvis operator или оператор ?: (не путать с тернарным оператором).
Полезен в ситуации, когда необходимо либо вернуть некоторое значение сслылки, если она не null, либо некое значение "по-умолчанию".

Пример с if-else:

val l: Int = if (a != null) b.length else -1

Аналогом такому if-выражению является элвис-оператор ?::

val l = a?.length ?: -1

Если выражение, стоящее слева от элвис-оператора, не является null, то элвис-оператор его вернёт (a.length). В противном случае в качестве возвращаемого значения послужит то, что стоит справа (-1).

Оператор !!

Для любителей NPE существует ещё один способ. Можно написать a!! и это вернёт либо non-null значение a, либо выкинет NPE:

val l = a!!.length

Создать проект в IntelliJ IDEA

New -> Kotlin -> JVM -> Next.

Project Name -> Finish.

Add kotlin file -> Add configuration -> Run.