Skip to content
Michael Bely edited this page Apr 29, 2024 · 184 revisions

Important

ВНИМАНИЕ!
ЭТОТ РАЗДЕЛ БОЛЬШЕ НЕ ПОДДЕРЖИВАЕТСЯ!
РОАДМАП ПЕРЕЕХАЛ В NOTION

Kotlin features, которых нет в Java
• Null safety
• Extension functions
• Named arguments
• Type inference

Basic types
Примитивные типы. В kotlin нет примитивных типов данных, все является объектами. Byte, Short, Int, Double, Char, Float, Long, Boolean - все это объекты, наследуются от Any и переопределяют его методы

Null safety
Cистема типов Kotlin различает ссылки на nullable и not null. Для безопасных вызовов используются операторы ?. и ?: (элвис). Для утверждающего вызова оператор !!

== vs ===
Оператор == сравнивает значения, а === ссылки на переменные

val number = Integer(1)
val anotherNumber = Integer(1)
number == anotherNumber // true (structural equality)
number === anotherNumber // false (referential equality)

val
Постоянная переменная, может быть инициализирована 1 раз, вычисляется в run time. Аналог final в Java

var
Мутабельная переменная

const
Иммутабельна как и val, вычисляется в compile time

varargs
Передать переменное количество аргументов

init
Обычная inline-функция, вызывается после создания класса и вызова конструктора. Класс может содержать несколько блоков init, вызывающихся последовательно

lateinit
Используется с мутабельными значениями, когда поле не имеет изначального метода получения или установки значения (внедрение зависимостей, настройка тестов)

constructor
В классах можно объявить один или несколько вторичных конструкторов. Каждый вторичный конструктор должен делегировать полномочия первичному конструктору через ключевое слово this

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

for (n in 1..8) {
    if (n == 5) continue
    println(n * n)
}

break
Завершает выполнение цикла. При некоторых условиях нам вовсе надо выйти из цикла, прекратить его выполнение. В данном случае когда n окажется равен 5, то с помощью оператора break будет выполнен выход из цикла. Цикл полностью завершится

for (n in 1..8) {
    if (n == 5) break
    println(n * n)
}

inner
Вложенный класс по умолчанию не имеет доступа к свойствам и функциям внешнего класса. Чтобы вложенный класс мог иметь доступ к свойствам и функциям внешнего класса, необходимо определить вложенный класс с ключевым словом inner

class A {
    private val n: Int = 1
        
    inner class B {
        private val n: Int = 1
            
        fun action() {
            println(n)        // n из класса B
            println(this.n)   // n из класса B
            println(this@B.n) // n из класса B
            println(this@A.n) // n из класса A
        }
    }
}

final
Запретить повторное переопределение

open class Sam: Person() {
    final override fun printName() {
        println("Sam")
    }
}

tilerec
Сообщить компилятору о выполнении хвостовой рекурсии

Types

Byte
Хранит целое число от -128 до 127 и занимает 1 байт

Short
Хранит целое число от -32 768 до 32 767 и занимает 2 байта

Int
Хранит целое число от -2 147 483 648 (-231) до 2 147 483 647 (231 - 1) и занимает 4 байта

Long
Хранит целое число от –9 223 372 036 854 775 808 (-263) до 9 223 372 036 854 775 807 (263-1) и занимает 8 байт

UByte
Хранит целое число от 0 до 255 и занимает 1 байт

UShort
Хранит целое число от 0 до 65 535 и занимает 2 байта

UInt
Хранит целое число от 0 до 232 - 1 и занимает 4 байта

ULong
Хранит целое число от 0 до 264-1 и занимает 8 байт

Float
Хранит число с плавающей точкой от -3.41038 до 3.41038 и занимает 4 байта

Double
Хранит число с плавающей точкой от ±5.010-324 до ±1.710308 и занимает 8 байт

Boolean
Может хранить одно из двух значений: true (истина) или false (ложь)

Char
Отдельный символ, который заключается в одинарные кавычки

String
Строка представляет последовательность символов, заключенную в двойные кавычки, либо в тройные двойные кавычки

Number
Суперкласс для всех классов платформы, представляющих числовые значения

Open-ended ranges ..<
Оператор для выражения диапазона значений. Работает как until. Дает понять, что верхняя граница не включена

when (value) {
    in 0.0..<0.25 -> // first quarter
    in 0.25..<0.5 -> // second quarter
    in 0.5..<0.75 -> // third quarter
    in 0.75..1.0 ->  // last quarter  <- note closed range here
}

Float vs Double?
Точность Double в 2 раза выше, чем Float. Double занимает 64 бит и может содержать 15 чисел после запятой, Float - 32 бит и 7 чисел после запятой

Целочисленные типы?
Объекты целочисленных типов хранят целые числа. Byte, Short, Int, Long. Начиная с Kotlin 1.5: UByte, UShort, UInt, ULong. Кроме чисел в десятичной системе мы можем определять числа в двоичной и шестнадцатеричной системах. Шестнадцатеричная запись числа начинается с 0x, затем идет набор символов от 0 до F, которые представляют число. Двоичная запись числа предваряется символами 0b, после которых идет последовательность из нулей и единиц

Числа с плавающей точкой?
Позволяют хранить дробные числа. В качестве разделителя целой и дробной части применяется точка. Double поддерживает экспоненциальную запись

val a: Byte = -10
val b: Short = 45
val c: Int = -250
val d: Long = 30000L

val a: UByte = 10U
val b: UShort = 45U
val c: UInt = 250U
val d: ULong = 30000U

val a: Int = 0x0A1 // 161

val a: Int = 0b0101 // 5
val b: Int = 0b1011 // 11

val height: Double = 1.78
val pi: Float = 3.14F

val d: Double = 23e3 // 23 000
val g: Double = 23e-3 // 0.023

val a: Boolean = true
val b: Boolean = false

val a: Char = 'A'
val b: Char = 'B'
val c: Char = 'T'

val t: Char = '\t' // табуляция
val n: Char = '\n' // перевод строки
val r: Char = '\r' // возврат каретки
val a: Char = '\'' // одинарная кавычка
val b: Char = '\"' // двойная кавычка
val c: Char = '\\' // обратный слеш

val name: String = "Michael"
val name: String = """Michael"""
val range: IntRange = 1..5 // [1, 2, 3, 4, 5]
val range: CharRange = 'a'..'d'
val range: LongRange = 0L..1L
val range: ClosedRange<String> = "a".."d"

val range: IntProgression = 5 downTo 1 // 5 4 3 2 1
val range: IntProgression = 1..10 step 2 // 1 3 5 7 9
val range: IntProgression = 10 downTo 1 step 3 // 10 7 4 1
val range: IntProgression = 1 until 9 // 1 2 3 4 5 6 7 8
val range: IntProgression = 1 until 9 step 2 // 1 3 5 7

val isInRange: Boolean = 5 in 1..5
val isNotInRange: Boolean = 5 !in 1..5

val numbers: Array<Int> = arrayOf(1, 2, 3, 4, 5)
val numbers = Array(3) { 5 } // [5, 5, 5]

var i = 1;
val numbers = Array(3) { i++ * 2 } // [2, 4, 6]

val numbers: IntArray = intArrayOf(1, 2, 3, 4, 5)
val doubles: DoubleArray = doubleArrayOf(2.4, 4.5, 1.2)

val numbers: IntArray = IntArray(3) { 5 }
val doubles: DoubleArray = DoubleArray(3) { 1.5 }

val table: Array<Array<Int>> = Array(3) { Array(5) { 0 } } // двумерный массив
fun changeNumbers(vararg values: Int, koef: Int) {}

val numbers: IntArray = intArrayOf(1, 2, 3, 4)
changeNumbers(*numbers, koef = 2)
val a: Int = 65 % 10  // 5. Возвращает остаток от целочисленного деления двух чисел

Any
Аналог Object в Java, но с меньшим количеством методов: equals, hashCode, toString. Все классы в Kotlin - наследники Any

Unit
Аналог void в Java. У функции возвращаемый тип можно не указывать, если она ничего не возвращает. Unit - объект, который наследуется от Any и имеет один метод - toString

Notning
Класс, который наследуется от всех классов Kotlin, даже классов с модификатором final. Nothing нельзя создать — у него приватный конструктор. Так как невозможно передать или вернуть тип Nothing, он описывает результат «функции, которая никогда ничего не вернёт». Примером может быть функция, которая выбрасывает exception или в которой запущен бесконечный цикл: в любом из этих случаев она никогда не вернёт значения. В приложениях — независимо от того, какой тип данных возвращает функция, — она может никогда не вернуть данные, потому что произошла ошибка или вычисления затянулись на неопределённый срок. В этом случае имеет смысл использовать Nothing. Который никогда не возвращает управление назад. Используют для методов Error, которые пробрасывают ошибку. Как прыгнуть с обрыва. Unit - прыгнуть с обрыва на веревке и вернуться назад без результата. Удобно использовать в generics, чтобы не возвращать nullable значения

TODO("do this") // Пример функции Nothing

sealed class Result<out A: Any, out B: Any> {
    data class Success<A: Any>(val value: A): Result<A, Nothing>()
    data class Failure<B: Any>(val error: B): Result<Nothing, B>()
}

Properties

get
Получить значение свойства

set
Присвоить новое значение свойству

val number: Int
    get() = 1

var number: Int = 0
    set(value) {
        field = value
    }

var dependency: MyDependency? = null
    @Inject set

Inline Properties

Как и встроенные функции, свойства и методы доступа к свойствам можно объявлять как встроенные

val password: String
    inline get() = "qwerty"

inline var complexProperty: Int
    get() {
        val x = 2
        val y = 4
        return x + y
    }
    set(value) {
        val adjustedValue = value + 10
        println("Setting adjusted value $adjustedValue")
    }

var complexProperty: Int
    inline get() {
        val x = 2
        val y = 4
        return x + y
    }
    inline set(value) {
        val adjustedValue = value + 10
        println("Setting adjusted value $adjustedValue")
    }

Lambdas

Lambda
Функция без названия (анонимная функция). Можно присвоить переменной и передавать как значение. Под капотом статический класс Java с 1 методом, который создается только один раз для всех вызовов

val printer: (String) -> Unit = { message: String -> 
    println(message) 
}

printer("Hello")

val empty: (value: String) -> Boolean
    get() = { it.isEmpty() }

empty("World)

Delegates

Делегирование представляет паттерн объектно-ориентированного программирования, который позволяет одному объекту делегировать/перенаправить все запросы другому объекту. В определенной степени делегирование может выступать альтернативой наследованию

interface Messenger {
    fun send(message: String)
}

class InstantMessenger: Messenger {
    override fun send(message: String) {
        println(message)
    }
}

class SmartPhone(name: String, messenger: Messenger): Messenger by messenger

val telegram = InstantMessenger()
val pixel = SmartPhone("Pixel 5", telegram)
pixel.send("Hello World")

Delegated Properties

Делегированные свойства позволяют делегировать получение или присвоение их значения во вне - другому классу. Это позволяет нам добавить некоторую дополнительную логику при операции со свойствами, например, логгирование, какую-то предобработку и т.д

class Person(personName: String) {
    var name: String by LoggerDelegate(personName)
}

class LoggerDelegate(private var personName: String) {

    operator fun getValue(thisRef: Person, property: KProperty<*>): String {
        return personName
    }

    operator fun setValue(thisRef: Person, property: KProperty<*>, value: String) {
        personName = value
    }
}

val tom = Person("Tom")
tom.name = "Bob"
println(tom.name)

by
Делегирует реализацию интерфейса другому объекту. Делегирует реализацию средств доступа для свойства другому объекту

lazy
Используется с иммутабельными объектами, когда требуется объявить переменную, но отложить инициализацию. Есть определенные классы, инициализация объектов которых занимает так много времени, что приводит к задержке всего процесса создания класса
LazyThreadSafetyMode.SYNCHRONIZED - Используется по умолчанию. Значение вычисляется только в одном потоке выполнения, и все остальные потоки могут видеть одно и то же значение
LazyThreadSafetyMode.PUBLICATION - Используется, когда синхронизация не требуется, тогда несколько потоков смогут исполнять вычисление одновременно
LazyThreadSafetyMode.NONE - Используется, если инициализация всегда будет происходить в одном потоке исполнения. Не гарантирует никакой потокобезопасности и связанных с этим дополнительных затрат

val name: String by lazy(::getName)
val name: String by lazy(LazyThreadSafetyMode.PUBLICATION) {
    getName()
}

notNull
Возвращает делегат свойства, которое != null и инициализируется не во время создания объекта, а позднее. Попытка прочитать свойство до присвоения начального значения приводит к исключению

private var number: Int by notNull()

Extension Functions

Kotlin позволяет расширять класс или интерфейс новой функциональностью, не наследуясь от него и не используя шаблоны проектирования. Для того, чтобы объявить функцию-расширение, нужно указать в качестве префикса расширяемый тип. Расширения вычисляются статически. Расширения на самом деле не проводят никаких модификаций с классами, которые они расширяют. Объявляя расширение, вы создаёте новую функцию, а не новый член класса
• как расширения поддерживаются функции и свойства
• под капотом статическая функция Java, в качестве параметра - объект расширяемого типа

val Int.toHexColor: String
    get() = String.format("#%06X", 0xFFFFFF and this)

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1]
    this[index1] = this[index2]
    this[index2] = tmp
}
interface IAction {
    fun someA(): Int
    fun someB(): Int
}

fun IAction.sum(): Int {
    return someA() + someB()
}

Inline Functions

Встроенные функции. Инструктирует компилятор вставлять полное тело функции везде, где она вызывается. Чтобы понять за счёт чего она повышается, нужно вспомнить лямбда-выражения. Лямбды компилируются в анонимные классы, каждый раз, когда используется лямбда-выражение - создаётся дополнительный класс. Отсюда дополнительные накладные расходы у функций, которые принимают лямбду в качестве аргумента. Если функцию заинлайнить, то компилятор не будет создавать анонимные классы и их объекты для каждого лямбда-выражения, а просто вставит код её реализации в место вызова, другими словами встроит её. Inline лучше не использовать , если функция очень большая, тогда вставлять ее становится затратно. Когда в лямбде нет замыкания (грубо говоря: из лямбды не вызываются переменные и методы, которые не лежат внутри самой лямбды), для её реализации компялтор создаёт синглтон. Но для лямбды с замыканием компилятор вынужен создавать инстанс для каждого вызова лямбды
• основное предназначение - повысить производительность (вызов функции с параметрами разных типов внутри цикла)
• non-local return
• reified generics

inline
Указывает компилятору встроить функцию и лямбда-выражение на стороне вызова. Позволяют избежать дополнительного выделения памяти и ненужных вызовов методов для каждого лямбда-выражения. Если inline-функция не имеет inline-параметров (лямбд) и параметров вещественного типа (reified), то компилятор выдаст предупреждение, так как встраивание такой функции вряд ли принесёт пользу

class Lock {
    fun lock() {}
    fun unlock() {}
}

inline fun lock(
    lock: Lock,
    block: () -> Unit
) {
    lock.lock()
    try {
        block()
    } finally {
        lock.unlock()
    }
}

lock(Lock()) {
    // do something
}

noinline
По умолчанию вызов метода и все переданные лямбды встроятся на стороне вызова, лямбда с noinline встроена не будет. Встраиваемые лямбды могут быть вызваны только внутри inline-функций или переданы в качестве встраиваемых аргументов, с noinline-функциями можно работать без ограничений: хранить внутри полей, передавать куда-либо и т.д. Порой в inline функции нам нужно не вызвать лямбду на месте, а передать дальше, в другой метод. Тогда лямбда-параметр можно пометить как noinline, и он не будет встраиваться в место вызова

inline fun executeAll(action1: () -> Unit, noinline action2: () -> Unit) {
    action1() // эта лямбда встроится
    someNoinlineMethod(action2()) // а эта останется лямбдой и будет передана в другой метод
}

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

inline fun sayHello(crossinline block: () -> Unit) {
    println("in sayHello")
    block()
}

fun main() {
    println("start execution:")
    sayHello {
        println("in lambda")
        return // error (because function is crossinline)
    }
    println("end execution")
}

reified
Используются только в inline функциях. Параметр типа будет доступен во время компиляции

inline fun <reified T> doSomething(value: T) {
    println("Doing something with type: ${T::class.simpleName}")
}

Infix Functions

Инфиксная функция используется для вызова функции без использования скобок. Под капотом компилируются в обычные статические методы Java

infix
Вызывает функцию в инфиксной записи

infix fun Int.add(b: Int): Int = this + b
val y: Int = 10 add 20

Collections

Коллекции - контейнеры для хранения данных
• kotlin полагается на коллекции Java
• делятся на mutable и immutable

Iterable
Находится на вершине коллекций. Все изменяемый коллекции реализуют интерфейс MutableIterable - он представляет функцию итератора для перебора коллекции

Collection
Для изменения данных определен интерфейс MutableCollection, наследуется от Collection и предоставляет методы для удаления и добавления элементов
• add(element): добавляет элемент
• remove(element): удаляет элемент
• addAll(elements): добавляет набор элементов
• removeAll(elements): удаляет набор элементов
• clear(): удаляет все элементы из коллекции

List
Представляет обычный список на основе массива

val list: List<String> = listOf("Tom", "Sam", "Bob", "Mike")

MutableList
Изменяемый список. Под капотом ArrayList

val list: MutableList<String> = mutableListOf("Tom", "Sam", "Bob", "Mike")

Set
Представляет неупорядоченную коллекцию элементов, не допускающую дублирования элементов. Интерфейс MutableSet реализуется следующими типами изменяемых наборов: LinkedHashSet: объединяет возможности хеш-таблицы и связанного списка, HashSet: представляет хеш-таблицу

val people: Set<String> = setOf("Tom", "Sam", "Bob", "Mike")
val people: HashSet<String> = hashSetOf("Tom", "Sam", "Bob", "Mike") // java collections
val people: LinkedHashSet<String> = linkedSetOf("Tom", "Sam", "Bob", "Mike") // java collections

MutableSet
Изменяемая неупорядоченная коллекция уникальных элементов

val people: MutableSet<String> = mutableSetOf("Tom", "Sam", "Bob", "Mike")

Map
Не расширяет Collection и представляет набор пар ключ-значение, где каждому ключу сопоставляет некоторое значение. Все ключи в коллекции являются уникальными. Интерфейс MutableMap реализуется рядом коллекций - HashMap: простейшая реализация интерфейса MutableMap, не гарантирует порядок элементов в коллекции, LinkedHashMap: представляет комбинацию HashMap и связанного списка

val people: Map<Int, String> = mapOf(1 to "Tom", 2 to "Sam", 3 to "Bob")
val people: LinkedHashMap<Int, String> = linkedMapOf(1 to "Tom", 2 to "Sam", 3 to "Bob") // java collections
val people: HashMap<Int, String> = hashMapOf(1 to "Tom", 2 to "Sam", 3 to "Bob") // java collections

MutableMap
Изменяемая коллекция Map

val people: MutableMap<Int, String> = mutableMapOf(1 to "Tom", 2 to "Sam", 3 to "Bob")

Sequences

Kotlin Sequences: Getting Started

Последовательности предоставляют похожую функциональность, что и интерфейс Iterable, который реализуется типами коллекций. Ключевая разница состоит в том, как обрабатываются элементы последовательности при применении к ним набора операций. При применении набора операций к коллекции Iterable каждая отдельная операция возвращает промежуточный результат - промежуточную коллекцию. А при обработке последовательности весь набор операций выполняется только тогда, когда требуется конечный результат обработки. Также меняется порядок применения операций. Коллекция применяет каждую операцию последовательно к каждому элементу. То есть сначала выполняет первую операцию для всех элементов, потом вторую операцию для элементов коллекции, полученных после первой операции. И так далее. Последовательности выполняют все операции для первого элемента, потом для второго и так далее. Последовательности лениво выполняют свои операторы, когда вызывается их результат
• в последовательностях промежуточные операции (map, distinct groupBy) не выполняются пока не вызовется терминальная операция (first, toList, count)
• хранят ссылки на все промежуточные операции, которые необходимо выполнить (эти функции не inline)
• sequences изменяют базовый экземпляр коллекции, поскольку они не создают новый экземпляр после каждой операции, в отличие от collections, которые создают новый экземпляр после каждой операции
• меньше методов и операций, чем у collections (foldRight, takeLast, reverse)
• не поддерживают параллелизм

val people: Sequence<String> = sequenceOf("Tom", "Sam", "Bob")

val people: Sequence<String> = sequence {
    yield("Tom")
    yield("Sam")
    yield("Sam")
}

Value Classes

value
Ключевое слово для объявления класса значений (или встроенного класса, раньше назывались inline). Может иметь только один параметр, заданный в первичном конструкторе. Этот класс - оболочка вокруг значения это параметра без накладных расходов. Пример: удобный параметр Duration для установки времени в миллисекундах, вместо установки значения в Long, где легче ошибиться. Без value каждый раз создается объект класса и размещается в памяти, компилятор котлина убирает упаковку и обеспечивает производительность

value class Duration private constructor(val millis: Long) {

    constructor(millis: Int): this(millis.toLong()) {
        check(millis != 0)
    }

    init {
        check(millis != 0L)
    }

    companion object {
        fun millis(millis: Long): Duration = Duration(millis)
        fun seconds(seconds: Long): Duration = Duration(seconds * 1000)
    }
}

val duration: Long = Duration.seconds(2L).millis
val duration: Long =  Duration.millis(1200).millis

Generic inline classes

@JvmInline
value class UserId<T>(val value: T)

fun compute(s: UserId<String>)

Sealed Classes

Изолированные (запечатанные) классы и интерфейсы позволяют выразить ограниченные иерархии классов, которые обеспечивают больший контроль над наследованием. Например, сторонние клиенты не могут расширить ваш изолированный класс в своем коде. То же самое справедливо для изолированных интерфейсов и их реализаций: новые реализации не могут появиться после компиляции модуля с изолированным интерфейсом. Изолированные классы похожи на enum-классы на стероидах: набор значений enum типа также ограничен, но каждая enum-константа существует только в единственном экземпляре, в то время как наследник изолированного класса может иметь несколько экземпляров, которые могут нести в себе какое-то состояние
• все наследники известны в compile time
• не можем быть создан напрямую, может быть только предком
• нельзя присвоить анонимному классу, так как это вычисляется в run time
• не могут быть open, inner
• модификатор abstract является избыточным, поскольку класс sealed, может иметь абстрактные компоненты
• можно использовать sealed interface вместо класса, если не нужно передавать параметры в конструктор

sealed interface Error

sealed class IOError(): Error

class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()

object RuntimeError: Error

Data Classes

Класс с единственным назначением - хранить данные. Компилятор автоматически добавляет в такой класс функции с определенной реализацией, которая учитывает свойства класса, определенные в первичном конструкторе
• не может быть суперклассом, от data class нельзя унаследоваться (из-за сложности кодогенерации)
• должен иметь как минимум один параметр val или var в первичном конструкторе
• поля data-класса не учитываются при генерации методов, только поля из первичного конструктора (слишком накладно)
• не поддерживает модификаторы open, abstract, sealed, inner
• можно реализовывать интерфейсы и наследоваться от других open-классов. Если имена параметров совпадают в обоих классах их можно переопределить (должны быть также open)
• если в суперклассе реализованы equals(), hashCode(), toString() - data class будет использовать их
• стандартные реализации: Pair, Triple
• unlike java, не нужно указывать getters/setters
• можно переопределить только equals(), только hashCode() или только любой один из методов
• поддерживает декомпозицию на переменные с помощью функций componentN()

Methods
equals() - сравнивает два объекта. Выполняется instanceof-check, каждое поле в конструкторе сравнивается через equals
hashCode() - возвращает хеш-код объекта. Вычисляется на основе каждого поля, объявленного в конструкторе
toString() - возвращает строковое представление объекта
copy() - копирует данные объекта в другой объект
componentN() - компонентные функции в порядке объявления полей класса

open class Man {
    open val name: String = "John"
}

data class Person(
    override val name: String,
    val age: Int
): Man(), Serializable

val alice: Person = Person("Alice", age = 24)

val (name, age) = alice // декомпозиция на переменные

Object

object
Singleton, инициализируется один раз при первом обращении. Другое назначение - анонимный класс

data object
Объект с чистым представлением toString

package org.example
object MyObject
data object MyDataObject

fun main() {
    println(MyObject) // org.example.MyObject@1f32e575
    println(MyDataObject) // MyDataObject
}

companion object
Object, объявленный внутри класса. Функции такого объекта можно вызывать через класс. Companion object отличается тем, что инициализируется в момент загрузки своего родительского класса, анонимные объекты инициализируются сразу же где используются, а обычные object лениво при первом доступе

Enums

Перечисления представляют тип данных, который позволяет определить набор логически связанных констант

enum class Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

enum class Day(val value: Int) {
    MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4), FRIDAY(5), SATURDAY(6), SUNDAY(7);

    fun getDuration(day: Day): Int {
        return value - day.value;
    }
}

val day: Day = Day.FRIDAY
println(day.name) // FRIDAY. Возвращает название константы в виде строки
println(day.ordinal) // 4. Возвращает порядковый номер константы
println(Day.values().map(Day::name)) // [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
println(Day.entries) // [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]

Scope functions

Функции контекста. Функции области видимости. Позволяют выполнить для некоторого объекта некоторый код в виде лямбда-выражение. При вызове подобной функции, создается временная область видимости. В этой области видимости можно обращаться к объекту без использования его имени. Scoped functions are functions that execute a block of code within the context of an object. There are five scoped functions in kotlin: let, run, with, also and apply. Scope function различаются результатом, который они возвращают

let
Лямбда-выражение в функции let в качестве параметра it получает объект, для которого вызывается функция. Возвращаемый результат функции let представляет результат данного лямбда-выражения

email?.let { 
    println("Email: $it") 
}
email?.let(::println)

run
Лямбда-выражение в функции run в качестве параметра this получает объект, для которого вызывается функция. Возвращаемый результат функции run представляет результат данного лямбда-выражения

val tom = Person("Tom", null)
val validationResult = tom.email?.run {"valid"} ?: "undefined"

with
Позволяет выполнить несколько операций над одним объектом, не повторяя его имени. Функция принимает два аргумента - объект и лямбда-выражение. Первый аргумент преобразуется в получатель лямбда-выражения. К получателю можно обращаться через this

with(email) {
    substring(1)
    this.take(1)
}

apply
Лямбда-выражение в функции apply в качестве параметра this получает объект, для которого вызывается функция. Возвращаемым результатом функции фактически является передаваемый в функцию объект для которого выполняется функция

val person = Person("Tom")
person.apply { 
    tom.name = "Sam"
}

also
Лямбда-выражение в функции also в качестве параметра it получает объект, для которого вызывается функция. Возвращаемым результатом функции фактически является передаваемый в функцию объект для которого выполняется функция. Эта функция аналогична функции apply за тем исключением, что внутри also объект, над которым выполняется блок кода, доступен через параметр it

val person = Person("Tom")
person.also { 
    it.name = "Sam"
}

Special Identifiers

it
Используется внутри лямбды, чтобы косвенно ссылаться на ее параметр

field
Автоматически генерируемое поле, которое непосредственно хранит значение свойства

var age: Int = 18
    set(value) {
        if (value < 100) {
            field = value
        }
    }

Access Modifiers

public
Видны везде. Общедоступный модификатор по умолчанию

private
Видны только в том файле, в котором определены
• несовместим с override и internal
• можно указать private open class Person, нельзя private open fun run()

protected
Видны в классах и наследниках
• нельзя использовать в методах верхнего уровня, только внутри классов
• если переопределить protected open метод и в наследнике добавить модификатор public - для метода ничего не изменится

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

internal
Виден на уровне модуля. Не будут индексироваться в других модулях, не будут перегружать компилятор и автодополнение
• в Java нет модификатора internal, при компиляции станет просто общедоступным public
• в Java методы доступа к переменным помеченные как internal будут искажены, чтобы их сложнее было случайно вызвать. Класс при этом не обязательно помечать как internal

internal class Foo {
    internal var name: String = "John"
}
Foo foo = new Foo();
foo.getName$android_app_template_app_main(); // get
foo.setName$android_app_template_app_main("name"); // set

Generics

Generics или обобщения представляют технику, посредством которой методы и классы могут использовать объекты, типы которых на момент определения классов и функций неизвестны. Обобщения позволяют определять шаблоны, в которые можно подставлять различные типы. Ограничения обобщений (generic constraints) ограничивают набор типов, которые могут передаваться вместо параметра в обобщениях. По умолчанию ко всем параметрам типа также применяется ограничение в виде типа Any?. То есть определение параметра типа фактически аналогично определению <T: Any?>

open class Grandpa // Дед

open class Dad: Grandpa() // Батя

class Son: Dad() // Сынок

Covariance
Ковариантность. Если есть тип T (батя), то разрешены эти типы и его подтипы (сынок). Пример - есть налог на людей без внуков TaxPayer, такой налог будут платить батя и сынок

Contravariance
Контравариантность. Если есть тип T (батя), то разрешены эти типы и его супертипы (дед). Пример - только взрослые могут пить пивас Adult, таким образом пивас пьют батя и дед

Invariance
Инвариантность. Если есть тип T (батя), то разрешены эти типы и никаки другие. Пример - скидка для людей, у которых есть родители и дети Customer<Parent, скидку получает только батя

Bivariance
Бивариантность. Если есть тип T (батя), то разрешены как подтипы, так и супертипы

out
Определяет ковариантность в месте объявления. Делает параметризованный тип ковариантным. Если T - Any, то разрешены также все наследники Any

interface Source<out T: Any> {
    fun nextT(): T
}

fun demo(strings: Source<String>) {
    val objects: Source<Any> = strings // Всё в порядке, т.к. T — out-параметр
}
val value: List<Any> = listOf(1, 2, 3) // since List signature is List<out T> in Kotlin

in
Определяет контравариантность в месте объявления. Делает параметризованный тип контравариантным. Если T - Double, то разрешен также Number

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 имеет тип Double, расширяющий Number
    val y: Comparable<Double> = x // Можем присвоить значение x переменной типа Comparable<Double>
}

where
Используется для указания нескольких ограничений типов

class Person<T>(val id: T, val name: String)

fun <T> display(obj: T) {
    println(obj)
}

val tom: Person<Int> = Person(367, "Tom")
val bob: Person<String> = Person("A65", "Bob")

display("Hello Kotlin")
display(1234)
display(true)

fun <T: Comparable<T>> getBiggest(a: T, b: T): T {
    return if (a > b) a else b
}

fun <T> getBiggest(a: T, b: T): T where T: Comparable<T>, T: Number {
    return if (a > b) a else b
}

class Messenger<T> where T: Serializable, T: Parcelable {
    fun convert(obj: T) {}
}

Исключение представляет событие, которое возникает при выполнении программы и нарушает ее нормальной ход. Конструкция try может возвращать значение. В отличие от Java, в Kotlin нет checked exceptions, исключения не объявляются явно в сигнатурах функций

try {
    // код, генерирующий исключение
} catch (e: Exception) {
    // обработка исключения
} finally {
    // постобработка
}

throw
Генерирует исключение. Выражение throw имеет тип Nothing

throw Exception("Invalid value")

Throwable
Базовый класс для всех исключений

@Throws
Сообщает вызывающим объектам Java, что функция может выдать исключение

@Throws(IllegalArgumentException::class)
fun addNumberToTwo(a: Any): Int {
    if(a !is Int) {
        throw IllegalArgumentException("Number must be an integer")
    }
    return 2 + a
}

Smart casts

is
Проверяет выражение на пренадлежность к определенному типу данных

hello is String
hello !is Int

as
Приводит значения одного типа к другому

hello as String
hello as? String

Operator overloading

operator
Определяет для типов ряд встроенных операторов

class Counter(var value: Int) {
    operator fun plus(counter: Counter): Counter {
        return Counter(this.value + counter.value)
    }

    operator fun minus(counter: Counter): Counter {
        return Counter(this.value - counter.value)
    }

    operator fun compareTo(counter: Counter): Int {
        return this.value - counter.value
    }
}

operator fun Counter.plus(value: Int): Counter {
    return Counter(this.value + value)
}

operator fun Int.plus(counter: Counter): Counter {
    return Counter(this + counter.value)
}

val counter1 = Counter(5)
val counter2 = Counter(7)

val plus: Counter = counter1 + counter2
val plus: Counter = 4 + counter1
val plus: Counter = counter1 + 4
val minus: Counter = counter1 - counter2
val compareTo: Boolean = counter1 > counter2

Context Receivers

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

class PrintingScope(
    val separator: String = "________________"
)

context(PrintingScope)
fun <K, V> Map<K, V>.customPrint() {
    forEach { (k, v) ->
        println("K: $k")
        println("V: $v")
        println(separator)
    }
}

val mapping = mapOf("a" to 1, "b" to 2, "c" to 3)
val scope = PrintingScope()

with(scope) {
    mapping.customPrint()
}

Type-safe builders (DSL)

Типобезопасные статически-типизированные билдеры

data class User(
    var location: Location? = null
)

data class Location(
    var long: String = ""
) {
    fun long(lambda: () -> String) {
        this.long = lambda()
    }
}

fun user(lambda: User.() -> Unit): User {
    val user = User()
    user.apply(lambda)
    return user
}

fun location(lambda: Location.() -> Unit): Location {
    val location = Location()
    location.apply(lambda)
    return location
}

val user = user {
    location = location {
        long { "30.0" }
    }
}

Интерфейсы только с одним абстрактным методом называются функциональными интерфейсами или Single Abstract Method (SAM) интерфейсами. Функциональный интерфейс может иметь несколько неабстрактных членов, но только один абстрактный. Для функциональных интерфейсов вы можете использовать SAM преобразования, которые помогают сделать ваш код более лаконичным и читаемым, используя лямбда-выражения.

fun interface IntPredicate {
   fun accept(i: Int): Boolean
}

// без преобразования
val isEven = object : IntPredicate {
   override fun accept(i: Int): Boolean {
       return i % 2 == 0
   }
}

// с преобразованием
val isEven = IntPredicate { it % 2 == 0 }

fun main() {
   println("Is 7 even? - ${isEven.accept(7)}")
}

Type Aliases

typealias
Псевдонимы типов. Предоставляют альтернативные имена для существующих типов. Если имя типа слишком длинное, можно создать другое более короткое и использовать вместо него
• nested and local type aliases are not supported
• к typealias можно применять модификаторы доступа public, private, internal

typealias OnClick = (view: View) -> Unit

class A {
    inner class Inner
}

typealias AInner = A.Inner

typealias Predicate<T> = (T) -> Boolean

fun foo(p: Predicate<Int>) = p(42)

fun main() {
    val f: (Int) -> Boolean = { it > 0 }
    println(foo(f)) // prints "true"

    val p: Predicate<Int> = { it > 0 }
    println(listOf(1, -2).filter(p)) // prints "[1]"
}

Annotations

@JvmStatic
Сообщает компилятору, что метод является статическим и может использоваться в коде Java

@JvmOverloads
Чтобы использовать значения по умолчанию, переданные в качестве аргумента в коде Kotlin из кода Java

@JvmField
Для доступа к полям класса Kotlin из кода Java без использования каких-либо геттеров и сеттеров

@JvmInline
Указывает для JVM, что класс является value-классом

Math

Round a number

val value: Double = 3.14123456789
val roundedValue: Double = value
    .toBigDecimal()
    .setScale(2, RoundingMode.HALF_EVEN)
    .toDouble() // 3.14