Доступ к готовым решениям

Переход в группу "Пользователь"

300.00
Одноразовый платёж
Быстрый переход в группу "Пользователи", без надобности написания постов и ожидания.

Покупка дает возможность:
Быть полноправным участником форума
Нормальное копирование кода
Создавать темы
Скачивать файлы
Доступ к архиву Pawno-Info

Руководство по языку Kotlin

Alex_Bardakov

Изучающий
Пользователь
Регистрация
18 Фев 2015
Сообщения
542
Лучшие ответы
0
Репутация
106
Kotlin представляет современный, статически типизированный и один из самых быстроразвивающихся языков программирования, созданный и развиваемый компанией JetBrains. Kotlin можно использовать для создания самых разных приложений. Это и приложения для мобильных устройств - Android, iOS. Причем Kotlin позволяет писать кроссплатформенный код, который будет применяться на всех платформах. Это и веб-приложения, причем как серверные приложения, которые отрабатывают на стороне на стороне сервера - бекэнда, так и браузерные клиентские приложения - фронтенд. Kotlin также можно применять для создания десктопных приложений, для Data Science и так далее.

Таким образом, круг платформ, для которых можно создавать приложения на Kotlin, чрезвычайно широк - Windows, Linux, Mac OS, iOS, Android.

Самым популярным направлением, где применяется Kotlin, является прежде всего разработка под ОС Android. Причем настолько популярным, что компания Google на конференции Google I/O 2017 провозгласила Kotlin одним из официальных языков для разработки под Android (наряду с Java и C++), а инструменты по работе с данным языком были по умолчанию включены в функционал среды разработки Android Studio начиная с версии 3.0.

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

Первая версия языка вышла 15 февраля 2016 года. Хотя сама разработка языка велась с 2010 года. Текущей версией языка на данный момент является версия 1.5, которая вышла 5 мая 2021 года.

Kotlin испытал влияние многих языков: Java, Scala, Groovy, C#, JavaScript, Swift и позволяет писать программы как в объектно-ориентированном, так и в функциональном стиле. Он имеет ясный и понятный синтаксис и довольно легок для обучения.

Но Kotlin - это не просто очередной язык программирования. На сегодняшний день это целая экосистема:



Ядро этой экосистемы - Common Kotlin, которое включает в себя собственно язык, основные библиотеки и базовые инструменты для построения программ.

Для взаимодействия с конкретной платформой имеются предназначенные для этой платформы версия Kotlin: Kotlin/JVM, Kotlin/JS и Kotlin/Native. Эти специфические версии представляют расширения для языка Kotlin, а также специфичные для конкретной платформы бибилиотеки и инструменты разработки.

В будущем вся эта экосистема будет объединена в единую платформу Kotlin Multiplatform, которая на данный момент находится в альфа-версии.

Также стоит отметить, что Kotin развивается как opensource, исходный код проекта можно посмотреть в репозитории на github.
Создадим первую программу на языке Kotlin. Что для этого необходимо? Для набора кода программы понадобится текстовый редактор. Это может быть любой тестовый редактор, например, Notepad++ или Visual Studio Code. И для компиляции программы необходим компилятор.

Кроме того, необходимо установить JDK (Java Development Kit). Загрузить пакеты JDK.

Загрузить компилятор непосредственно для самого языка Kotlin можно по адресу GitHub. В самом низу страницы мы можем найти общую версию компилятора, версии компилятора Kotlin/Native для разных операционных систем, а также исходный код. Загрузим файл kotlin-compiler-*.zip:



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



Теперь определим на жестком диске каталог для файлов с исходным кодом. Например, в моем случае каталог будет находиться по пути c:/kotlin. В этом каталоге создадим текстовый файл и переименуем его в app.kt. Расширение kt - это расширение файлов на языке Kotlin.

Далее определим в этом файле код, который будет выводить некоторое сообщение на консоль:
C++:
fun main(){
    println("Hello Kotlin")
}
Точкой входа в программу на Kotlin является функция main. Для определения функции применяется ключевое слово fun, после которого идет название функции - то есть main. Даннуя функция не принимает никаких параметров, поэтому после названия функции указываются пустые скобки.

Далее в фигурных скобках определяются собственно те действия, которые выполняет функция main. В данном случае внутри функции main выполняется другая функция - println(), которая выводит некоторое сообщение на консоль.



Откроем командную строку. Вначале с помощью команды cd перейдем к папке, где находится файл app.kt. Затем для компиляции программы введем следующую команду:

Bash:
c:\kotlinc\bin\kotlinc app.kt -include-runtime -d app.jar
В данном случае мы передаем компилятору c:\kotlin\bin\kotlinc для компиляции файл app.kt. (Чтобы не писать полный путь к компилятору, путь к нему можно добавить в переменную PATH в переменных среды). Далее с помощью параметра -include-runtime указывается, что создаваемый файл будет включать среду Kotlin. А параметр -d указывает, как будет называться создаваемый файл приложения, то есть в данном случае это будет app.jar.

После выполнения этой команды будет создан файл app.jar. Теперь запустим его на выполнение. Для этого введем команду:

Bash:
java -jar app.jar
В данном случае считается, что путь к JDK, установленном на компьютере, прописан в переменной PATH в переменных среды. Иначе вместо "java" придется писать полный путь к утилите java.

В итоге при запуске файла мы увидим на консоли строку "Hello Kotlin"

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

Так, в прошлой теме была определена следующая функция main:

Java:
fun main(){
    println("Hello Kotlin")
}
Определение функции main() (в принципе как и других функций в Kotlin) начинается с ключевого слова fun. По сути оно указывает, что дальше идет определение функции. После fun указывается имя функции. В данном случае это main.

После имени функции в скобках идет список параметров функции. Здесь функция main не принимает никаких параметров, поэтому после имени функции идут пустые скобки.

Все действия, которые выполняет функция, заключаются в фигурные скобки. В данном случае единственное, что делает функция main, - вывод на консоль некоторого сообщения с помощью другой встроенной функции println().

Стоит отметить, что до версии 1.3 в Kotlin функция main должна была принимать параметры:

Java:
fun main(args: Array<String>) {
    println("Hello Kotlin")
}
Параметр args: Array<String> представляет массив строк, через который в программу можно передать различные данные.

Начиная с версии 1.3 использовать это определение функции с параметрами необязательно. Хотя мы можем его использовать.
Основным строительным блоком программы на языке Kotlin являются инструкции (statement). Каждая инструкция выполняет некоторое действие, например, вызовы функций, объявление переменных и присвоение им значений. Например:

Java:
println("Hello Kotlin!");
Данная строка представляет встроенной функции println(), которая выводит на консоль, некоторое сообщение (в данном случае строку "Hello Kotlin!").

Стоит отметить, что в отличие от других похожих языков программирования, например, Java, в Kotlin не обязательно ставить после инструкции точку запятой. Каждая инструкция просто размещается на новой строке:

Java:
fun main(){
    println("Kotlin on Metanit.com")
    println("Hello Kotlin")
    println("Kotlin is a fun")
}
Тем не менее, если инструкции располагаются на одной строке, то чтобы их отделить друг от друга, надо указывать после инструкции точку с запятой:

Java:
fun main(){
    println("Kotlin on Metanit.com");println("Hello Kotlin");println("Kotlin is a fun")
}
Код программы может содержать комментарии. Комментарии позволяют понять смысл программы, что делают те или иные ее части. При компиляции комментарии игнорируются и не оказывают никакого влияния на работу приложения и на его размер.

В Kotlin есть два типа комментариев: однострочный и многострочный. Однострочный комментарий размещается на одной строке после двойного слеша //. А многострочный комментарий заключается между символами /* текст комментария */. Он может размещаться на нескольких строках. Например:

Java:
/*
    многострочный комментарий
    Функция main -
    точка входа в программу
*/
fun main(){         // начало функции main

    println("Hello Kotlin") // вывод строки на консоль
}                   // конец функции main
Для хранения данных в программе в Kotlin, как и в других языках программирования, применяются переменные. Переменная представляет именованный участок памяти, который хранит некоторое значение.

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

Формальное определение переменной:

JavaScript:
val|var имя_переменной: тип_переменной
Вначале идет слово val или var, затем имя переменной и через двоеточие тип переменной.

Например, определим переменную age:

Java:
val age: Int
То есть в данном случае объявлена переменная age, которая имеет тип Int. Тип Int говорит о том, что переменная будет содержать целочисленные значения.

После определения переменной ей можно присвоить значение:

Java:
fun main() {
    val age: Int
    age = 23
    println(age)
}
Для присвоения значения переменной используется знак равно. Затем мы можем производить с переменной различные операции. Например, в данном случае с помощью функции println() значение переменной выводится на консоль. И при запуске этой программы на консоль будет выведено число 23.

Присвоение значения переменной должно производиться только после ее объявления. И также мы можем сразу присвоить переменной начальное значение при ее объявлении. Такой прием называется инициализацией:

Java:
fun main() {
    val age: Int = 23
    println(age)
}
Однако обязательно надо присвоить переменной некоторое значение до ее использования:

Java:
fun main() {

    val age: Int
    println(age)// Ошибка, переменная не инициализирована
}
Выше было сказано, что переменные могут объявляться как с помощью слова val, так и с помощью слова var. В чем же разница между двумя этими способами?

С помощью ключевого слова val определяется неизменяемая переменная (immutable variable). То есть мы можем присвоить значение такой переменной только один раз, но изменить его после первого присвоения мы уже не сможем. Например, в следующем случае мы получим ошибку:

Java:
fun main() {
    val age: Int
    age = 23        // здесь норм - первое присвоение
    age = 56        // здесь ошибка - переопределить значение переменной нельзя
    println(age)
}
А у переменной, которая определена с помощью ключевого слова var мы можем многократно менять значения (mutable variable):

Java:
fun main() {
    var age: Int
    age = 23
    println(age)
    age = 56
    println(age)
}
Поэтому если не планируется изменять значение переменной в программе, то лучше определять ее с ключевым словом val.
В Kotlin все компоненты программы, в том числе переменные, представляют объекты, которые имеют определенный тип данных. Тип данных определяет, какой размер памяти может занимать объект данного типа и какие операции с ним можно производить. В Kotlin есть несколько базовых типов данных: числа, символы, строки, логический тип и массивы.
  • 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 байт
В последней версии Kotlin также добавлена поддержка для целочисленных типов без знака:
  • UByte: хранит целое число от 0 до 255 и занимает 1 байт

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

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

  • ULong: хранит целое число от 0 до 264-1 и занимает 8 байт
Объекты целочисленных типов хранят целые числа:

Java:
fun main(){

    val a: Byte = -10
    val b: Short = 45
    val c: Int = -250
    val d: Long = 30000
    println(a) // -10
    println(b) // 45
    println(c) // -250
    println(d) // 30000
}
Для передачи значений объектам, которые представляют беззнаковые целочисленные типы данных, после числа указывается суффикс U:

Java:
fun main(){

    val a: UByte = 10U
    val b: UShort = 45U
    val c: UInt = 250U
    val d: ULong = 30000U
    println(a) // 10
    println(b) // 45
    println(c) // 250
    println(d) // 30000
}
Кроме чисел в десятичной системе мы можем определять числа в двоичной и шестнадцатеричной системах.

Шестнадцатеричная запись числа начинается с 0x, затем идет набор символов от 0 до F, которые представляют число:

Java:
val address: Int = 0x0A1    // 161
println(address) // 161
Двоичная запись числа предваряется символами 0b, после которых идет последовательность из нулей и единиц:

Java:
val a: Int = 0b0101    // 5
val b: Int = 0b1011     // 11
println(a)      // 5
println(b)      // 11
Кроме целочисленных типов в Kotlin есть два типа для чисел с плавающей точкой, которые позволяют хранить дробные числа:
  • Float: хранит число с плавающей точкой от -3.4*1038 до 3.4*1038 и занимает 4 байта

  • Double: хранит число с плавающей точкой от ±5.0*10-324 до ±1.7*10308 и занимает 8 байта.
В качестве разделителя целой и дробной части применяется точка:

Java:
val height: Double = 1.78
val pi: Float = 3.14F
println(height)      // 1.78
println(pi)         // 3.14
Чтобы присвоить число объекту типа Float после числа указывается суффикс f или F.

Также тип Double поддерживает экспоненциальную запись:

Java:
val d: Double = 23e3
println(d)      // 23 000

val g: Double = 23e-3
println(g)      // 0.023
Тип Boolean может хранить одно из двух значений: true (истина) или false (ложь).

Java:
val a: Boolean = true
val b: Boolean = false
Символьные данные представлены типом Char. Он представляет отдельный символ, который заключается в одинарные кавычки.

Java:
val a: Char = 'A'
val b: Char = 'B'
val c: Char = 'T'
Также тип Char может представлять специальные последовательности, которые интерпретируются особым образом:
  • \t: табуляция

  • \n: перевод строки

  • \r: возврат каретки

  • \': одинарная кавычка

  • \": двойная кавычка

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

Java:
fun main() {

    val name: String = "Eugene"

    println(name)
}
Строка может содержать специальные символы или эскейп-последовательности. Например, если необходимо вставить в текст перевод на другую строку, можно использовать эскейп-последовательность \n:

val text: String = "SALT II was a series of talks between United States \n and Soviet negotiators from 1972 to 1979"

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

Java:
fun main() {

    val text: String = """
                        SALT II was a series of talks between United States
                        and Soviet negotiators from 1972 to 1979.
                        It was a continuation of the SALT I talks.
                    """
    println(text)
}
Шаблоны строк (string templates) представляют удобный способ вставки в строку различных значений, в частности, значений переменных. Так, с помощью знака доллара $ мы можем вводить в строку значения различных переменных:

Java:
fun main() {

    val firstName = "Tom"
    val lastName = "Smith"
    val welcome = "Hello, $firstName $lastName"
    println(welcome)    // Hello, Tom Smith
}
В данном случае вместо $firstName и $lastName будут вставляться значения этих переменных. При этом переменные необязательно должны представлять строковый тип:

Java:
val name = "Tom"
val age = 22
val userInfo = "Your name: $name  Your age: $age"
Kotlin позволяет выводить тип переменной на основании данных, которыми переменная инициализируется. Поэтому при инициализации переменной тип можно опустить:

Java:
val age = 5
В данном случае компилятор увидит, что переменной присваивается значение типа Int, поэтому переменная age будет представлять тип Int.

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

Java:
val name = "Tom"
Любые целые числа, воспринимаются как данные типа Int.

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

Java:
val sum = 45L
Если надо указать, что объект представляет беззнаковый тип, то применяется суффикс u или U:

Java:
val sum = 45U
Аналогично все числа с плавающей точкой (которые содержат точку в качестве разделителя целой и дробной части) рассматриваются как числа типа Double:

Java:
val height = 1.78
Если мы хотим указать, что данные будут представлять тип Float, то необходимо использовать суффикс f или F:

Java:
val height = 1.78F
Однако нельзя сначала объявить переменную бз указания типа, а потом где-то в программе присвоить ей какое-то значение:

Java:
val age     // Ошибка, переменная не инициализирована
age = 5
Тип данных ограничивает набор значений, которые мы можем присвоить переменной. Например, мы не можем присвоить переменной типа Double строку:

Java:
val height: Double = "1.78"
И после того, как тип переменной установлен, он не может быть изменен:

Java:
fun main() {

    var height: String = "1.78"
    height = 1.81       // !Ошибка - переменная height хранит только строки
    println(height)
}
Однако в Kotlin также есть тип Any, который позволяет присвоить переменной данного типа любое значение:

Java:
fun main() {

    var name: Any = "Tom"
    println(name)   // Tom
    name = 6758
    println(name)   // 6758
}
Для вывода информации на консоль в Kotlin есть две встроенные функции:

Java:
print()
println()
Обе эти функции принимают некоторый объект, который надо вывести на консоль, обычно это строка. Различие между ними состоит в том, что функция println() при выводе на консоль добавляет перевод на новую строку:

Java:
fun main() {

    print("Hello ")
    print("Kotlin ")
    print("on Metanit.com")
    println()
    println("Kotlin is a fun")
}
Причем функция println() необязательно должна принимать некоторое значения. Так, здесь применяется пустой вызов функции, который просто перевод консольный вывод на новую строку:

Java:
println()
Консольный вывод программы:

Код:
Hello Kotlin on Metanit.com
Kotlin is a fun
Для ввода с консоли применяется встроенная функция readLine(). Она возвращает введенную строку. Стоит отметить, что результат этой функции всегда представляет объект типа String. Соответственно введеную строку мы можем передать в переменную типа String:


Java:
fun main() {

    print("Введите имя: ")
    val name = readLine()

    println("Ваше имя: $name")
}
Здесь сначала выводится приглашение к вводу данных. Далее введенное значение передается в переменную name. Результат работы программы:

Код:
Введите имя: Евгений
Ваше имя: Евгений
Подобным образом можно вводить разные данные:

Java:
fun main() {

    print("Введите имя: ")
    val name = readLine()
    print("Введите email: ")
    val email = readLine()
    print("Введите адрес: ")
    val address = readLine()

    println("Ваше имя: $name")
    println("Ваш email: $email")
    println("Ваш адрес: $address")
}
Kotlin поддерживает базовые арифметические операции:
  • + (сложение): возвращает сумму двух чисел.
    Java:
    val x = 5
    val y = 6
    val z = x + y
    println(z) // z = 11
  • - (вычитание): возвращает разность двух чисел.
    Java:
    val x = 5
    val y = 6
    val z = x - y  // z = -1
  • * (умножение): возвращает произведение двух чисел.
    Код:
    val x = 5
    val y = 6
    val z = x * y  // z = 30
  • / (деление): возвращает частное двух чисел.
    Java:
    val x = 60
    val y = 10
    val z = x / y  // z = 6
При этом если в операции деления оба операнда представляют целые числа, то результатом тоже будет целое число, а если в процессе деления образовалась дробная часть, то она отбрасывается:

Java:
fun main() {

    val x = 11
    val y = 5
    val z = x / y  // z =2
    println(z)// 2
}
Так в данном случае, хотя если согласно стандартной математике разделить 11 на 5, то получится 2.2. Однако поскольку оба операнда представляют целочисленный тип, а именно тип Int, то дробная часть - 0.2 отрабрасывается, поэтому результатом будет число 2, а переменная z будет представлять тип Int.

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

Java:
fun main() {

    val x = 11
    val y = 5.0
    val z = x / y   // z =2.2
    println(z)      // 2.2
}
В данном случае переменная y представляет тип Double, поэтому результатом деления будет число 2.2, а переменная z также будет представлять тип Double

  • %: возвращает остаток от целочисленного деления двух чисел
    Java:
    val x = 65
    val y = 10
    val z = x % y // z = 5
  • ++ (инкремент): увеличивает значение на единицу.
    Java:
    //Префиксный инкремент возвращает увеличенное значение:var x = 5
    val y = ++x
    println(x) // x = 6
    println(y) // y = 6
    Постфиксный инкремент возвращает значение до увеличения на единицу:
    Java:
    var x = 5
    val y = x++
    println(x) // x = 6
    println(y) // y = 5[CODE]
    [*]-- (декремент): уменьшает значение на единицу.[CODE=java]//Префиксный декремент возвращает уменьшенное значение:var x = 5
    val y = --x
    println(x) // x = 4
    println(y) // y = 4
    Постфиксный декремент возвращает значение до уменьшения на единицу:
    Код:
    var x = 5val y = x--
    println(x) // x = 4
    println(y)      // y = 5
Также есть ряд операций присвоения, которые сочетают арифметические операции и присвоение:
  • +=: присваивание после сложения. Присваивает левому операнду сумму левого и правого операндов: A += B эквивалентно A = A + B
  • -=: присваивание после вычитания. Присваивает левому операнду разность левого и правого операндов: A -= B эквивалентно A = A - B
  • *=: присваивание после умножения. Присваивает левому операнду произведение левого и правого операндов: A *= B эквивалентно A = A * B
  • /=: присваивание после деления. Присваивает левому операнду частное левого и правого операндов: A /= B эквивалентно A = A / B
  • %=: присваивание после деления по модулю. Присваивает левому операнду остаток от целочисленного деления левого операнда на правый: A %= B эквивалентно A = A % B
Ряд операций выполняется над двоичными разрядами числа. Здесь важно понимать, как выглядит двоичное представление тех или иных чисел. В частности, число 4 в двоичном виде - 100, а число 15 - 1111.

Есть следующие поразрядные операторы (они применяются только к данным типов Int и Long):
  • shl: сдвиг битов числа со знаком влево
    Java:
    val z = 3 shl 2 // z = 11 << 2 = 1100
    [/LIST]
    println(z) // z = 12
    val d = 0b11 shl 2
    println(d) // d = 12
    В данном случае число сдвигается на два разряда влево, поэтому справа число в двоичном виде дополняется двумя нулями. То есть в двоичном виде 3 представляет 11. Сдвигаем на два разряда влево (дополняем справа двумя нулями) и получаем 1100, то есть в десятичной системе число 12.
  • shr: сдвиг битов числа со знаком вправо
    Java:
    val z = 12 shr 2 // z = 1100 >> 2 = 11println(z) // z = 3
    val d = 0b1100 shr 2
    println(d)          // d = 3
    Число 12 сдвигается на два разряда вправо, то есть два числа справа факически отбрасываем и получаем число 11, то есть 3 в десятичой системе.
  • ushr: сдвиг битов беззнакового числа вправо
    Java:
    val z = 12 ushr 2     // z = 1100 >> 2 = 11println(z)          // z = 3
  • and: побитовая операция AND (логическое умножение или конъюнкция). Эта операция сравнивает соответствующие разряды двух чисел и возвращает единицу, если эти разряды обоих чисел равны 1. Иначе возвращает 0.
    Java:
    val x = 5 // 101val y = 6 // 110
    val z = x and y // z = 101 & 110 = 100
    println(z) // z = 4
    
    val d = 0b101 and 0b110
    println(d)          // d = 4
  • or: побитовая операция OR (логическое сложение или дизъюнкция). Эта операция сравнивают два соответствуюших разряда обоих чисел и возвращает 1, если хотя бы один разряд равен 1. Если оба разряда равны 0, то возвращается 0.
    Java:
    val x = 5 // 101val y = 6 // 110
    val z = x or y // z = 101 | 110 = 111
    println(z) // z = 7
    
    val d = 0b101 or 0b110
    println(d)          // d = 7
  • xor: побитовая операция XOR. Сравнивает два разряда и возвращает 1, если один из разрядов равен 1, а другой равен 0. Если оба разряда равны, то возвращается 0.
    Java:
    val x = 5 // 101val y = 6 // 110
    val z = x xor y // z = 101 ^ 110 = 011
    println(z) // z = 3
    
    val d = 0b101 xor 0b110
    println(d)          // d = 3
  • inv: логическое отрицание или инверсия - инвертирует биты числа
    Java:
    val b = 11 // 1011val c = b.inv()
    println(c)      // -12
Условные выражения представляют некоторое условие, которое возвращает значение типа Boolean: либо true (если условие истинно), либо false (если условие ложно).

  • > (больше чем): возвращает true, если первый операнд больше второго. Иначе возвращает false
    Java:
    val a = 11
    val b = 12
    val c : Boolean = a > b
    println(c) // false - a меньше чем b
    
    val d = 35 > 12
    println(d) // true - 35 больше чем 12
  • < (меньше чем): возвращает true, если первый операнд меньше второго. Иначе возвращает false
    Java:
    val a = 11
    val b = 12
    val c = a < b // true
    
    val d = 35 < 12  // false
  • >= (больше чем или равно): возвращает true, если первый операнд больше или равен второму
    Java:
    val a = 11
    val b = 12
    val c = a >= b // false
    val d = 11 >= a     // true
  • <= (меньше чем или равно): возвращает true, если первый операнд меньше или равен второму.
    Java:
    val a = 11
    val b = 12
    val c = a <= b // true
    val d = 11 <= a     // false
  • == (равно): возвращает true, если оба операнда равны. Иначе возвращает false
    Java:
    val a = 11
    val b = 12
    val c = a == b // false
    val d = b == 12     // true
  • != (не равно): возвращает true, если оба операнда НЕ равны
    Java:
    val a = 11
    val b = 12
    val c = a != b // true
    val d = b != 12     // false
Операндами в логических операциях являются два значения типа Boolean. Нередко логические операции объединяют несколько операций отношения:
  • and: возвращает true, если оба операнда равны true.
    Код:
    val a = trueval b = false
    val c = a and b // false
    val d = (11 >= 5) and (9 < 10) // true
    println(c)
    println(d)
  • or: возвращает true, если хотя бы один из операндов равен true.
    Java:
    val a = true
    val b = false
    val c = a or b // true
    val d = (11 < 5) or (9 > 10)     // false
  • xor: возвращает true, если только один из операндов равен true. Если операнды равны, возвращается false
    Код:
    val a = true
    val b = false
    val c = a xor b // true
    val d = a xor (90 > 10)      // false
  • !: возвращает true, если операнд равен false. И, наоборот, если операнд равен true, возвращается false.
    Java:
    val a = true
    val b = !a // false
    val c = !b // true
    В качестве альтернативы оператору ! можно использовать метод not():
  • in: возвращает true, если операнд имеется в некоторой последовательности.
    Java:
    val a = 5
    val b = a in 1..6 // true - число 5 входит в последовательность от 1 до 6
    
    val c = 4
    val d = c in 11..15 // false - число 4 НЕ входит в последовательность от 11 до 15
    Выражение 1..6 создает последовательность чисел от 1 до 6. И в данном случае оператор in проверяет, есть ли значение переменной a в этой последовательности. Поскольку значение переменной a имеется в данной последовательности, то возвращается true.

    А выражение 11..15 создает последовательность чисел от 11 до 15. И поскольку значение переменной с в эту последовательность не входит, поэтому возвращается false.

    Если нам, наоборот, хочется возвращать true, если числа нет в указанной последовательности, то можно применить комбинацию операторов !in:
    Java:
    val a = 8
    val b = a !in 1..6      // true - число 8 не входит в последовательность от 1 до 6
Условные конструкции позволяют направить выполнение программы по одному из путей в зависимости от условия.
Конструкция if принимает условие, и если это условие истинно, то выполняется последующий блок инструкций.

Java:
val a = 10
if(a == 10) {

    println("a равно 10")
}
В данном случае в конструкции if проверяется истинность выражения a == 10, если оно истинно, то выполняется последующий блок кода в фигурных скобках, и на консоль выводится сообщение "a равно 10". Если же выражение ложно, тогда блок кода не выполняется.

Если необходимо задать альтернативный вариант, то можно добавить блок else:

Java:
val a = 10
if(a == 10) {
    println("a равно 10")
}
else{
    println("a НЕ равно 10")
}
Таким образом, если условное выражение после оператора if истинно, то выполняется блок после if, если ложно - выполняется блок после else.

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

Java:
val a = 10
if(a == 10)
    println("a равно 10")
else
    println("a НЕ равно 10")
Если необходимо проверить несколько альтернативных вариантов, то можно добавить выражения else if:

Java:
val a = 10
if(a == 10) {
    println("a равно 10")
}
else if(a == 9){
    println("a равно 9")
}
else if(a == 8){
    println("a равно 8")
}
else{
    println("a имеет неопределенное значение")
}
Стоит отметить, что конструкция if может возвращать значение. Например, найдем максимальное из двух чисел:

Java:
val a = 10
val b = 20
val c = if (a > b) a else b

println(c)  // 20
Если при определении возвращаемого значения надо выполнить еще какие-нибудь действия, то можно заключить эти действия в блоки кода:

Java:
val a = 10
val b = 20
val c = if (a > b){
    println("a = $a")
    a
} else {
    println("b = $b")
    b
}
В конце каждого блока указывается возвращаемое значение.
Конструкция when проверяет значение некоторого объекта и в зависимости от его значения выполняет тот или иной код. Конструкция when аналогична конструкции switch в других языках. Формальное определение:

Java:
when(объект){

    значение1 -> действия1
    значение2 -> действия2

    значениеN -> действияN
}
Если значение объекта равно одному из значений в блоке кода when, то выполняются соответствующие действия, которые идут после оператора -> после соответствующего значения.

Например:

Java:
fun main() {

    val isEnabled = true
    when(isEnabled){
        false -> println("isEnabled off")
        true -> println("isEnabled on")
    }
}
Здесь в качестве объекта в конструкцию when передается переменная isEnabled. Далее ее значение по порядку сравнивается со значениями в false и true. В данном случае переменная isEnabled равна true, поэтому будет выполняться код

println("isEnabled on")
В примере выше переменная isEnabled имела только два возможных варианта: true и false. Однако чаще бывают случаи, когда значения в блоке when не покрывают все возможные значения объекта. Дополнительное выражение else позволяет задать действия, которые выполняются, если объект не соответствует ни одному из значений. Например:

Код:
val a = 30
when(a){
    10 -> println("a = 10")
    20 -> println("a = 20")
    else -> println("неопределенное значение")
}
То есть в данном случае если переменная a равна 30, поэтому она не соответствует ни одному из значений в блоке when. И соответственно будут выполняться инструкции из выражения else.

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

Java:
var a = 10
when(a){
    10 -> {
        println("a = 10")
        a *= 2
    }
    20 -> {
        println("a = 20")
        a *= 5
    }
    else -> { println("неопределенное значение")}
}
println(a)
Можно определить одни и те же действия сразу для нескольких значений. В этом случае значения перечисляются через запятую:

Java:
val a = 10
when(a){
    10, 20 -> println("a = 10 или a = 20")
    else -> println("неопределенное значение")
}
Также можно сравнивать с целым диапазоном значений с помощью оператора in:

Java:
val a = 10
when(a){
    in 10..19 -> println("a в диапазоне от 10 до 19")
    in 20..29 -> println("a в диапазоне от 20 до 29")
    !in 10..20 -> println("a вне диапазона от 10 до 20")
    else -> println("неопределенное значение")
}
Если оператор in позволяет узнать, есть ли значение в определенном диапазоне, то связка операторов !in позволяет проверить отсутствие значения в определенной последовательности.
Выражение в when также может сравниваться с динамически вычисляемыми значениями:

Java:
fun main() {

    val a = 10
    val b = 5
    val c = 3
    when(a){
        b - c -> println("a = b - c")
        b + 5 -> println("a = b + 5")
        else -> println("неопределенное значение")
    }
}
Так, в данном случае значение переменной a сравнивается с результатом операций b - c и b + 5.

Кроме того, when также может может принимать динамически вычисляемый объект:

Java:
fun main() {

    val a = 10
    val b = 20
    when(a + b){
        10 -> println("a + b = 10")
        20 -> println("a + b = 20")
        30 -> println("a + b = 30")
        else -> println("Undefined")
    }
}
Можно даже определять переменные, которые будут доступны внутри блока when:

Java:
fun main() {

    val a = 10
    val b = 26
    when(val c = a + b){
        10 -> println("a + b = 10")
        20 -> println("a + b = 20")
        else -> println("c = $c")
    }
}
Причем в принципе нам необязатльно вообще сравнивать значение какого-либо объекта. Конструкция when аналогично конструкции if..else просто может поверять набор условий и если одно из условий возвращает true, то выполнять соответствующий набор действий:

Java:
fun main() {

    val a = 15
    val b = 6
    when{
        (b > 10) -> println("b больше 10")
        (a > 10) -> println("a больше 10")
        else -> println("и a, и b меньше или равны 10")
    }
}
Как и if конструкция when может возвращать значение. Возвращаемое значение указывается после оператора ->:

Java:
val sum = 1000

val rate = when(sum){
    in 100..999 -> 10
    in 1000..9999 -> 15
    else -> 20
}
println(rate)       // 15
Таким образом, если значение переменной sum располагается в определенном диапазоне, то возвращается то значение, которое идет после стрелки.
Циклы представляют вид управляющих конструкций, которые позволяют в зависимости от определенных условий выполнять некоторое действие множество раз.
Цикл for пробегается по всем элементам коллекции. В этом плане цикл for в Kotlin эквивалентен циклу for-each в ряде других языков программирования. Его формальная форма выглядит следующим образом:

Java:
for(переменная in последовательность){
    выполняемые инструкции
}
Например, выведем все квадраты чисел от 1 до 9, используя цикл for:

Java:
for(n in 1..9){
    print("${n * n} \t")
}
В данном случае перебирается последовательность чисел от 1 до 9. При каждом проходе цикла (итерации цикла) из этой последовательности будет извлекаться элемент и помещаться в переменную n. И через переменную n можно манипулировать значением элемента. То есть в данном случае мы получим следующий консольный вывод:

Код:
1     4     9     16     25     36     49     64     81
Циклы могут быть вложенными. Например, выведем таблицу умножения:

Java:
for(i in 1..9){
    for(j in 1..9){
        print("${i * j} \t")
    }
    println()
}
В итоге на консоль будет выведена следующая таблица умножения:

Код:
1     2     3     4     5     6     7     8     9
2     4     6     8     10     12     14     16     18
3     6     9     12     15     18     21     24     27
4     8     12     16     20     24     28     32     36
5     10     15     20     25     30     35     40     45
6     12     18     24     30     36     42     48     54
7     14     21     28     35     42     49     56     63
8     16     24     32     40     48     56     64     72
9     18     27     36     45     54     63     72     81
Цикл while повторяет определенные действия пока истинно некоторое условие:

Java:
var i = 10
while(i > 0){
    println(i*i)
    i--;
}
Здесь пока переменная i больше 0, будет выполняться цикл, в котором на консоль будет выводиться квадрат значения i.

В данном случае вначале проверяется условие (i > 0) и если оно истинно (то есть возвращает true), то выполняется цикл. И вполне может быть ситуация, когда к началу выполнения цикла условие не будет выполняться. Например, переменная i изначально меньше 0, тогда цикл вообще не будет выполняться.

Но есть и другая форма цикла while - do..while:

Java:
var i = -1
do{
    println(i*i)
    i--;
}
while(i > 0)
В данном случае вначале выполняется блок кода после ключевого слова do, а потом оценивается условие после while. Если условие истинно, то повторяется выполнение блока после do. То есть несмотря на то, что в данном случае переменная i меньше 0 и она не соответствует условию, тем не менее блок do выполнится хотя бы один раз.
Иногда при использовании цикла возникает необходимость при некоторых условиях не дожидаться выполнения всех инструкций в цикле, перейти к новой итерации. Для этого можно использовать оператор continue:

Java:
for(n in 1..8){
    if(n == 5) continue;
    println(n * n)
}
В данном случае когда n будет равно 5, сработает оператор continue. И последующая инструкция, которая выводит на консоль квадрат числа, не будет выполняться. Цикл перейдет к обработке следующего элемента в массиве

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

Java:
for(n in 1..5){
    if(n == 5) break;
    println(n * n)
}
В данном случае когда n окажется равен 5, то с помощью оператора break будет выполнен выход из цикла. Цикл полностью завершится.
Диапазон представляет набор значений или неокторый интервал. Для создания диапазона применяется оператор ..:

Java:
val range = 1..5    // диапазон [1, 2, 3, 4, 5]
Этот оператор принимает два значения - границы диапазона, и все элементы между этими значениями (включая их самих) составляют диапазон.

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

Java:
val range =  "a".."d"
Оператор .. позволяет создать диапазон по нарастающей, где каждый следующий элемент будет больше предыдущего. С помощью специальной функции downTo можно построить диапазон в обратном порядке:

Java:
val range1 =  1..5      // 1 2 3 4 5
val range2 =  5 downTo 1    // 5 4 3 2 1
Еще одна специальная функция step позволяет задать шаг, на который будут изменяться последующие элементы:

Java:
val range1 = 1..10 step 2           // 1 3 5 7 9
val range2 = 10 downTo 1 step 3     // 10 7 4 1
Еще одна функция until позволяет не включать верхнюю границу в диапазон:

Java:
val range1 = 1 until 9          // 1 2 3 4 5 6 7 8
val range2 = 1 until 9 step 2   // 1 3 5 7
С помощью специальных операторов можно проверить наличие или отсутствие элементов в диапазоне:
  • in: возвращает true, если объект имеется в диапазоне
  • !in: возвращает true, если объект отсутствует в диапазоне
Java:
fun main() {

    val range = 1..5

    var isInRange = 5 in range
    println(isInRange)      // true

    isInRange = 86 in range
    println(isInRange)      // false

    var isNotInRange = 6 !in range
    println(isNotInRange)   // true

    isNotInRange = 3 !in range
    println(isNotInRange)   // false
}
С помощью цикла for можно перебирать диапазон:
Java:
val range1 = 5 downTo 1
for(c in range1) print(c)   // 54321
println()

val range2 = 'a'..'d'
for(c in range2) print(c)   // abcd
println()

for(c in 1..9) print(c)     // 123456789
println()

for(c in 1 until 9) print(c)    // 12345678
println()

for(c in 1..9 step 2) print(c)  // 13579
Для хранения набора значений в Kotlin, как и в других языках программирования, можно использовать массивы. При этом массив может хранить данные только одного того же типа. В Kotlin массивы представлены типом Array.

При определении массива после типа Array в угловых скобках необходимо указать, объекты какого типа могут храниться в массиве. Например, определим массив целых чисел:

Java:
val numbers: Array<Int>
С помощью встроенной функции arrayOf() можно передать набор значений, которые будут составлять массив:

Java:
val numbers: Array<Int> = arrayOf(1, 2, 3, 4, 5)
То есть в данном случае в массиве 5 чисел от 1 до 5.

С помощью индексов мы можем обратиться к определенному элементу в массиве. Индексация начинается с нуля, то есть первый элемент будет иметь индекс 0. Индекс указывается в квадратных скобках:

Java:
val numbers: Array<Int> = arrayOf(1, 2, 3, 4, 5)
val n = numbers[1]  // получаем второй элемент  n=2
numbers[2] = 7      // переустанавливаем третий элемент
println("numbers[2] = ${numbers[2]}") // numbers[2] = 7
Также инициализировать массив значениями можно следующим способом:

Java:
val numbers = Array(3, {5}) // [5, 5, 5]
Здесь применяется конструктор класса Array. В этот конструктор передаются два параметра. Первый параметр указывает, сколько элементов будет в массиве. В данном случае 3 элемента. Второй параметр представляет выражение, которое генерирует элементы массива. Оно заключается в фигурные скобки. В данном случае в фигурных скобках стоит число 5, то есть все элементы массива будут представлять число 5. Таким образом, массив будет состоять из трех пятерок.

Но выражение, которое создает элементы массива, может быть и более сложным. Например:

Java:
var i = 1;
val numbers = Array(3, { i++ * 2}) // [2, 4, 6]
В данном случае элемент массива является результатом умножения переменной i на 2. При этом при каждом обращении к переменой i ее значение увеличивается на единицу.

Для перебора массивов можно применять цикл for:

Java:
fun main() {

    val numbers = arrayOf(1, 2, 3, 4, 5)
    for(number in numbers){
        print("$number \t")
    }
}
В данном случае переменная numbers представляет массив чисел. При переборе этого массива в цикле каждый его элемент оказывается в переменной number, значение которой, к примеру, можно вывести на консоль. Консольный вывод программы:

Код:
1  2  3  4  5
Подобным образом можно перебирать массивы и других типов:

Java:
fun main() {

    val people = arrayOf("Tom", "Sam", "Bob")
    for(person in people){
        print("$person \t")
    }
}
Консольный вывод программы:

Код:
Tom   Sam   Bob
Можно применять и другие типы циклов для перебора массива. Например, используем цикл while:

Java:
fun main() {

    val people = arrayOf("Tom", "Sam", "Bob")

    var i = 0
    while( i in people.indices){
        println(people[i])
        i++;
    }
}
Здесь определена дополнительная переменная i, которая представляет индекс элемента массива. У массива есть специальное свойство indices, которое содержит набор всех индексов. А выражение i in people.indices возвращает true, если значение переменной i входит в набор индексов массива.

В самом цикле по индексу обащаемся к элементу массива: println(people). И затем переходим к следующему индексу, увеличивая счетчик: i++.

То же самое мы могли написать с помощью цикла for:

Java:
for (i in people.indices) {
    println(people[i])
}
Как и в случае с последовательностью мы можем проверить наличие или отсутствие элементов в массиве с помощью операторов in и !in:

Java:
val numbers: Array<Int> = arrayOf(1, 2, 3, 4, 5)

println(4 in numbers)       // true
println(2 !in numbers)      // false
Для упрощения создания массива в Kotlin определены дополнительные типы BooleanArray, ByteArray, ShortArray, IntArray, LongArray, CharArray, FloatArray и DoubleArray, которые позволяют создавать массивы для определенных типов. Например, тип IntArray позволяет определить массив объектов Int, а DoubleArray - массив объектов Double:

Java:
val numbers: IntArray = intArrayOf(1, 2, 3, 4, 5)
val doubles: DoubleArray = doubleArrayOf(2.4, 4.5, 1.2)
Для определения данных для этих массивов можно применять функции, которые начинаются на название типа в нижнем регистре, например, int, и затем идет ArrayOf.

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

Java:
val numbers = IntArray(3, {5})
val doubles = DoubleArray(3, {1.5})
Выше рассматривались одномерные массивы, которые можно представить в виде ряда или строки значений. Но кроме того, мы можем использовать многомерные массивы. К примеру, возьмем двухмерный массив - то есть такой массив, каждый элемент которого в свою очередь сам является массивом. Двухмерный массив еще можно представить в виде таблицы, где каждая строка - это отдельный массив, а ячейки строки - это элементы вложенного массива.

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

Java:
val table: Array<Array<Int>> = Array(3, { Array(5, {0}) })
В данном случае двухмерный массив будет иметь три элемента - три строки. Каждая строка будет иметь по пять элементов, каждый из которых равен 0.

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

Java:
val table = Array(3, { Array(3, {0}) })
table[0] = arrayOf(1, 2, 3)     // первая строка таблицы
table[1] = arrayOf(4, 5, 6)     // вторая строка таблицы
table[2] = arrayOf(7, 8, 9)     // третья строка таблицы
Для обращения к элементам подмассивов двухмерного массива необходимы два индекса. По первому индексу идет получение строки, а по второму индексу - столбца в рамках этой строки:

Java:
val table = Array(3, { Array(3, {0}) })
table[0][1] = 6  // второй элемент первой строки
val n = table[0][1]     // n = 6
Используя два цикла, можно перебирать двухмерные массивы:

Java:
fun main() {

    val table: Array<Array<Int>> = Array(3, { Array(3, {0}) })
    table[0] = arrayOf(1, 2, 3)
    table[1] = arrayOf(4, 5, 6)
    table[2] = arrayOf(7, 8, 9)
    for(row in table){

        for(cell in row){
            print("$cell \t")
        }
        println()
    }
}
С помощью внешнего цикла for(row in table) пробегаемся по всем элементам двухмерного массива, то есть по строкам таблицы. Каждый из элементов двухмерного массива сам представляет массив, поэтому мы можем пробежаться по этому массиву и получить из него непосредственно те значения, которые в нем хранятся. В итоге на консоль будет выведено следующее:

Код:
1     2     3     
4     5     6     
7     8     9
Одним из строительных блоков программы являются функции. Функция определяет некоторое действие. В Kotlin функция объявляется с помощью ключевого слова fun, после которого идет название функции. Затем после названия в скобках указывается список параметров. Если функция возвращает какое-либо значение, то после списка параметров через запятую можно указать тип возвращаемого значения. И далее в фигурных скобках идет тело функции.

Java:
fun имя_функции (параметры) : возвращаемый_тип{
    выполняемые инструкции
}
Параметры необязательны.

Например, определим и вызовем функцию, которая просто выводит некоторую строку на консоль:

Java:
fun main() {

    hello() // вызов функции hello
    hello() // вызов функции hello
    hello() // вызов функции hello
}
// определение функции hello
fun hello(){
    println("Hello")
}
Функции можно определять в файле вне других функций или классов, сами по себе, как например, определяется функция main. Такие функции еще называют функциями верхнего уровня (top-level functions).

Здесь кроме главной функции main также определена функция hello, которая не принимает никаких параметров и ничего не возвращает. Она просто выводит строку на консоль.

Функция hello (и любая другая определенная функция, кроме main) сама по себе не выполняется. Чтобы ее выполнить, ее надо вызвать. Для вызова функции указывается ее имя (в данном случае "hello"), после которого идут пустые скобки.

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

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

Java:
fun main() {

    showMessage("Hello Kotlin")
    showMessage("Привет Kotlin")
    showMessage("Salut Kotlin")
}

fun showMessage(message: String){
    println(message)
}
Функция showMessage() принимает один параметр типа String. Поэтому при вызове функции в скобках необходимо передать значение для этого параметра: showMessage("Hello Kotlin"). Причем это значение должно представлять тип String, то есть строку. Значения, которые передаются параметрам функции, еще назвают аргументами.

Консольный вывод программы:

Код:
Hello Kotlin
Привет Kotlin
Salut Kotlin
Другой пример - функция, которая выводит данные о пользователе на консоль:

Java:
fun main() {

    displayUser("Tom", 23)
    displayUser("Alice", 19)
    displayUser("Kate", 25)
}
fun displayUser(name: String, age: Int){
    println("Name: $name   Age: $age")
}
Функция displayUser() принимает два параметра - name и age. При вызове функции в скобках ей передаются значения для этих параметров. При этом значения передаются параметрам по позиции и должны соответствовать параметрам по типу. Так как вначале идет параметр типа String, а потом параметр типа Int, то при вызове функции в скобках вначале передается строка, а потом число.
В примере выше при вызове функций showMessage и displayUser мы обязательно должны предоставить для каждого их параметра какое-то определенное значение, которое соответствует типу параметра. Мы не можем, к примеру, вызвать функцию displayUser, не передав ей аргументы для параметров, это будет ошибка:

Java:
displayUser()
Однако мы можем определить какие-то параметры функции как необязательные и установить для них значения по умолчанию:

Java:
fun displayUser(name: String, age: Int = 18, position: String="unemployed"){
    println("Name: $name   Age: $age  Position: $position")
}

fun main() {

    displayUser("Tom", 23, "Manager")
    displayUser("Alice", 21)
    displayUser("Kate")
}
В данном случае функция displayUser имеет три параметра для передачи имени, возраста и должности. Для первого параметр name значение по умолчанию не установлено, поэтому для него значение по-прежнему обязательно передавать значение. Два последующих - age и position являются необязательными, и для них установлено значение по умолчанию. Если для этих параметров не передаются значения, тогда параметры используют значения по умолчанию. Поэтому для этих параметров в принципе нам необязательно передавать аргументы. Но если для какого-то параметра определено значение по умолчанию, то для всех последующих параметров тоже должно быть установлено значение по умолчанию.

Консольный вывод программы

Код:
Name: Tom   Age: 23  Position: Manager
Name: Alice   Age: 21  Position: unemployed
Name: Kate   Age: 18  Position: unemployed
По умолчанию значения передаются параметрам по позиции: первое значение - первому параметру, второе значение - второму параметру и так далее. Однако, используя именованные аргументы, мы можем переопределить порядок их передачи параметрам:

Java:
fun main() {

    displayUser("Tom", position="Manager", age=28)
    displayUser(age=21, name="Alice")
    displayUser("Kate", position="Middle Developer")
}
При вызове функции в скобках мы можем указать название параметра и с помощью знака равно передать ему нужное значение.

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

Также если до обязательного параметра функции идут необязательные параметры, то для обязательного параметра значение передается по имени:

Java:
fun displayUser(age: Int = 18, name: String){
    println("Name: $name   Age: $age")
}
fun main() {

    displayUser(name="Tom", age=28)
    displayUser(name="Kate")
}
По умолчанию все параметры функции равносильны val-переменным, поэтому их значение нельзя изменить. Например, в случае следующей функции при компиляции мы получим ошибку:

Java:
fun double(n: Int){
    n = n * 2   // !Ошибка - значение параметра нельзя изменить
    println("Значение в функции double: $n")
}
Однако если параметр предствляет какой-то сложный объект, то можно изменять отдельные значения в этом объекте. Например, возьмем функцию, которая в качестве параметра принимает массив:

Java:
fun double(numbers: IntArray){
    numbers[0] = numbers[0] * 2
    println("Значение в функции double: ${numbers[0]}")
}

fun main() {

    var nums = intArrayOf(4, 5, 6)
    double(nums)
    println("Значение в функции main: ${nums[0]}")
}
Здесь функция double принимает числовой массив и увеличивает значение его первого элемента в два раза. Причем изменение элемента массива внутри функции приведет к тому, что также будет изменено значение элемента в том массиве, который передается в качестве аргумента в функцию, так как этот один и тот же массив. Консольный вывод:

Код:
Значение в функции double: 8
Значение в функции main: 8
Функция может принимать переменное количество параметров одного типа. Для определения таких параметров применяется ключевое слово vararg. Например, нам необходимо передать в функцию несколько строк, но сколько именно строк, мы точно не знаем. Их может быть пять, шесть, семь и т.д.:

Java:
fun printStrings(vararg strings: String){
    for(str in strings)
        println(str)
}
fun main() {

    printStrings("Tom", "Bob", "Sam")
    printStrings("Kotlin", "JavaScript", "Java", "C#", "C++")
}
Функция printStrings принимает неопределенное количество строк. В самой функции мы можем работать с параметром как с последовательностью строк, например, перебирать элементы последовательности в цикле и производить с ними некоторые действия.

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

Другой пример - подсчет суммы неопределенного количества чисел:

Java:
fun sum(vararg numbers: Int){
    var result=0
    for(n in numbers)
        result += n
    println("Сумма чисел равна $result")
}
fun main() {

    sum(1, 2, 3, 4, 5)
    sum(1, 2, 3, 4, 5, 6, 7, 8, 9)
}
Если функция принимает несколько параметров, то обычно vararg-параметр является последним.

Java:
fun printUserGroup(count:Int, vararg users: String){
    println("Count: $count")
    for(user in users)
        println(user)
}

fun main() {

    printUserGroup(3, "Tom", "Bob", "Alice")
}
Однако это необязательно, но если после vararg-параметра идут еще какие-нибудь параметры, то при вызове функции значения этим параметрам передаются через именованные аргументы:

Java:
fun printUserGroup(group: String, vararg users: String, count:Int){
    println("Group: $group")
    println("Count: $count")
    for(user in users)
        println(user)
}
fun main() {

    printUserGroup("KT-091", "Tom", "Bob", "Alice", count=3)
}
Здесь функция printUserGroup принимает три параметра. Значения параметрам до vararg-параметра передаются по позициям. То есть в данном случае "KT-091" будет представлять значение для параметра group. Последующие значения интерпретируются как значения для vararg-параметра вплоть до именнованных аргументов.
Оператор * (spread operator) (не стоит путать со знаком умножения) позволяет передать параметру в качестве значения элементы из массива:

Java:
fun changeNumbers(vararg numbers: Int, koef: Int){
    for(number in numbers)
        println(number * koef)
}
fun main() {

    val nums = intArrayOf(1, 2, 3, 4)
    changeNumbers(*nums, koef=2)
}
Обратите внимание на звездочку перед nums при вызове функции: changeNumbers(*nums, koef=2). Без применения данного оператора мы столкнулись бы с ошибкой, поскольку параметры функции представляют не массив, а неопределенное количество значений типа Int.
Функция может возвращать некоторый результат. В этом случае после списка параметров через двоеточие указывается возвращаемый тип. А в теле функции применяется оператор return, после которого указывается возвращаемое значение.

Например, определим функцию, которая возвращает сумму двух чисел:

Java:
fun sum(x:Int, y:Int): Int{
    
    return x + y
}
fun main() {

    val a = sum(4, 3)
    val b = sum(5, 6)
    val c = sum(6, 9)
    println("a=$a  b=$b  c=$c")
}
В объявлении функции sum после списка параметров через двоеточие указывается тип Int, который будет представлять тип возвращаемого значения:

Java:
fun sum(x:Int, y:Int): Int
В самой функции с помощью оператора return возвращаем полученное значение - результат операции сложения:

Java:
return x + y
Так как функция возвращает значение, то при ее вызове это значение можно присвоить переменной:

Java:
val a = sum(4, 3)
Если функция не возвращает какого-либо результата, то фактически неявно она возвращает значение типа Unit. Этот тип аналогичен типу void в ряде языков программирования, которое указывает, что функция ничего не возвращает. Например, следующая функция

Java:
fun hello(){
    println("Hello")
}
будет аналогична следующей:

Java:
fun hello() : Unit{
    println("Hello")
}
Формально мы даже можем присвоить результат такой функции переменной:

Java:
val d = hello()
val e = hello()
Однако практического смысла это не имеет, так как возвращаемое значение представляет объект Unit, который больше никак не применяется.

Если функция возвращает значение Unit, мы также можем использовать оператор return для возврата из функции:

Java:
fun checkAge(age: Int){
    if(age < 0 || age > 110){
        println("Invalid age")
        return
    }
    println("Age is valid")
}
fun main() {

    checkAge(-10)
    checkAge(10)
}
В данном случае если значение параметра age выходит за пределы диапазона от 0 до 110, то с помощью оператора return осуществляется выход из функции, и последующие инструкции не выполняются. При этом если функция возвращает значение Unit, то после оператора return можно не указывать никакого значения.
Однострочные функции (single expression function) используют сокращенный синтаксис определения функции в виде одного выражения. Эта форма позволяет опустить возвращаемый тип и оператор return.

Java:
fun имя_функции (параметры_функции) = тело_функции
Функция также определяется с помощью ключевого слова fun, после которого идет имя функции и список параметров. Но после списка параметров не указывается возвращаемый тип. Возвращаемый тип будет выводится компилятором. Далее через оператор присвоения = определяется тело функции в виде одного выражения.

Например, функция возведения числа в квадрат:

Java:
fun square(x: Int) = x * x

fun main() {

    val a = square(5)   // 25
    val b = square(6)   // 36
    println("a=$a  b=$b")
}
В данном случае функция square возводит число в квадрат. Она состоит из одного выражения x * x. Значение этого выражения и будет возвращаться функцией. При этом оператор return не используется.

Такие функции более лаконичны, более читабельны, но также опционально можно и указывать возвращаемый тип явно:

Java:
fun square(x: Int) : Int = x * x
Одни функции могут быть определены внутри других функций. Внутренние или вложенные функции еще называют локальными.

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

Например, у нас есть функция, которая сравнивает два возраста:

Java:
fun compareAge(age1: Int, age2: Int){

    fun ageIsValid(age: Int): Boolean{
        return age > 0 && age < 111
    }
    if( !ageIsValid(age1) || !ageIsValid(age2)) {
        println("Invalid age")
        return
    }

    when {
        age1 == age2 -> println("age1 == age2")
        age1 > age2 -> println("age1 > age2")
        age1 < age2 -> println("age1 < age2")
    }
}
fun main() {

    compareAge(20, 23)
    compareAge(-3, 20)
    compareAge(34, 134)
    compareAge(15, 8)
}
Однако извне могут быть переданы некорректные данные. Имеет ли смысл сравнивать возраст меньше нуля с другим? Очевидно нет. Для этой цели в функции определена локальная функция ageIsValid(), которая возвращает true, если возраст является допустимым. Больше в программе эта функция нигде не используется, поэтому ее можно сделать локальной.

При этом локальная может использоваться только в той функции, где она определена.

Причем в данном случае удобнее сделать локальную функцию однострочной:

Java:
fun compareAge(age1: Int, age2: Int){

    fun ageIsValid(age: Int)= age > 0 && age < 111

    if( !ageIsValid(age1) || !ageIsValid(age2)) {
        println("Invalid age")
        return
    }

    when {
        age1 == age2 -> println("age1 == age2")
        age1 > age2 -> println("age1 > age2")
        age1 < age2 -> println("age1 < age2")
    }
}
Перегрузка функций (function overloading) представляет определение нескольких функций с одним и тем же именем, но с различными параметрами. Параметры перегруженных функций могут отличаться по количеству, типу или по порядку в списке параметров.

Java:
fun sum(a: Int, b: Int) : Int{
    return a + b
}
fun sum(a: Double, b: Double) : Double{
    return a + b
}
fun sum(a: Int, b: Int, c: Int) : Int{
    return a + b + c
}
fun sum(a: Int, b: Double) : Double{
    return a + b
}
fun sum(a: Double, b: Int) : Double{
    return a + b
}
В данном случае для одной функции sum() определено пять перегруженных версий. Каждая из версий отличается либо по типу, либо количеству, либо по порядку параметров. При вызове функции sum компилятор в зависимости от типа и количества параметров сможет выбрать для выполнения нужную версию:

Java:
fun main() {

    val a = sum(1, 2)
    val b = sum(1.5, 2.5)
    val c = sum(1, 2, 3)
    val d = sum(2, 1.5)
    val e = sum(1.5, 2)
}
При этом при перегрузке не учитывает возвращаемый результат функции. Например, пусть у нас будут две следующие версии функции sum:

Java:
fun sum(a: Double, b: Int) : Double{
    return a + b
}
fun sum(a: Double, b: Int) : String{
    return "$a + $b"
}
Они совпадают во всем за исключением возвращаемого типа. Однако в данном случае мы сталкивамся с ошибкой, так как перегруженные версии должны отличаться именно по типу, порядку или количеству параметров. Отличие в возвращаемом типе не имеют значения.
В Kotlin все является объектом, в том числе и функции. И функции, как и другие объекты, имеют определенный тип. Тип функции определяется следующим образом:

Java:
(типы_параметров) -> возвращаемый_тип
Возьмем функцию которая не принимает никаких параметров и ничего не возвращает:

Java:
fun hello(){

    println("Hello Kotlin")
}
Она имеет тип

Java:
() -> Unit
Если функция не принимает параметров, в определении типа указываются пустые скобки. Если не указан возвращаемый тип, то фактически а в качестве типа возвращаемого значения применяется тип Unit.

Возьмем другую функцию:

Java:
fun sum(a: Int, b: Int): Int{
    return a + b
}
Эта функция принимает два параметра типа Int и возвращает значение типа Int, поэтому она имеет тип

Java:
(Int, Int) -> Int
Что дает нам знание типа функции? Используя тип функции, мы можем определять переменные и параметры других функций, которые будут представлять функции.
Переменная может представлять функцию. С помощью типа функции можно определить, какие именно функции переменная может представлять:

Java:
fun main() {

    val message: () -> Unit
    message = ::hello
    message()
}

fun hello(){
    println("Hello Kotlin")
}
Здесь переменная message представляет функцию с типом () -> Unit, то есть функцию без параметров, которая ничего не возвращает. Далее определена как раз такая функция - hello(), соответственно мы можем передать функцию hello переменной message.

Чтобы передать функцию, перед названием функции ставится оператор ::

Java:
message = ::hello
Затем мы можем обращаться к переменной message() как к обычной функции:

Java:
message()
Так как переменная message ссылается на функцию hello, то при вызове message() фактически будет вызываться функция hello().

При этом тип функции также может выводится исходя из присваемого переменной значения:

Java:
val message = ::hello // message имеет тип () -> Unit
Рассмотрим другой пример, когда переменная ссылается на функцию с параметрами:

Java:
fun main() {

    val operation: (Int, Int) -> Int = ::sum
    val result = operation(3, 5)
    println(result) // 8
}
fun sum(a: Int, b: Int): Int{
    return a + b
}
Переменная operation представляет функцию с типом (Int, Int) -> Int, то есть функцию с двумя параметрами типа Int и возвращаемым значением типа Int. Соответственно такой переменной мы можем присвоить функцию sum, которая соответствует этому типу.

Затем через имя переменной фактически можно обращаться к функции sum(), передавая ей значения для параметров и получая ее результат:

Java:
val result = operation(3, 5)
При этом динамически можно менять значение, главное чтобы оно соответствовало типу переменной:

Java:
fun main() {

    // operation указывает на функцию sum
    var operation: (Int, Int) -> Int = ::sum
    val result1 = operation(14, 5)
    println(result1) // 19

    // operation указывает на функцию subtract
    operation = ::subtract
    val result2 = operation(14, 5)
    println(result2) // 9

}
fun sum(a: Int, b: Int): Int{
    return a + b
}
fun subtract(a: Int, b: Int): Int{
    return a - b
}
Функции высокого порядка (high order function) - это функции, которые либо принимают функцию в качестве параметра, либо возвращают функцию, либо и то, и другое.
Чтобы функция могла принимать другую функцию через параметр, этот параметр должен представлять тип функции:

Java:
fun main() {

    displayMessage(::morning)
    displayMessage(::evening)
}
fun displayMessage(mes: () -> Unit){
    mes()
}
fun morning(){
    println("Good Morning")
}
fun evening(){
    println("Good Evening")
}
В данном случае функция displayMessage() через параметр mes принимает функцию типа () -> Unit, то есть такую функцию, которая не имеет параметров и ничего не возвращает.

Java:
fun displayMessage(mes: () -> Unit){
При вызове этой функции мы можем передать этому параметру функцию, которая соответствует этому типу:

Java:
displayMessage(::morning)
Рассмотрим пример параметра-функции, которая принимает параметры:

Java:
fun main() {

    action(5, 3, ::sum)         // 8
    action(5, 3, ::multiply)    // 15
    action(5, 3, ::subtract)    // 2
}

fun action (n1: Int, n2: Int, op: (Int, Int)-> Int){
    val result = op(n1, n2)
    println(result)
}
fun sum(a: Int, b: Int): Int{
    return a + b
}
fun subtract(a: Int, b: Int): Int{
    return a - b
}
fun multiply(a: Int, b: Int): Int{
    return a * b
}
Здесь функция action принимает три параметра. Первые два параметра - значения типа Int. А третий параметр представляет функцию, которая имеет тип (Int, Int)-> Int, то есть принимает два числа и возвращает некоторое число.

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

При вызове функции action мы можем передать для ее третьего параметра конкретную функцию, которая соответствует этому параметру по типу:

Java:
action(5, 3, ::sum)         // 8
action(5, 3, ::multiply)    // 15
action(5, 3, ::subtract)    // 2
В более редких случаях может потребоваться возвратить функцию из другой функции. В этом случае для функции в качестве возвращаемого типа устанавливается тип другой функции. А в теле функции возвращается лямбда выражение. Например:

Java:
fun main() {
    val action1 = selectAction(1)
    println(action1(8,5))    // 13

    val action2 = selectAction(2)
    println(action2(8,5))    // 3
}
fun selectAction(key: Int): (Int, Int) -> Int{
    // определение возвращаемого результата
    when(key){
        1 -> return ::sum
        2 -> return ::subtract
        3 -> return ::multiply
        else -> return ::empty
    }
}
fun empty (a: Int, b: Int): Int{
    return 0
}
fun sum(a: Int, b: Int): Int{
    return a + b
}
fun subtract(a: Int, b: Int): Int{
    return a - b
}
fun multiply(a: Int, b: Int): Int{
    return a * b
}
Здесь функция selectAction принимает один параметр - key, который представляет тип Int. В качестве возвращаемого типа у функции указан тип (Int, Int) -> Int. То есть selectAction будет возвращать некую функцию, которая принимает два параметра типа Int и возвращает объект типа Int.

В теле функции selectAction в зависимости от значения параметра key возвращается определенная функция, которая соответствует типу (Int, Int) -> Int.

Далее в функции main определяется переменная action1 хранит результат функции selectAction. Так как selectAction() возвращает функцию, то и переменная action1 будет хранить эту функцию. Затем через переменную action1 можно вызвать эту функцию.

Поскольку возвращаемая функция соответствует типу (Int, Int) -> Int, то при вызове в action1 необходимо передать два числа, и соответственно мы можем получить результат и вывести его на консоль.
Анонимные функции выглядят как обычные за тем исключением, что они не имеют имени. Анонимная функция может иметь одно выражение:

Java:
fun(x: Int, y: Int): Int = x + y
Либо может представлять блок кода:

Java:
fun(x: Int, y: Int): Int{
    return x + y
}
Анонимную функцию можно передавать в качестве значения переменной:

Java:
fun main() {

    val message = fun()=println("Hello")
    message()
}
Здесь переменной message передается анонимная функция fun()=println("Hello"). Эта анонимная функция не принимает параметров и просто выводит на консоль строку "Hello". Таким образом, переменная message будет представлять тип () -> Unit.

Далее мы можем вызывать эту функцию через имя переменной как обычную функцию: message().

Другой пример - анонимная функция с параметрами:

Java:
fun main() {

    val sum = fun(x: Int, y: Int): Int = x + y
    val result = sum(5, 4)
    println(result)     // 9
}
В данном случае переменной sum присваивается анонимная функция, которая принимает два параметра - два целых числа типа Int и возвращает их сумму.

Также через имя переменной мы можем вызвать эту анонимную функцию, передав ей некоторые значения для параметров и получить ее результат: val result = sum(5, 4)
Анонимную функцию можно передавать в функцию, если параметр соответствует типу этой функции:

Java:
fun main() {

    doOperation(9,5, fun(x: Int, y: Int): Int = x + y )     // 14
    doOperation(9,5, fun(x: Int, y: Int): Int = x - y)      // 4

    val op = fun(x: Int, y: Int): Int = x * y
    doOperation(9, 5, op)       // 45
}
fun doOperation(x: Int, y: Int, op: (Int, Int) ->Int){

    val result = op(x, y)
    println(result)
}
И также фунция может возвращать анонимную функцию в качестве результата:

Java:
fun main() {

    val action1 = selectAction(1)
    val result1 = action1(4, 5)
    println(result1)        // 9

    val action2 = selectAction(3)
    val result2 = action2(4, 5)
    println(result2)        // 20

    val action3 = selectAction(9)
    val result3 = action3(4, 5)
    println(result3)        // 0
}

fun selectAction(key: Int): (Int, Int) -> Int{
    // определение возвращаемого результата
    when(key){
        1 -> return fun(x: Int, y: Int): Int = x + y
        2 -> return fun(x: Int, y: Int): Int = x - y
        3 -> return fun(x: Int, y: Int): Int = x * y
        else -> return fun(x: Int, y: Int): Int = 0
    }
}
Здесь функция selectAction() в зависимости от переданного значения возвращает одну из четырех анонимных функций. Последняя анонимная функция fun(x: Int, y: Int): Int = 0 просто возвращает число 0.

При обращении к selectAction() переменная получит определенную анонимную функцию:

Java:
val action1 = selectAction(1)
То есть в данном случае переменная action1 хранит ссылку на функцию fun(x: Int, y: Int): Int = x + y
Лямбда-выражения представляют небольшие кусочки кода, которые выполняют некоторые действия. Фактически лямбды преставляют сокращенную запись функций. При этом лямбды, как и обычные и анонимные функции, могут передаваться в качестве значений переменным и параметрам функции.

Лямбда-выражения оборачиваются в фигурные скобки:

Java:
{println("hello")}
В данном случае лямбда-выражение выводит на консоль строку "hello".

Лямбда-выражение можно сохранить в обычную переменную и затем вызывать через имя этой переменной как обычную функцию.

Java:
fun main() {

    val hello = {println("Hello Kotlin")}
    hello()
    hello()
}
В данном случае лямбда сохранена в переменную hello и через эту переменную вызывается два раза. Поскольку лямбда-выражение представляет сокращенную форму функции, то переменная hello имеет тип функции () -> Unit.

Java:
val hello: ()->Unit = {println("Hello Kotlin")}
Также лямбда-выражение можно запускать как обычную функцию, используя круглые скобки:

Java:
fun main() {

    {println("Hello Kotlin")}()
}
Следует учитывать, что если до подобной записи идут какие-либо инструкции, то Kotlin автоматически может не определять, что определения лямбда-выражения составляет новую инструкцию. В этом случае предыдущую инструкции можно завершить точкой с запятой:

Java:
fun main() {

    {println("Hello Kotlin")}();
    {println("Kotlin on Metanit.com")}()
}
Лямбды как и функции могут принимать параметры. Для передачи параметров используется стрелка ->. Параметры указываются слева от стрелки, а тело лямбда-выражения, то есть сами выполняемые действия, справа от стрелки.

Java:
fun main() {

    val printer = {message: String -> println(message)}
    printer("Hello")
    printer("Good Bye")
}
Здесь лямбда-выражение принимает один параметр типа String, значение которого выводится на консоль. Переменная printer в данном случае имеет тип (String) -> Unit.

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

Java:
fun main() {

    {message: String -> println(message)}("Welcome to Kotlin")
}
Если параметров несколько, то они передаются слева от стрелки через запятую:

Java:
fun main() {

    val sum = {x:Int, y:Int -> println(x + y)}
    sum(2, 3)   // 5
    sum(4, 5)   // 9
}
Если в лямбда-выражении надо выполнить не одно, а несколько действий, то эти действия можно размещать на отдельных строках после стрелки:

Java:
val sum = {x:Int, y:Int ->
    val result = x + y
    println("$x + $y = $result")
}
Выражение, стоящее после стрелки, определяет результат лямбда-выражения. И этот результат мы можем присвоить, например, переменной.

Если лямбда-выражение формально не возвращает никакого результата, то фактически, как и в функциях, возвращается значение типа Unit:

Java:
val hello = { println("Hello")}
val h = hello()             // h представляет тип Unit

val printer = {message: String -> println(message)}
val p = printer("Welcome")    // p представляет тип Unit
В обоих случаях используется функция println, которая формально не возвращает никакого значения (точнее возвращает объект типа Unit).

Но также может возвращаться конкретное значение:

Java:
fun main() {

    val sum = {x:Int, y:Int -> x + y}

    val a = sum(2, 3)   // 5
    val b = sum(4, 5)   // 9
    println("a=$a  b=$b")
}
Здесь выражение справа от стрелки x + y продуцирует новое значение - сумму чисел, и при вызове лямбда-выражения это значение можно передать переменной. В данном случае лямбда-выражение имеет тип (Int, Int) -> Int.

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

Java:
val sum = {x:Int, y:Int ->
    val result = x + y
    println("$x + $y = $result")
    result
}
Последнее выражение по сути представляет число - сумму чисел x и y и оно будет возвращаться в качестве результата лямбда-выражения.
Лямбда-выражения можно передавать параметрам функции, если они представляют один и тот же тип функции:

Java:
fun main() {

    val sum = {x:Int, y:Int -> x + y }
    doOperation(3, 4, sum)                          // 7
    doOperation(3, 4, {a:Int, b: Int -> a * b}) // 12

}
fun doOperation(x: Int, y: Int, op: (Int, Int) ->Int){

    val result = op(x, y)
    println(result)
}
При передаче лямбды параметру или переменной, для которой явным образом указан тип, мы можем опустить в лямбда-выражении типы параметров:

Java:
fun main() {
    val sum: (Int, Int) -> Int = {x, y -> x + y }
    doOperation(3, 4, {a, b -> a * b})
}
fun doOperation(x: Int, y: Int, op: (Int, Int) ->Int){

    val result = op(x, y)
    println(result)
}
Здесь в случае с переменной sum Kotlin видит, что ее тип (Int, Int) -> Int, то есть и первый, и второй параметр представляют тип Int. Поэтому при присвоении переменной лямбды {x, y -> x + y } Kotlin автоматически поймет, что параметры x и y представляют именно тип Int.

То же самое касается и вызова функции doOperation() - при передаче в него лямбды Kotlin автоматически поймет какой параметр какой тип представляет.
Если параметр, который принимает функцию, является последним в списке, то при передачи ему лямбда-выражения, саму лямбду можно прописать после списка параметров. Например, возьмем выше использованную функцию doOperation():

Java:
fun doOperation(x: Int, y: Int, op: (Int, Int) ->Int){

    val result = op(x, y)
    println(result)
}
Здесь параметр, который представляет функцию - параметр op, является последним в списке параметров. Поэтому вместо того, чтобы написать так:

Java:
doOperation(3, 4, {a, b -> a * b}) // 12
Мы также можем написать так:

Java:
doOperation(3, 4) {a, b -> a * b} // 12
То есть вынести лямбду за список параметров. Это так называемая конечная лямбда или trailing lambda
Также фукция может возвращать лямбда-выражение, которое соответствует типу ее возвращаемого результата:

Java:
fun main() {
    val action1 = selectAction(1)
    val result1 = action1(4, 5)
    println(result1)        // 9

    val action2 = selectAction(3)
    val result2 = action2(4, 5)
    println(result2)        // 20

    val action3 = selectAction(9)
    val result3 = action3(4, 5)
    println(result3)        // 0
}
fun selectAction(key: Int): (Int, Int) -> Int{
    // определение возвращаемого результата
    when(key){
        1 -> return {x, y -> x + y }
        2 -> return {x, y -> x - y }
        3 -> return {x, y -> x * y }
        else -> return {x, y -> 0 }
    }
}
Обратим внимание на предыдущий пример на последнюю лямбду:

Java:
else -> return {x, y -> 0 }
Если в функцию selectAction() передается число, отличное от 1, 2, 3, то возвращается лямбда-выражение, которое просто возвращает число 0. С одной стороны, это лямбда-выражение должно соответствовать типу возвращаемого результата функции selectAction() - (Int, Int) -> Int

С другой стороны, оно не использует параметры, эти параметры не нужны. В этом случае вместо неиспользуемых параметров можно указать прочерки:

Java:
else -> return {_, _ -> 0 }
Kotlin поддерживает объектно-ориентированную парадигму программирования, а это значит, что программу на данном языке можно представить в виде взаимодействующих между собой объектов.

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

Для определения класса применяется ключевое слово class, после которого идет имя класса. А после имени класса в фигурных скобках определяется тело класса. Если класс не имеет тела, то фигурные скобки можно опустить. Например, определим класс, который представляет человека:

Java:
class Person

// либо можно так
class Person { }
Класс фактически представляет новый тип данных, поэтому мы можем определять переменные этого типа:

Java:
fun main() {

    val tom: Person
    val bob: Person
    val alice: Person
}

class Person
В функции main определены три переменных типа Person. Стоит также отметить, что в отличие от других объектно-ориентированных языков (как C# или Java), функция main в Kotlin не помещается в отдельных класс, а всегда определяется вне какого-либо класса.

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

Java:
val tom: Person = Person()
Часть кода после знака равно Person() как раз и представляет вызов конструктора, который создает объект класса Person. До вызова конструктора переменная класса не указывает ни на какой объект.

Например, создадим три объекта класса Person:

Java:
fun main() {

    val tom: Person = Person()
    val bob: Person = Person()
    val alice: Person = Person()
}

class Person
Каждый класс может хранить некоторые данные или состояние в виде свойств. Свойства представляют переменные, определенные на уровне класса с ключевыми словами val и var. Если свойство определено с помощью val, то значение такого свойства можно установить только один раз, то есть оно immutable. Если свойство определено с помощью var, то значение этого свойства можно многократно изменять.

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

Java:
class Person{
    var name: String = "Undefined"
    var age: Int = 18
}
В данном случае в классе Person, который представляет человека, определены свойства name (имя человека) и age (возраст человека). И эти свойства инициализированы начальными значениями.

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

Java:
fun main() {

    val bob: Person = Person()  // создаем объект
    println(bob.name)       // Undefined
    println(bob.age)        // 18

    bob.name = "Bob"
    bob.age = 25

    println(bob.name)       // Bob
    println(bob.age)        // 25
}

class Person{
    var name: String = "Undefined"
    var age: Int = 18
}
Для обращения к свойствам используется имя переменной, которая предствляет объект, и после точки указывается имя свойства. Например, получение значения свойства:

Java:
val personName : String = bob.name
Установка значения свойства:

Java:
bob.name = "Bob"
Класс также может содержать функции. Функции определяют поведение объектов данного класса. Такие функции еще называют member functions или функции-члены класса. Например, определим класс с функциями:

Java:
class Person{
    var name: String = "Undefined"
    var age: Int = 18

    fun sayHello(){
        println("Hello, my name is $name")
    }

    fun go(location: String){
        println("$name goes to $location")
    }

    fun personToString() : String{
        return "Name: $name  Age: $age"
    }
}
Функции класса определяется также как и обычные функции. В частности, здесь в классе Person определена функция sayHello(), которая выводит на консоль строку "Hello" и эмулирует приветствие объекта Person. Вторая функция - go() эмулирует движение объекта Person к определенному местоположению. Местоположение передается через параметр location. И третья функция personToString() возвращает информацию о текущем объекте в виде строки.

В функциях, которые определены внутри класса, доступны свойства этого класса. Так, в данном случае в функциях можно обратиться к свойствам name и age, которые определены в классе Person.

Для обращения к функциям класса необходимо использовать имя объекта, после которого идет название функции и в скобках значения для параметров этой функции:

Java:
fun main() {

    val tom = Person()
    tom.name = "Tom"
    tom.age = 37

    tom.sayHello()
    tom.go("the shop")
    println(tom.personToString())

}

class Person{
    var name: String = "Undefined"
    var age: Int = 18

    fun sayHello(){
        println("Hello, my name is $name")
    }

    fun go(location: String){
        println("$name goes to $location")
    }

    fun personToString() : String{
        return "Name: $name  Age: $age"
    }
}
Консольный вывод программы:

Код:
Hello, my name is Tom
Tom goes to the shop
Name: Tom  Age: 37
Для создания объекта необходимо вызвать конструктор класса. По умолчанию компилятор создает конструктор, который не принимает параметров и который мы можем использовать. Но также мы можем определять свои собственные конструкторы. Для определения конструкторов применяется ключевое слово constructor.

Классы в Kotlin могут иметь один первичный конструктор (primary constructor) и один или несколько вторичных конструкторов (secondary constructor).

Первичный конструктор является частью заголовка класса и определяется сразу после имени класса:

Java:
class Person constructor(_name: String){
 
}
Конструкторы, как и обычные функции, могут иметь параметры. Так, в данном случае конструктор имеет параметр _name, который представляет тип String. Через параметры конструктора мы можем передать извне данные и использовать их для инициализации объекта. При этом первичный конструктор в отличие от функций не определяет никаких действий, он только может принимать данные извне через параметры.

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

Java:
class Person(_name: String){
 
}
Что делать с полученными через конструктор данными? Мы их можем использовать для инициализации свойств класса. Для этого применяются блоки инициализаторов:

Java:
class Person(_name: String){
    val name: String
    init{
        name = _name
    }
}
В классе Person определено свойство name, которое хранит имя человека. Чтобы передать эту свойству значение параметра _name из первичного конструктора, применяется блок инициализатора. Блок инициализатора определяется после ключевого слова init.

Цель инициализатора состоит в инициализации объекта при его создании. Стоит отметить, что здесь свойству name не задается начальное значение, потому это свойство в любом случае будет инициализировано в блоке инициализатора, и при создании объекта оно в любом случае получит значение.

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

Java:
fun main() {
    val tom = Person("Tom")
    val bob = Person("Bob")
    val alice = Person("Alice")
 
    println(tom.name)   // Tom
    println(bob.name)   // Bob
    println(alice.name) // Alice
}

class Person(_name: String){
    val name: String
    init{
        name = _name
    }
}
Важно учитывать, что если мы определили первичный конструктор, то мы не можем использовать конструктор по умолчанию, который генерируется компилятором. Для создания объекта обязательно надо использовать первичный конструктор, если он определен в классе.

Стоит отметить, что в классе может быть определено одновременно несколько блоков инициализатора.

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

Java:
class Person(_name: String){
 
    val name: String = _name
}
Первичный конструктор также может использоваться для определения свойств:
Java:
fun main() {

    val bob: Person = Person("Bob", 23)

    println("Name: ${bob.name}  Age: ${bob.age}")
}

class Person(val name: String, var age: Int){

}
Свойства определяются как и параметры, при этом их определение начинается с ключевого слова val (если их не планируется изменять) и var (если свойства должны быть изменяемыми). И в этом случае нам уже необязательно явным образом определять эти свойства в теле класса, так как их уже определяет конструктор. И при вызове конструктора этим свойствам автоматически передаются значения: Person("Bob", 23)
Класс также может определять вторичные конструкторы. Они применяются в основном, чтобы определить дополнительные параметры, через которые можно передавать данные для инициализации объекта.

Вторичные конструкторы определяются в теле класса. Если для класса определен первичный конструктор, то вторичный конструктор должен вызывать первичный с помощью ключевого слова this:
Java:
class Person(_name: String){
    val name: String = _name
    var age: Int = 0
 
    constructor(_name: String, _age: Int) : this(_name){
        age = _age
    }
}
Здесь в классе Person определен первичный конструктор, который принимает значение для установки свойства name:

Java:
class Person(_name: String)
И также добавлен вторичный конструктор. Он принимает два параметра: _name и _age. С помощью ключевого слова this вызывается первичный конструктор, поэтому через этот вызов необходимо передать значения для параметров первичного конструктора. В частности, в первичный конструктор передается значение параметра _name. В самом вторичном конструкторе устанавливается значение свойства age.

Java:
constructor(_name: String, _age: Int) : this(_name){
    age = _age
}
Таким образом, при вызове вторичного конструктора вначале вызывается первичный конструктор, срабатывает блок инициализатора, который устанавливает свойство name. Затем выполняются собственно действия вторичного конструктора, который устанавливает свойство age.

Используем данную модификацию класса Person:

Java:
fun main() {

    val tom: Person = Person("Tom")
    val bob: Person = Person("Bob", 45)
 
    println("Name: ${tom.name}  Age: ${tom.age}")
    println("Name: ${bob.name}  Age: ${bob.age}")
}

class Person(_name: String){
    val name: String = _name
    var age: Int = 0
 
    constructor(_name: String, _age: Int) : this(_name){
        age = _age
    }
}
В функции main создаются два объекта Person. Для создания объекта tom применяется первичный конструктор, который принимает один параметр. Для создания объекта bob применяется вторичный конструктор с двумя параметрами.

Консольный вывод программы:

Код:
Name: Tom  Age: 0
Name: Bob  Age: 45
При необходимости мы можем определять и больше вторичных конструкторов:

Java:
fun main() {

    val tom = Person("Tom")
    val bob = Person("Bob", 41)
    val sam = Person("Sam", 32, "JetBtains")

    println("Name: ${tom.name}  Age: ${tom.age}  Company: ${tom.company}")
    println("Name: ${bob.name}  Age: ${bob.age}  Company: ${bob.company}")
    println("Name: ${sam.name}  Age: ${sam.age}  Company: ${sam.company}")
}

class Person(_name: String){
    val name = _name
    var age: Int = 0
    var company: String = "Undefined"

    constructor(_name: String, _age: Int) : this(_name){
        age = _age
    }

    constructor(_name: String, _age: Int, _comp: String) : this(_name, _age){
         company = _comp
    }
}
Здесь в класс Person добавлено новое свойство - company, которое описывает компании, в которой работает человек. И также добавлен еще один конструктор, который принимает три параметра:

Java:
constructor(_name: String, _age: Int, _comp: String) : this(_name, _age){
    company = _comp
}
Чтобы не дублировать код установки свойств name и age, этот вторичный конструктор передает установку этих свойств другому вторичному конструктору, который принимает два параметра, через вызов this(_name, _age). То есть данный вызов по сути будет вызывать первый вторичный конструктор с двумя параметрами.

Консольный вывод программы:

Код:
Name: Tom  Age: 0  Company: Undefined
Name: Bob  Age: 41  Company: Undefined
Name: Sam  Age: 32  Company: JetBtains
Пакеты в Kotlin представляют логический блок, который объединяет функционал, например, классы и функции, используемые для решения близких по характеру задач. Так, классы и функции, которые предназначены для решения одной задачи, можно поместить в один пакет, классы и функции для других задач можно поместить в другие пакеты.

Для определения пакета применяется ключевое слово package, после которого идет имя пакета:
Код:
package email
Определение пакета помещается в самое начало файла. И все содержимое файла рассматривается как содержимое этого пакета.

Например, добавим в проект новый файл email.kt:


И определим в нем следующий код:

Java:
package email

class Message(val text: String)

fun send(message: Message, address: String){
    println("Message `${message.text}` has been sent to $address")
}
Пакет называется "email". Он содержит класс Message, который содежит одно свойство text. Условно говоря, это класс представляет email-сообщение, а свойство text - его текст.

Также в этом пакете определена функция send(), которая условно отправляет сообшение на некоторый адрес.

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

Код:
import email.*
После названия пакета ставится точка и звездочка, тем самым импортируются все типы из этого пакета. Например, возьмем другой файл проекта - app.kt, который определяет функцию main, и используем в нем функционал пакета email:

Java:
import email.*

fun main() {

    val myMessage = Message("Hello Kotlin")
    send(myMessage, "tom@gmail.com")
}
Поскольку в начале файла импортированы все типы из пакета email, то мы можем использовать класс Message и функцию send в функции main.

Консольный вывод данной программы:

Код:
Message `Hello Kotlin` has been sent to tom@gmail.com
Также можно импортировать типы, определенные в пакете, по отдельности:

Java:
import email.send
import email.Message
С помощью оператора as можно определять псевдоним для подключаемого типа и затем обращаться к этому типу через его псевдоним:.

Java:
import email.send as sendEmail
import email.Message as EmailMessage

fun main() {

    val myMessage = EmailMessage("Hello Kotlin")
    sendEmail(myMessage, "tom@gmail.com")
}
Здесь для функции send() определен псевдоним sendEmail. И далее для обращения к этой функции надо использовать ее псевдоним:

Java:
sendEmail(myMessage, "tom@gmail.com")
Также для класса Message определен псевдоним EmailMessage. Соответственно при использовании класса необходимо применять его псевдоним, а не оригинальное имя:

Java:
val myMessage = EmailMessage("Hello Kotlin")
Псевдонимы могут нам особенно пригодится, если у нас импортируются из разных пакетов типы с одним и тем же именем. Например, пусть в проекте есть файл sms.kt:

Java:
package sms

class Message(val text: String)

fun send(message: Message, phoneNumber: String){
    println("Message `${message.text}` has been sent to $phoneNumber")
}
Здесь определен пакет sms также с классом Message и функцией send для отправке сообшения по sms.

Допустим, в файле app.kt мы одновременно хотим использовать класс Message и функцию send и из файла email.kt, и из файла sms.kt:

Java:
import email.send as sendEmail
import email.Message as EmailMessage
import sms.send as sendSms
import sms.Message as SmsMessage

fun main() {

    val myEmailMessage = EmailMessage("Hello Kotlin")
    sendEmail(myEmailMessage, "tom@gmail.com")

    val mySmsMessage = SmsMessage("Hello Kotlin")
    sendSms(mySmsMessage, "+1234567890")
}
Kotlin имеет ряд встроенных пакетов, которые подключаюся по умолчанию в любой файл на языке Kotlin:
  • kotlin.*
  • kotlin.annotation.*
  • kotlin.collections.*
  • kotlin.comparisons.*
  • kotlin.io.*
  • kotlin.ranges.*
  • kotlin.sequences.*
  • kotlin.text.*
Поэтому если возникнет необходимость использовать какие-то типы, определенные в этих пакетах, то явным образом эти пакеты не нужно импортировать.
Наследование позволяет создавать классы, которые расширяют функциональность или изменяют поведение уже существующих классов. В отношении наследования выделяются два ключевых компонента. Прежде всего это базовый класс (класс-родитель, родительский класс, суперкласс), который определяет базовую функциональность. И производный класс (класс-наследник, подкласс), который наследует функциональность базового класса и может расширять или модифицировать ее.

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

Java:
open class базовый_класс
class производный_класс: базовый_класс
Для установки наследования после названия производного класса идет двоеточие и затем указывает класс, от которого идет наследование.

Например:

Java:
open class Person{
    var name: String = "Undefined"
    fun printName(){
        println(name)
    }
}
class Employee: Person()
Например, в данном случае класс Person представляет человека, который имеет свойство name (имя человека) и метод printName() для вывода информации о человеке. Класс Employee представляет условного работника. Поскольку работник является человеком, то класс работника будет разделять общий функционал с классом человека. Поэтому вместо того, чтобы заново определять в классе Employee свойство name, лучше уснаследовать весь функционал класса Person. То есть в данном случае класс Person является базовым или суперклассом, а класс Employee - производным классом или классом-наследником.

Но стоит учитывать, что при наследовании производный класс должен вызывать первичный конструктор (а если такого нет, то конструктор по умолчанию) базового класса.

Здесь класс Person явным образом не определяет первичных конструкторов, поэтому в классе Employee надо вызывать конструктор по умолчанию для класса Person

Вызвать конструктор базового класса в производном классе можно двумя способами. Первый способ - после двоеточия сразу указать вызов конструктора базового класса:

Java:
class Employee: Person()
Здесь запись Person() как раз представляет вызов конструктора по умолчанию класса Person.

Второй способ вызвать конструктор базового класса - определить в производном классе вторичный конструктор и в нем вызвать конструктор базового класса с помощью ключевого слова super:

Java:
open class Person{
    var name: String = "Undefined"
    fun printName(){
        println(name)
    }
}
class Employee: Person{

    constructor() : super(){
     
    }
}
Здесь с помощью ключевого слова constructor в классе Employee определяется вторичный конструктор. А после списка его параметров после двоеточия идет обращение к конструктору базового класса: constructor() : super(). То есть здесь вызов super() - это и есть вызов конструктора базового класса.

Вне зависимости какой способ будет выбран, далее мы сможем создавать объекты класса Employee и использовать для него уснаследованный от класса Person функционал:

Java:
fun main() {

    val bob: Employee = Employee()
    bob.name = "Bob"
    bob.printName()
}
open class Person{
    var name: String = "Undefined"
    fun printName(){
        println(name)
    }
}
class Employee: Person()
Если базовый класс явным образом определяет конструктор (первичный или вторичный), то производный класс должен вызывать этот конструктор. Для вызова конструктора базового в производном применяются те ж способы.

Первый способ - вызвать конструктор после названия класса через двоеточие:

Java:
open class Person(val name: String){
    fun printName(){
        println(name)
    }
}
class Employee(empName: String): Person(empName)
В данном случае класс Person через конструктор устанавливает свойство name. Поэтому в классе Employee тоже определен конструктор, который принимает стороковое значение и передает его в конструктор Person.

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

Java:
open class Person(val name: String){
    fun printName(){
        println(name)
    }
}
class Employee: Person{

    constructor(empName: String) : super(empName){}
}
Опять же, поскольку конструктор Person принимает один параметр, то в super() нам надо передать значение для этого параметра.

Применение классов:

Java:
fun main() {

    val bob = Employee("Bob")
    bob.printName()
}

open class Person(val name: String){
    fun printName(){
        println(name)
    }
}
class Employee(empName: String): Person(empName)
Выше рассматривался случай, когда в базовом классе определен первичный конструктор.Но все то же действует и в том случае, если в базовом классе есть только вторичные конструкторы:

Java:
fun main() {

    val bob = Employee("Bob")
    bob.printName()
}

open class Person{

    val name: String
    constructor(userName: String){
        name = userName
    }
    fun printName(){
        println(name)
    }
}
class Employee(empName: String): Person(empName)
Производный класс наследует функционал от базового класса, но также может определять и свой собственный функционал:

Java:
fun main() {

    val bob = Employee("Bob", "JetBrains")
    bob.printName()
    bob.printCompany()
}

open class Person(val name: String){
    fun printName(){
        println(name)
    }
}
class Employee(empName: String, val company: String): Person(empName){

    fun printCompany(){
        println(company)
    }
}
В данном случае класс Employee добаваляет к унаследованному функционалу свойство company, которое хранит компанию работника, и функцию printCompany().

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

Также, стоит отметить, что все классы по умолчанию наследуются от класса Any, даже если класс Any явным образом не указан в качестве базового. Поэтому любой класс уже по умолчанию будет иметь все свойства и функции, которые определены в классе Any. Поэтому все классы по умолчанию уже будут иметь такие функции как equals, toString, hashcode.
Все используемые типы, а также компоненты типов (классы, объекты, интерфейсы, конструкторы, функции, свойства) имеют определеннй уровень видимости, определяемый модификатором видимости (модификатором доступа). Модификатор видимости определяет, где те или иные типы и их компоненты доступны и где их можно использовать. В Kotlin есть следующие модификаторы видимости:
  • private: классы, объекты, интерфейсы, а также функции и свойства, определенные вне класса, с этим модификатором видны только в том файле, в котором они определены. Члены класса с этим модификатором видны только в рамках своего класса
  • protected: члены класса с этим модификатором видны в классе, в котором они определены, и в классах-наследниках
  • internal: классы, объекты, интерфейсы, функции, свойства, конструкторы с этим модификатором видны в любой части модуля, в котором они определены. Модуль представляет набор файлов Kotlin, скомпилированных вместе в одну структурную единицу. Это может быть модуль IntelliJ IDEA или проект Maven
  • public: классы, функции, свойства, объекты, интерфейсы с этим модификатором видны в любой части программы. (При этом если функции или классы с этим модификатором определены в другом пакете их все равно нужно импортировать)

Для установки уровня видимости модификатор ставится перед ключевыми словами var/val/fun в самом начале определения свойства или функции.

Если модификатор видимости явным образом не указан, то применяется модификатор public. То есть следующий класс

Java:
class Person(){

    var name = "Undefined"
    var age = 18
 
    fun printPerson(){
        println("Name: $name  Age: $age")
    }
}
Будет эквивалентен следующему определению класса:

Java:
class Person(){

    public var name = "Undefined"
    public var age = 18

    public fun printPerson(){
        println("Name: $name  Age: $age")
    }
}
Если свойства объявляются через первичный конструктор и для них явным образом не указан модификатор видимости:

Java:
class Person(val name: String, val age: Int){
    public fun printPerson(){
        println("Name: $name  Age: $age")
    }
}
То также к таким свойствам автоматически применяется public:

Java:
class Person(public val name: String, public val age: Int){
    public fun printPerson(){
        println("Name: $name  Age: $age")
    }
}
Соответственно мы можем обращаться к подобным компонентам класса в любом месте программы:

Java:
fun main() {

    val tom = Person("Tom", 37)
    tom.printPerson()       // Name: Tom    Age: 37

    println(tom.name)
    println(tom.age)
}
Если же к свойствам и методам применяется модификатор private, то к ним нельзя будет обратиться извне - вне данного класса.

Java:
class Person(private val name:String, _age: Int){

    private val age = _age

    fun printPerson(){
        printName()
        printAge()
    }
    private fun printName(){
        println("Name: $name")
    }
    private fun printAge(){
        println("Age: $age")
    }
}

fun main() {

    val tom = Person("Tom", 37)
    tom.printPerson()
 
    // println(tom.name)   // Ошибка! - свойство name - private
    // tom.printAge()  // Ошибка! - функция printAge - private
}
В данном случае класс Person определяет два свойства name (имя человека) и age (возраст человека). Чтобы было более показательно, одно свойство определено через конструктор, а второе как переменная класса. И поскольку эти свойства определены с модификатором private, то мы можем к ним обращаться только внутри этого класса. Вне класса обращаться к ним нельзя:

Java:
println(tom.name)   // Ошибка! - свойство name - private
Также в классе определены три функции printPerson(), printAge() и printName(). Последние две функции выводят значения свойств. А функция printPerson выводит информацию об объекте, вызывая предыдущие две функции.

Однако функции printAge() и printName() определены как приватные, поэтому их можно использовать только внутри класса:

Java:
tom.printAge()  // Ошибка! - функция printAge - private
Модификатор protected определяет свойства и функции, которые из вне класса видны только в классах-наследниках:

Java:
fun main() {

    val tom = Employee("Tom", 37)
    tom.printEmployee()       // Name: Tom    Age: 37

    // println(tom.name)   // Ошибка! - свойство name - protected
    // tom.printPerson()  // Ошибка! - функция printPerson - protected
}
open class Person(protected val name:String, private val age: Int){

     protected fun printPerson(){
        printName()
        printAge()
    }
    private fun printName(){
        println("Name: $name")
    }
    private fun printAge(){
        println("Age: $age")
    }
}
class Employee(name:String, age: Int) : Person(name, age){

    fun printEmployee(){
        println("Employee $name. Full information:")
        printPerson()
        // printName() // нельзя - printName - private
        // println("Age: $age")    // нельзя age - private
    }
}
Здесь в классе Person свойство name определенно как protected, поэтому оно доступно в классе-наследнике Employee (однако вне базового и производного класса - например, в функции main оно недоступно). А вот свойство age - приватное, поэтому оно доступно только внутри класса Person.

Также в классе Employee будет доступна функция printPerson(), так как она имеет модификатор protected, а функции printAge() и printName() с модификатором private будут недоступны.
Конструкторы как первичные, так и вторичные также могут иметь модификаторы. Модификатор указывается перед ключевым словом constructor. По умолчанию они имеют модификатор public. Если для первичного конструктора необходимо явным образом установить модификатор доступа, то конструктор определяется с помощью ключевого слова constructor:

Java:
fun main() {

    // val bob  = Person("Bob")    // Так нельзя - конструктор private
}
open class Person private constructor(val name:String){

     fun printPerson(){
        println("Name: $name")
    }
}
// class Employee(name:String) : Person(name)  // так нельзя - конструктор в Person private
Стоит отметить, что в данном случае, поскольку конструктор приватный мы не можем его использовать вне класса ни для создания объекта класса в функции main, ни при наследовании. Но мы можем использовать такой конструктор в других конструкторах внутри класса:

Java:
fun main() {

    val tom = Employee("Tom", 37)
    tom.printPerson()
}
open class Person private constructor(val name:String){

    var age: Int = 0
    protected constructor(_name:String, _age: Int): this(_name){    // вызываем приватный конструктор
        age = _age
    }
     fun printPerson(){
        println("Name: $name Age: $age")
    }
}
class Employee(name:String, age: Int) : Person(name, age)
Здесь вторичный конструктор класса Person, который имеет модификатор protected (то есть доступен в текущем классе и классах-наследниках) вызывает первичный конструктор класса Person, который имеет модификатор private.
Классы, а также переменные и функции, которые определены вне других классов, также могут иметь модификаторы public, private и internal.

Допустим, у нас есть файл base.kt, который определяет одноименный пакет:

Java:
package base

private val privateVal = 3
val publicVal = 5

private class PrivateClass(val name: String)
class PublicClass(val name:String)

private fun privateFun(){
    println("privateFn")
    println(privateVal)
    val privateClass= PrivateClass("Tom")
}

fun publicFun(){
    println("publicFn")
    println(privateVal)
    val privateClass= PrivateClass("Tom")
}
Внутри данного файла мы можем использовать его приватные переменные, функции классы. Однако при подключении этого пакета в другие файлы, приватные переменные, функции и классы будут недоступны:

Java:
import base.*

fun main() {

    publicFun()
    val publicClass= PublicClass("Tom")
    println(publicVal)


    // privateFun()                         // функция недоступна
    // val privateClass= PrivateClass("Tom")    // класс недоступен
    // println(privateVal)      // переменная недоступна
}
Однако даже внутри одного файла есть ограничения на использование приватных классов:

Java:
package email

private class Message(val text: String)

fun send(message: Message, address : String){
    println("Message `${message.text}` has been sent to $address")
}
Здесь мы столкнемся с ошибкой, так как публичная функция не может принимать параметр приватного класса. И в данном случае нам надо либо сделать класс Message публичным, либо функцию send приватной.
Геттеры (getter) и сеттеры (setter) (еще их называют методами доступа) позволяют управлять доступом к переменной. Их формальный синтаксис:

Java:
var имя_свойства[: тип_свойства] [= инициализатор_свойства]
    [getter]
    [setter]
Инициализатор, геттер и сеттер свойства необязательны. Указывать тип свойства также необязательно, если он может быть выведен их значения инициализатора или из возвращаемого значения геттера.

Геттеры и сеттеры необязательно определять именно для свойств внутри класса, они могут также применяться к переменным верхнего уровня.
Сеттер определяет логику установки значения переменной. Он определяется с помощью слова set. Например, у нас есть переменная age, которая хранит возраст пользователя и представляет числовое значение.

Java:
var age: Int = 18
Но теоретически мы можем установить любой возраст: 2, 6, -200, 100500. И не все эти значения будут корректными. Например, у человека не может быть отрицательного возраста. И для проверки входных значений можно использовать сеттер:

Java:
var age: Int = 18
    set(value){
        if((value>0) and (value <110))
            field = value
    }

fun main() {

    println(age)    // 18
    age = 45
    println(age)    // 45
    age = -345
    println(age)    // 45
}
Блок set определяется сразу после свойства, к которому оно относится - в данном случае после свойства age. При этом блок set фактически представляет собой функцию, которая принимает один параметр - value, через этот параметр передается устанавливаемое значение. Например, в выражении age = 45 число 45 и будет представлять тот объект, который будет храниться в value.

В блоке set проверяем, входит ли устанавливаемое значение в диапазон допустимых значений. Если входит, то есть если значение корректно, то передаем его объекту field. Если значение некорректно, то свойство просто сохраняет свое предыдущее значение.

Идентификатор field представляет автоматически генерируемое поле, которое непосредственно хранит значение свойства. То есть свойства фактически представляют надстройку над полями, но напрямую в классе мы не можем определять поля, мы можем работать только со свойствами. Стоит отметить, что к полю через идентификатор field можно обратиться только в геттере или в сеттере, и в каждом конкретном свойстве можно обращаться только к своему полю.

В функции main при втором обращении к сеттеру (age = -345) можно заметить, что значение свойства age не изменилось. Так как новое значение -345 не входит в диапазон от 0 до 110.
Геттер управляет получением значения свойства и определяется с помощью ключевого слова get:

Java:
var age: Int = 18
    set(value){
        if((value>0) and (value <110))
            field = value
    }
    get() = field
Справа от выражения get() через знак равно указывается возвращаемое значение. В данном случае возвращается значения поля field, которое хранит значение свойства name. Хотя в таком геттер большого смысла нет, поскольку получить подобное значение мы можем и без геттера.

Если геттер должен содержать больше инструкций, то геттер можно оформить в блок с кодом внутри фигурных скобок:

Java:
var age: Int = 18
    set(value){
        println("Call setter")
        if((value>0) and (value <110))
            field = value
    }
    get(){
        println("Call getter")
        return field
    }
Если геттер оформлен в блок кода, то для возвращения значения необходимо использовать оператор return. И, таким образом, каждый раз, когда мы будем получать значение переменной age (например, в случае с вызовом println(age)), будет срабатывать геттер, когда возвращает значение. Например:

Java:
fun main() {

    println(age)    // срабатывает get
    age = 45        // срабатывает set
    println(age)    // срабатывает get
}
Консольный вывод программы

Код:
Call getter
18
Call setter
Call getter
45
Хотя геттеры и сеттеры могут использоваться к глобальным переменным, как правило, они применяются для опосредования доступа к свойствам класса.

Используем сеттер:

Java:
fun main() {

    val bob: Person = Person("Bob")
    bob.age = 25        // вызываем сеттер

    println(bob.age)   // 25
    bob.age = -8        // вызываем сеттер
    println(bob.age)   // 25
}
class Person(val name: String){

    var age: Int = 1
        set(value){
            if((value>0) and (value <110))
                field = value
        }
}
При втором обращении к сеттеру (bob.age = -8) можно заметить, что значение свойства age не изменилось. Так как новое значение -8 не входит в диапазон от 0 до 110.
Геттер может возвращать вычисляемые значения, которые могут задействовать несколько свойств:

Java:
fun main() {
    val tom = Person("Tom", "Smith")
    println(tom.fullname)   // Tom Smith
    tom.lastname = "Simpson"
    println(tom.fullname)   // Tom Simpson
}
class Person(var firstname: String, var lastname: String){

    val fullname: String
        get() = "$firstname $lastname"
}
Здесь свойство fullname определяет блок get, который возвращает полное имя пользователя, созданное на основе его свойств firstname и lastname. При этом значение самого свойства fullname напрямую мы изменить не можем - оно определено доступно только для чтения. Однако если изменятся значения составляющих его свойств - firstname и lastname, то также изменится значение, возвращаемое из fullname.
Выше уже рассматривалось, что с помощью специального поля field в сеттере и геттере можно обращаться к непосредственному значению свойства, которое хранится в специальном поле. Однако мы сами можем явным образом определить подобное поле. Нередко это приватное поле:

Можно использовать одновременно и геттер, и сеттер:

Java:
fun main() {

    val tom = Person("Tom")
    println(tom.age)    // 1
    tom.age = 37
    println(tom.age)    // 37
    tom.age = 156
    println(tom.age)    // 37
}
class Person(val name: String){

    private var _age = 1
    var age: Int
        set(value){
            if((value > 0) and (value < 110))
                _age = value
        }
        get()=  _age
}
Здесь для свойства age добавлены геттер и сеттер, которые фактически являются надстройкой над полей _age, которое собственно хранит значение.
Kotlin позволяет переопределять в производном классе функции и свойства, которые определенны в базовом классе. Чтобы функции и свойства базового класа можно было переопределить, к ним применяется аннотация open. При переопределении в производном классе к этим функциям применяется аннотация override.
Чтобы указать, что свойство можно переопределить в производном классе, перед его определением указывается ключевое слово open:

Java:
open class Person(val name: String){
    open var age: Int = 1
}
В данном случае свойство age доступно для переопределения.

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

Java:
open class Person(val name: String, open var age: Int = 1){
}
В производном классе для переопределения свойства перед ним указывается аннотация override.

Java:
open class Person(val name: String, open var age: Int = 1){
}

open class Employee(name: String): Person(name){

    override var age: Int = 18
}
Здесь переопределение заключается в изменении начального значения для свойства age.

Также переопределить свойство можно сразу в первичном конструкторе:

Java:
open class Person(val name: String, open var age: Int = 1){
}

open class Employee(name: String, override var age: Int = 18): Person(name, age){}
Применение:

Java:
fun main() {

    val tom = Person("Tom")
    println("Name: ${tom.name}  Age: ${tom.age}")

    val bob = Employee("Bob")
    println("Name: ${bob.name}  Age: ${bob.age}")
}
open class Person(val name: String, open var age: Int = 1)

open class Employee(name: String, override var age: Int = 18): Person(name, age)
Консольный вывод:

Код:
Name: Tom  Age: 1
Name: Bob  Age: 18
Также можно переопределять геттеры и сеттеры свойств:

Java:
open class Person(val name: String){

    open val fullInfo: String
        get() = "Person $name - $age"

    open var age: Int = 1
        set(value){
            if(value > 0 && value < 110)
                field = value
        }
}
open class Employee(name: String): Person(name){

    override val fullInfo: String
        get() = "Employee $name - $age"

    override var age: Int = 18
        set(value){
            if(value > 17 && value < 110)
                field = value
        }
}

fun main() {

    val tom = Person("Tom")
    tom.age = 14
    println(tom.fullInfo)

    val bob = Employee("Bob")
    bob.age = 14
    println(bob.fullInfo)
}
Здесь класс Employee переопределяет геттер свойства fullInfo и сеттер свойства age
Чтобы функции базового класа можно было переопределить, к ним применяется аннотация open. При переопределении в производном классе к этим функциям применяется аннотация override:

Java:
open class Person(val name: String){
    open fun display(){
        println("Name: $name")
    }
}
class Employee(name: String, val company: String): Person(name){

    override fun display() {
        println("Name: $name    Company: $company")
    }
}

fun main() {

    val tom = Person("Tom")
    tom.display()       // Name: Tom

    val bob = Employee("Bob", "JetBrains")
    bob.display()       // Name: Bob  Company: JetBrains
}
Функция display определена в классе Person с аннотацией open, поэтому в производных классах его можно переопределить. В классе Employee эта функция переопределена с применением аннотации override.
Стоит учитывать, что переопределить функции можно по всей иерархии наследования. Например, у нас может быть класс Manager, унаследованный от Employee:

Java:
open class Person(val name: String){
    open fun display(){
        println("Name: $name")
    }
}
open class Employee(name: String, val company: String): Person(name){

    override fun display() {
        println("Name: $name    Company: $company")
    }
}
class Manager(name: String, company: String):Employee(name, company){
    override fun display() {
        println("Name: $name Company: $company  Position: Manager")
    }
}
В данном случае класс Manager переопределяет функцию display, поскольку среди его базовых классов есть класс Person, который определяет эту функцию с ключевым словом open.
В это же время иногда бывает необходимо запретить дальнейшее переопределение функции в классах-наследниках. Для этого применяется ключевое слово final:

Java:
open class Person(val name: String){
    open fun display(){
        println("Name: $name")
    }
}
open class Employee(name: String, val company: String): Person(name){

    final override fun display() {
        println("Name: $name    Company: $company")
    }
}
class Manager(name: String, company: String):Employee(name, company){
    // теперь функцию нельзя переопределить
    /*override fun display() {
        println("Name: $name Company: $company  Position: Manager")
    }*/
}
С помощью ключевого слова super в производном классе можно обращаться к реализации из базового класса.

Java:
open class Person(val name: String){

    open val fullInfo: String
        get() = "Name: $name"

    open fun display(){
        println("Name: $name")
    }
}
open class Employee(name: String, val company: String): Person(name){

    override val fullInfo: String
        get() = "${super.fullInfo} Company: $company"

    final override fun display() {
        super.display()
        println("Company: $company")
    }
}
В данном случае производный класс Employee при переопределении свойства и функции применяет реализацию из базового класса Person. Например, через super.fullInfo возвращается значение свойства из базового класса (то есть значение свойства name), а с помощью вызова super.display() вызывается реализация функции display из класса Person.
Абстрактные классы - это классы, определенные с модификатором abstract. Отличительной особенностью абстрактных классов является то, что мы не можем создать объект подобного класса. Например, определим абстрактный класс Human:

Java:
abstract class Human(val name: String)
Абстрактный класс, как и обычный, может иметь свойства, функции, конструкторы, но создать его объект напрямую вызвав его конструктор мы не можем:

Java:
val kate: Human        // норм, просто определение переменной
val alice: Human = Human("Alice")    // ! ошибка, создать объект нельзя
Такой класс мы можем только унаследовать:

Java:
abstract class Human(val name: String){

    fun hello(){
        println("My name is $name")
    }
}
class Person(name: String): Human(name)
Стоит отметить, что в данном случае перед абстрактным классом не надо указывать аннотацию open, как при наследовании неабстрактных классов.

Java:
fun main(args: Array<String>) {

    val kate: Person = Person("Kate")
    val slim: Human = Person("Slim Shady")
    kate.hello()    // My name is Kate
    slim.hello()    // My name is Slim Shady
}
Абстрактные классы могут иметь абстрактные методы и свойства. Это такие функции и свойства, которые определяются с ключевым словом abstract. Абстрактные методы не содержат реализацию, то есть у них нет тела. А для абстрактных свойств не указывается значение. При этом абстрактные методы и свойства можно определить только в абстрактных классах:

Java:
abstract class Human(val name: String){

    abstract var age: Int
    abstract fun hello()
}
class Person(name: String): Human(name){
 
    override var age : Int = 1
    override fun hello(){
        println("My name is $name")
    }
}
Если класс наследуется от абстрактного класса, то он должен либо реализовать все его абстрактные методы и свойства, либо также быть абстрактным.

Так, в данном случае класс Person должен обязательно определить реализацию для функции hello() и свойства age. При этом, как и при переопределении обычных методов и свойств, применяется аннотация override.

Абстрактные свойства также можно реализовать в первичном конструкторе:

Java:
abstract class Human(val name: String){

    abstract var age: Int
    abstract fun hello()
}
class Person(name: String, override var age : Int): Human(name){
    override fun hello(){
        println("My name is $name")
    }
}
Зачем нужны абстрактные классы? Классы обычно отражают какие-то сущности реального мира. Но некоторые из этих сущностей представляют абстракцию, которая непосредственного воплощения не имеет. Например, возьмем систему геометрических фигур. В реальности не существует геометрической фигуры как таковой. Есть круг, прямоугольник, квадрат, но просто фигуры нет. Однако же и круг, и прямоугольник имеют что-то общее и являются фигурами. В этом случае мы можем определить абстрактный класс фигуры и затем от него унаследовать все остальные классы фигур:

Java:
// абстрактный класс фигуры
abstract class Figure {
    // абстрактный метод для получения периметра
    abstract fun perimeter(): Float

    // абстрактный метод для получения площади
    abstract fun area(): Float
}
// производный класс прямоугольника
class Rectangle(val width: Float, val height: Float) : Figure()
{
    // переопределение получения периметра
    override fun perimeter(): Float{
        return width * 2 + height * 2;
    }
    // переопрелеление получения площади
    override fun area(): Float{
        return width * height;
    }
}
Интерфейсы представляют контракт, который должен реализовать класс. Интерфейсы могут содержать объявления свойств и функций, а также их реализацию по умолчанию.

Для определения интерфейса применяется ключевое слово interface. Например:

Java:
interface Movable{
    var speed: Int  // объявление свойства
    fun move()      // определение функции без реализации
    fun stop(){     // определение функции с реализацией по умолчанию
        println("Остановка")
    }
}
Например, в данном случае интерфейс Movable представляет функцонал транспортного средства. Он содержит две функции и одно свойство. Функция move() представляет абстрактный метод - она не имеет реализации. Вторая функция stop() имеет реализацию по умолчанию.

При определении свойств в интерфейсе им не присваиваются значения.

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

Определим два класса, которые применяют интерфейс:

Java:
class Car : Movable{

    override var speed = 60
    override fun move(){
        println("Машина едет со скоростью $speed км/ч")
    }
}
class Aircraft : Movable{
 
    override var speed = 600
    override fun move(){
        println("Самолет летит со скоростью $speed км/ч")
    }
    override fun stop(){
        println("Приземление")
    }
}
Для применения интерфейса после имени класса ставится двоеточие, за которым следует название интерфейса. При применении интерфейса класс должен реализовать все его абстрактные методы и свойства, а также может предоставить свою реализацию для тех свойств и методов, которые уже имеют реализацию по умолчанию. При реализации функций и свойств перед ними ставится ключевое слово override.

Так, класс Car представляет машину и применяет интерфейс Movable. Так как интерфейс содержит абстрактный метод move(), то класс Car обязательно должен его реализовать.

Тоже касается свойства speed - класс Car должен его определить. Здесь реализация свойства заключается в установке для него начального значения.

А вот функцию stop() класс Car может не реализовать, так как она уже содержит реализацию по умолчанию.

Класс Aircraft представляет самолет и тоже применяет интерфейс Movable. При этом класс Aircraft реализует обе функции интерфейса.

В последствии в программе мы можем рассматривать объекты классом Car и Aircraft как объекты Movable:

Java:
fun main() {

    val m1: Movable = Car()
    val m2: Movable = Aircraft()
    // val m3: Movable = Movable() напрямую объект интерфейса создать нельзя

    m1.move()
    m1.stop()
    m2.move()
    m2.stop()
}
Консольный вывод программы:

Код:
Машина едет со скоростью 60 км/ч
Останавливается
Самолет летит со скоростью 600 км/ч
Самолет приземляется
Рассмотрим еще пример. Определим интерфейс Info, который объявляет ряд свойств:

Java:
interface Info{
    val model: String
        get() = "Undefined"
    val number: String
}
Первое свойство имеет геттер, а это значит, что оно имеет реализацию по умолчанию. При применении интерфейса такое свойство необязательно реализовать. Второе свойство - number является абстрактным, оно не имеет ни геттера, ни сеттера, то есть не имеет реализации по умолчанию, поэтому классы его обязаны реализовать.

Для реализации интерфейса возьмем выше определенный класс Car:

Java:
class Car(override val model: String, override var number: String) : Movable, Info{

    override var speed = 60
    override fun move(){
        println("Машина едет со скоростью $speed км/ч")
    }
}
Теперь класс Car применяет два интерфейса. Класс может применять несколько интерфейсов, в этом случае они указываются через запятую, и все эти интерфейсы класс должен реализовать. Класс Car реализует оба свойства. При этом при реализации свойств в классе необязательно указывать геттер или сеттер. Кроме того, можно реализовать свойства в первичном конструкторе, как это сделано в случае со свойствами model и number

Применение класса:

Java:
fun main() {

    val tesla: Car = Car("Tesla", "2345SDG")
    println(tesla.model)
    println(tesla.number)

    tesla.move()
    tesla.stop()
}
В Kotlin мы можем наследовать класс и применять интерфейсы. При этом мы можем одновременно и наследоваться от класса, и применять один или несколько интерфейсов. Однако что, если переопределяемая функция из базового класса имеет то же имя, что и функция из применяемого интерфейса:

Java:
open class Video {
    open fun play() { println("Play video") }
}

interface AudioPlayable {
    fun play() { println("Play audio") }
}

class MediaPlayer() : Video(), AudioPlayable {
    // Функцию play обязательно надо переопределить
    override fun play() {
        super<Video>.play()         // вызываем Video.play()
        super<AudioPlayable>.play() // вызываем AudioPlayable.play()
    }
}
Здесь класс Video и интерфейс AudioPlayable определяют функцию play. В этом случае класс MediaPlayer, который наследуется от Video и применяет интерфейс AudioPlayable, обязательно должен определить функцию с тем же именем, то есть play. С помощью конструкции super<имя_типа>.имя_функции можно обратиться к опредленной реализации либо из базового класса, либо из интерфейса.
В Kotlin классы и интерфейсы могут быть определены в других классах и интерфейсах. Такие классы (вложенные классы или nested classes) обычно выполняют какую-то вспомогательную роль, а определение их внутри класса или интерфейса позволяет разместить их как можно ближе к тому месту, где они непосредственно используются.

Например, в следующем случае определяется вложенный класс:

Java:
class Person{
    class Account(val username: String, val password: String){

        fun showDetails(){
            println("UserName: $username  Password: $password")
        }
    }
}
В данном случае класс Account является вложенным, а класс Person{ - внешним.

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

Java:
fun main() {

    val userAcc = Person.Account("qwerty", "123456");
    userAcc.showDetails()
}
Если необходимо ограничить область применения вложенного класса только внешним классом, то следует определить вложенный класс с модификатором private:

Java:
class Person(username: String, password: String){

    private val account: Account = Account(username, password)

    private class Account(val username: String, val password: String)

    fun showAccountDetails(){
        println("UserName: ${account.username}  Password: $account.password")
    }
}
fun main() {

    val tom = Person("qwerty", "123456");
    tom.showAccountDetails()
}
Классы также могут содержать вложенные интерфейсы. Кроме того, интерфейсы тоже могут содержать вложенные классы и интерфейсы:

Java:
interface SomeInterface {
    class NestedClass
    interface NestedInterface
}

class SomeClass {
    class NestedClass
    interface NestedInterface
}
Стоит учитывать, что вложенный (nested) класс по умолчанию не имеет доступа к свойствам и функциям внешнего класса. Например, в следующем случае при попытке обратиться к свойству внешнего класса мы получим ошибку:

Java:
class BankAccount(private var sum: Int){
 
    fun display(){
        println("sum = $sum")
    }

    class Transaction{
        fun pay(s: Int){
            sum -= s
            display()
        }
    }
}
В данном случае у нас определен класс банковского счета BankAccount, который определяет свойство sum - сумма на счете и функцию display() для вывода информации о счете.

Кроме того, в классе BankAccount определен вложенный класс Transaction, который представляет операцию по счету. В данном случае класс Transaction определяет функцию pay() для оплаты со счета. Однако в нем мы не можем обратиться в свойствам и функциям внешнего класса BankAccount.

Чтобы вложенный класс мог иметь доступ к свойствам и функциям внешнего класса, необходимо определить вложенный класс с ключевым словом inner. Такой класс еще называют внутренним классом (inner class), чтобы отличать от обычных вложенных классов. Например:

Java:
fun main() {

    val acc = BankAccount(3400);
    acc.Transaction().pay(2500)
}
class BankAccount(private var sum: Int){

    fun display(){
        println("sum = $sum")
    }

    inner class Transaction{
        fun pay(s: Int){
            sum -= s
            display()
        }
    }
}
Теперь класс Transaction определен с ключевым словом inner, поэтому имеет полный доступ к свойствам и функциям внешнего класса BankAccount. Но теперь если мы хотим использовать объект подобного вложенного класса, то необходимо создать объект внешнего класса:

Java:
val acc = BankAccount(3400);
    acc.Transaction().pay(2500)
Но что если свойства и функции внутреннего класса называются также, как и свойства и функции внешнего класса? В этом случае внутренний класс может обратиться к свойствам и функциям внешнего через конструкцию this@название_класса.имя_свойства_или_функции:

Java:
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
        }
    }
}
Например, перепишем случай выше с классами Account и Transaction следующим образом:

Java:
fun main() {

    val acc = BankAccount(3400);
    acc.Transaction(2400).pay()
}
class BankAccount(private var sum: Int){

    fun display(){
        println("sum = $sum")
    }

    inner class Transaction(private var sum: Int){
        fun pay(){
            this@BankAccount.sum -= this@Transaction.sum
            display()
        }
    }
}
Иногда классы бывают необходимы только для хранения некоторых данных. В Kotlin такие классы называются data-классы. Они определяются с модификатором data:

Java:
data class Person(val name: String, val age: Int)
При компиляции такого класса компилятор автоматически добавляет в класс функции с определенной реализацией, которая учитывает свойства класса, которые определены в первичном конструкторе:

  • equals(): сравнивает два объекта на равенств
  • hashCode(): возвращает хеш-код объекта
  • toString(): возвращает строковое представление объекта
  • copy(): копирует данные объекта в другой объект

Например, возьмем функцию toString(), которая возвращает строковое представление объекта:

Java:
fun main() {

    val alice: Person = Person("Alice", 24)
    println(alice.toString())
}

class Person(val name: String, val age: Int)
Результатом программы будет следующий вывод:

Java:
Person@2a18f23c
По умолчанию строковое представление объекта нам практически ни о чем не говорит. Как правило, данная функция предназначена для вывода состояния объекта, но для этого ее надо переопределять. Однако теперь добавим модификатор data к определению класса:

Java:
data class Person(val name: String, val age: Int)
И результат будет отличаться:

[CODO=java]Person(name=Alice, age=24)
Код:
То есть мы можем увидить, какие данные хранятся в объекте, какие они имеют значения. То же самое касается всех остальных функций. Таким образом, в случае с data-классами мы имеем готовую реализацию для этих функций. Их не надо вручную переопределять. Но вполне возможно нас может не устраивать эта реализация, тогда мы можем определить свою:

[CODE=java]data class Person(val name: String, val age: Int){
    override fun toString(): String {
        return "Name: $name  Age: $age"
    }
}
В этом случае для функции toString() компилятор не будет определять реализацию.

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

Java:
fun main() {

    val alice: Person = Person("Alice", 24)
    val kate = alice.copy(name = "Kate")
    println(alice.toString())   // Person(name=Alice, age=24)
    println(kate.toString())    // Person(name=Kate, age=24)
}

data class Person(var name: String, var age: Int)
Опять же компилятор генерирует функцию копирования по умолчанию, которую мы можем использовать. Если мы хотим, чтобы некоторые данные у объкта отличались, то мы их можем указать в функции copy в виде именованных арументов, как в случае со свойством name в примере выше.

При этом чтобы класс определить как data-класс, он должен соответствовать ряду условий:

  • Первичный конструктор должен иметь как минимум один параметр
  • Все параметры первичного конструктора должны предваряться ключевыми словами val или var, то есть определять свойства
  • Свойства, которые определяются вне первичного конструктора, не используются в функциях toString, equals и hashCode
  • Класс не должен определяться с модификаторами open, abstract, sealed или inner.

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

Java:
data class Person(var name: String, var age: Int)
Но вообще в ряде ситуаций рекомендуется определять свойства через val, то есть делать их неизменяемыми, поскольку на их основании вычисляет хеш-код, который используется в качестве ключа объекта в такой коллекции как HashMap.
Kotlin предоставляет для data-классов возможность декомпозиции на переменные:

Java:
fun main() {

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

    val (username, userage) = alice
    println("Name: $username  Age: $userage") // Name: Alice  Age: 24
}

data class Person(var name: String, var age: Int)
Enums или перечисления представляют тип данных, который позволяет определить набор логически связанных констант. Для определения перечисления применяются ключевые слова enum class. Например, определим перечисление:

Java:
enum class Day{
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
Данное перечисление Day представляет день недели. Внутри перечисления определяются константы. В данном случае это названия семи дней недели. Константы определяются через запятую. Каждая константа фактически представляет объект данного перечисления.

Java:
fun main() {

    val day: Day = Day.FRIDAY
    println(day)            // FRIDAY
    println(Day.MONDAY)     // MONDAY
}
Классы перечислений как и обычные классы также могут иметь конструктор. Кроме того, для констант перечисления также может вызываться конструктор для их инициализации.

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

fun main() {

    val day: Day = Day.FRIDAY
    println(day.value)        // 5
    println(Day.MONDAY.value) // 1
}
В примере выше у класса перечисления через конструктор определяется свойство value. Соответственно при определении констант перечисления необходимо каждую из этих констант инициализировать, передав значение для свойства value.

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

Java:
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;
    }
}

fun main() {

    val day1: Day = Day.FRIDAY
    val day2: Day = Day.MONDAY
    println(day1.getDuration(day2))        // 4
}
В данном случае в перечислении определена функция getDuration(), которая вычисляет разницу в днях между двумя днями недели.

Все перечисления обладают двумя встроенными свойствами:

  • name: возвращает название константы в виде строки
  • ordinal: возвращает порядковый номер константы
Java:
enum class Day(val value: Int){
    MONDAY(1), TUESDAY(2), WEDNESDAY(3),
    THURSDAY(4), FRIDAY(5), SATURDAY(6),
    SUNDAY(7)
}

fun main() {

    val day1: Day = Day.FRIDAY
    println(day1.name)        // FRIDAY
    println(day1.ordinal)     // 4
}
Кроме того, в Kotlin нам доступны вспомогательные функции:

  • valueOf(value: String): возвращает объект перечисления по названию константы
  • values(): возвращает массив констант текущего перечисления

Java:
fun main() {

    for(day in Day.values())
        println(day)

    println(Day.valueOf("FRIDAY"))
}
Константы перечисления могут определять анонимные классы, которые могут иметь собственные методы и свойства или реализовать абстрактные методы класса перечисления:

Java:
enum class DayTime{
    DAY{
        override val startHour = 6
        override val endHour = 21
        override fun printName(){
            println("День")
        }
    },
    NIGHT{
        override val startHour = 22
        override val endHour = 5
        override fun printName(){
            println("Ночь")
        }
    };
    abstract fun printName()
    abstract val startHour: Int
    abstract val endHour: Int
}

fun main() {

    DayTime.DAY.printName()     // День
    DayTime.NIGHT.printName()   // Ночь

    println("Day from ${DayTime.DAY.startHour} to ${DayTime.DAY.endHour}")

}
В данном случае класс перечисления DayTime определяет абстрактный метод printName() и две переменных - startHour (начальный час) и endHour (конечный час). А константы определяют анонимные классы, которые реализуют эти свойства и функцию.

Также, классы перечислений могут применять интерфейсы. Для этого для каждой константы определяется анонимный класс, который содержат все реализуемые свойства и функции:

Java:
interface Printable{
    fun printName()
}
enum class DayTime: Printable{
    DAY{
        override fun printName(){
            println("День")
        }
    },
    NIGHT{
        override fun printName(){
            println("Ночь")
        }
    }
}

fun main() {

    DayTime.DAY.printName()     // День
    DayTime.NIGHT.printName()   // Ночь
}
Нередо перечисления применяются для хранения состояния в программе. И в зависимоси от этого состояния мы можем направить действие программы по определенному пути. Например, определим перечисление, которое представляет арифметические операции, и функцию, которая в зависимости от переданной операции выполняет то или иное действие:

Java:
fun main() {

    println(operate(5, 6, Operation.ADD))         // 11
    println(operate(5, 6, Operation.SUBTRACT))   // -1
    println(operate(5, 6, Operation.MULTIPLY))   // 30
}
enum class Operation{

    ADD, SUBTRACT, MULTIPLY
}
fun operate(n1: Int, n2: Int, op: Operation): Int{

    when(op){
        Operation.ADD -> return n1 + n2
        Operation.SUBTRACT -> return n1 - n2
        Operation.MULTIPLY -> return n1 *n2
    }
}
Функция operate() принимает два числа - операнды операции и тип операции в виде перечисления Operation. И в зависимоси от значения перечисления возвращает либо сумму, либо разность, либо произведение двух чисел.
Делегирование представляет паттерн объектно-ориентированного программирования, который позволяет одному объекту делегировать/перенаправить все запросы другому объекту. В определенной степени делегирование может выступать альтернативой наследованию. И преимуществом Kotlin в данном случае состоит в том, что Kotlin нативно поддерживает данный паттерн, предоставляя необходимый инструментарий.

Формальный синтаксис:

Java:
interface Base {
    fun someFun()
}

class BaseImpl() : Base {
    override fun someFun() { }
}

class Derived(someBase: Base) : Base by someBase
Есть некоторый интерфейс - Base, который определяет некоторый функционал. Есть его реализация в виде класса BaseImpl.

И есть еще один класс - Derived, который также применяет интерфейс Base. Причем после указания применяемого интерфейса идет ключевое слово by, а после него - объект, которому будут делегироваться вызовы.

Java:
class Derived(someBase: Base) : Base by someBase
То есть в данной схеме класс Derived будет делегировать вызовы объекту someBase, который представляет интерфейс Base и передается через первичный конструктор. При этом Derived может не реализовать интерфейс Base или реализовать неполностью - какие-то отдельные свойства и функции.

Например, рассмотрим следующие классы:

Java:
interface Messenger{
    fun send(message: String)
}
class InstantMessenger(val programName: String) : Messenger{

    override fun send(message: String){
        println("Message `$message` has been sent")
    }
}
class SmartPhone(val name: String, m: Messenger): Messenger by  m
Здесь определен интерфейс Messenger, который представляет условно программу для отправки сообщений. Для условной отправки сообщений определена функция send().

Также есть класс InstantMessenger - программа мгновенных сообщений или проще говоря мессенджер, который применяет интерфейс Messenger, реализуя его функцию send()

Далее определен класс SmartPhone, который представляет смартфон и также применяет интерфейс Messenger, но не реализует его. Вместо этого он принимает через первичный конструктор объект Messenger и делегирует ему обращение к функции send().

Применим классы:

Java:
fun main() {
    val telegram = InstantMessenger("Telegram")
    val pixel = SmartPhone("Pixel 5", telegram)
    pixel.send("Hello Kotlin")
    pixel.send("Learn Kotlin on Metanit.com")
}
Здесь создан объект pixel, который представляет класс SmartPhone. Поскольку SmartPhone применяет интерфейс Messenger, то мы можем вызвать у объекта pixel функцию send() для отправки условного сообщения. Однако сам класс SmartPhone НЕ реализует функцию send - само выполнение этой функции делегируется объекту telegram, который в реальности выполняет отправку сообщения. Соответственно при выполнении программы мы увидим следующий консольный вывод:

Код:
Message `Hello Kotlin` has been sent
Message `Learn Kotlin on Metanit.com` has been sent
Подобным образом один объект может делегировать выполнение различных функций разным объектам. Например:

Java:
fun main() {
    val telegram = InstantMessenger("Telegram")
    val photoCamera = PhotoCamera()
    val pixel = SmartPhone("Pixel 5", telegram, photoCamera)
    pixel.send("Hello Kotlin")
    pixel.takePhoto()
}

interface Messenger{
    fun send(message: String)
}
class InstantMessenger(val programName: String) : Messenger{
    override fun send(message: String) = println("Send message: `$message`")
}
interface PhotoDevice{
    fun takePhoto()
}
class PhotoCamera: PhotoDevice{
    override fun takePhoto() = println("Take a photo")
}
class SmartPhone(val name: String, m: Messenger, p: PhotoDevice)
    : Messenger by  m, PhotoDevice by p
Здесь класс SmartPhone также реализует интерфейс PhotoDevice, который предоставляет функцию takePhoto() для съемки фото. Но выполнение этой функции он делегирует параметру p, который представляет интерфейс PhotoDevice и в роли которого выступает объект PhotoCamera.
Класс может переопределять часть функций интерфейса, в этом случае выполнение этих функций не делегируется. Например:

Java:
fun main() {
    val telegram = InstantMessenger("Telegram")
    val pixel = SmartPhone("Pixel 5", telegram)
    pixel.sendTextMessage()
    pixel.sendVideoMessage()
}

interface Messenger{
    fun sendTextMessage()
    fun sendVideoMessage()
}
class InstantMessenger(val programName: String) : Messenger{
    override fun sendTextMessage() = println("Send text message")
    override fun sendVideoMessage() = println("Send video message")
}
class SmartPhone(val name: String, m: Messenger) : Messenger by  m{
    override fun sendTextMessage() = println("Send sms")
}
В данном случае класс SmartPhone реализует функцию sendTextMessage(), поэтому ее выполнение не делегируется. Консольный вывод программы:

Код:
Send sms
Send video message
По аналогии с функциями объект может делегировать обращение к свойствам:

Java:
fun main() {
    val telegram = InstantMessenger("Telegram")
    val pixel = SmartPhone("Pixel 5", telegram)
    println(pixel.programName)  // Telegram
}
interface Messenger{
    val programName: String
}
class InstantMessenger(override val programName: String) : Messenger
class SmartPhone(val name: String, m: Messenger) : Messenger by  m
Здесь при интерфейс Messenger определяет свойство programName - название программы отправки. Класс SmartPhone не реализует это свойство, поэтому обращение к этому свойству делегируется объекту m.

Если бы класс SmartPhone сам реализовал это свойство, то делегирования бы не было:

Java:
fun main() {
    val telegram = InstantMessenger("Telegram")
    val pixel = SmartPhone("Pixel 5", telegram)
    println(pixel.programName)  // Default Messenger
}
interface Messenger{
    val programName: String
}
class InstantMessenger(override val programName: String) : Messenger
class SmartPhone(val name: String, m: Messenger) : Messenger by  m{
    override val programName = "Default Messenger"
}
Иногда возникает необходимость создать объект некоторого класса, который больше нигде в программе не используется. То есть класс необходим только для создания только одного объекта. В этом случае мы, конечно, можем, как и обычно, определить класс и затем создать объект этого класса. Но Kotlin для таких ситуаций предоставлять возможность определить объект анонимного класса.

Анонимные классы не используют ключевое слово class для определения. Они не имеют имени, но как и обычные классы могут наслдовать другие классы или применять интерфейсы. Объекты анонимных классов называют анонимыми объктами.

Для определения анонимного объекта применяется ключевое слово object:

Java:
fun main() {

    val tom = object {
        val name = "Tom"
        var age = 37
        fun sayHello(){
            println("Hi, my name is $name")
        }
    }
    println("Name: ${tom.name}  Age: ${tom.age}")
    tom.sayHello()
}
После ключевого слова object идет блок кода в фигурных скобках, в которые помещается определение объекта. Как и в обычном классе, анонимный объект может содержать свойства, функции. И далее по имени переменной мы можем обращаться к свойствам и функциям этого объекта.

При наследовании после слова object через двоеточия указывается имя наследуемого класса или его первичный конструктор:

Java:
fun main() {

    val tom = object : Person("Tom"){

        val company = "JetBrains"
        override fun sayHello(){
            println("Hi, my name is $name. I work in $company")
        }
    }

    tom.sayHello()  // Hi, my name is Tom. I work in JetBrains
}
open class Person(val name: String){
    open fun sayHello(){
        println("Hi, my name is $name")
    }
}
Здесь класс анонимного объекта наследует класс Person и переопределяет его функцию sayHello().
Анонимный объект может передаваться в качестве аргумента в вызов функции:

Java:
fun main() {
    hello(
        object : Person("Sam"){
            val company = "JetBrains"
            override fun sayHello(){
                println("Hi, my name is $name. I work in $company")
            }
    })
}
fun hello(person: Person){
    person.sayHello()
}
open class Person(val name: String){
    open fun sayHello() = println("Hi, my name is $name")
}
Здесь поскольку класс анонимного объекта наследуется от класса Person, мы можем передавать этот анонимный объект параметру функции, который имеет тип Person.
Функция может возвращать анонимный объект:

Java:
fun main() {
    val tom = createPerson("Tom", "JetBrains")
    tom.sayHello()
}
private fun createPerson(_name: String, _company: String) = object{
    val name = _name
    val company = _company
    fun sayHello() = println("Hi, my name is $name. I work in $company")
}
Однако тут есть нюансы. Чтобы мы могли обращаться к свойствам и функциям анонимного объекта, функция, которая возвращает этот объект, должна быть приватной, как в примере выше.

Если функция имеет модификатор public или private inline, то в этом случае свойства и функции анонимного класса (за исключением унаследованных) недоступны:

Java:
fun main() {
    val tom = createPerson("Tom", "JetBrains")
    println(tom.name)   // норм - свойство name унаследовано от Person
    println(tom.company)    // ! Ошибка - свойство недоступно
}
private inline fun createPerson(_name: String, _comp: String) = object: Person(_name){
    val company = _comp
}

open class Person(val name: String)
В данном случае функция createPerson() имеет модификатор private inline, поэтому у анонимного объекта будут доступны только унаследованные свойства и функции от класса Person, но собственные свойства и функции будут не доступны.
Обобщенные типы (generic types) представляют типы, в которых типы объектов параметризированы. Что это значит? Рассмотрим следующий класс:

Java:
class Person<T>(val id: T, val name: String)
Класс Person использует параметр T. Параметры указываются после имени класса в угловых скобках. Данный параметр будет представлять некоторый тип данных, который на момент определения класса неизвестен.

В первичном конструкторе определяется свойство id, которое представляет идентификатор. Оно представляет тип, который передается через параметр T. На момент определения класса Person мы не знаем, что это будет за тип.

Само название параметра произвольное (если оно не совпадает с ключевыми словами). Но нередко используется T как сокращение от слова type.

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

Java:
fun main() {

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

    println("${tom.id} - ${tom.name}")
    println("${bob.id} - ${bob.name}")
}

class Person<T>(val id: T, val name: String)
Для типизации объекта после названия типа в угловых скобках указывается конкретный тип:

Java:
val tom: Person<Int>
В данном случае мы говорим, что параметр T фактически будет представлять тип Int. Поэтому в конструктор объекта Person для свойства id необходимо передать числовое значение Int:

Java:
Person(367, "Tom")
Второй объект типизируется типом String, поэтому в конструкторе для свойства id передается строка:

Java:
val bob: Person<String> = Person("A65", "Bob")
Если конструктор использует параметр T, то в принципе мы можем не указывать, каким типом типизируется объект - данный тип будет выводиться из типа параметра конструктора:

Java:
val tom = Person(367, "Tom")
val bob = Person("A65", "Bob")
При этом параметры типа могут широко применяться внутри класса, не только при определении свойств, но и в функциях:

Java:
fun main() {

    val tom = Person("qwrtf2", "Tom")
    tom.checkId("qwrtf2")   // The same
    tom.checkId("q34tt")    // Different
}

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

    fun checkId(_id: T){
        if(id == _id){
            println("The same")
        }
        else{
            println("Different")
        }
    }
}
Здесь класс Person определяет функцию checkId(), которая проверяет, равен ли id значению параметра _id. При этом параметр _id имеет тип T - то есть он будет представлять тот же тип, что и свойство id.

Стоит отметить, что generic-типы широко используются в Kotlin. Самый показательный пример, который представлен классом - Array<T>. Параметр класса определяет, элементы какого типа массив будет хранить:

Java:
val people: Array<String> = arrayOf("Tom", "Bob", "Sam")
val numbers: Array<Int> = arrayOf(1, 2, 3, 4)
Можно одновременно использовать несколько параметров:

Java:
fun main() {

    var word1: Word<String, String> = Word("one", "один")
    var word2: Word<String, Int> = Word("two", 2)

    println("${word1.source} - ${word1.target}")    // one - один
    println("${word2.source} - ${word2.target}")    // two - 2
}

class Word<K, V>(val source: K, var target: V)
В данном случае класс Word применяет два параметра - K и V. При создании объекта Word эти параметры могут представлять один и тот же тип, а могут представлять и разные типы.
Функции, как и классы, могут быть обобщенными.

Java:
fun main() {

    display("Hello Kotlin")
    display(1234)
    display(true)
}
fun <T> display(obj: T){
    println(obj)
}
Функция display() параметризирована параметром T. Параметр также указывается в угловых скобках после слова fun и перед названием функции. Функция принимает один параметр типа T и выводит его значение на консоль. И при использовании функции мы можем передавать в нее данные любых типов.

Другой более практический пример - определим функцию, которая будет возвращать наибольший массив:

Java:
fun main() {

    val arr1 = getBiggest(arrayOf(1,2,3,4), arrayOf(3, 4, 5, 6, 7, 7))
    arr1.forEach { item -> print("$item ") }    // 3  4  5  6  7  7

    println()
  
    val arr2 = getBiggest(arrayOf("Tom", "Sam", "Bob"), arrayOf("Kate", "Alice"))
    arr2.forEach { item -> print("$item ") }    // Tom  Sam  Bob
}

fun <T> getBiggest(args1: Array<T>, args2: Array<T>): Array<T>{
    if(args1.size > args2.size) return args1
    else return  args2
}
Здесь функция getBiggest() в качестве параметров принимает два массива. При этом мы точно не значем, объекты какого типа эти массивы будут содержать. Однако оба массива типизированы параметром T, что гарантирует, что оба массива будут хранить объекты одного и того же типа. Внутри функции сравниваем размер массивов с помощью их свойства size и возвращаем наибольший массив.
Ограничения обобщений (generic constraints) ограничивают набор типов, которые могут передаваться вместо параметра в обобщениях.

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

Java:
fun <T> getBiggest(a: T, b: T): T{
    if(a > b) return a    // ! Ошибка
    else return b
}
Но компилятор не скомпилирует эту функцию, потому что вместо параметра типа T могут передаваться самые различные типы, в том числе такие, которые не поддерживают операцию сравнения.

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

Java:
fun main() {

    val result1 = getBiggest(1, 2)
    println(result1)
    val result2 = getBiggest("Tom", "Sam")
    println(result2)

}
fun <T: Comparable<T>> getBiggest(a: T, b: T): T{
    return if(a > b) a
    else b
}
Ограничение указывается после названия параметра через двоеточие: <T: Comparable<T>> - то есть в данном случае тип T ограничен типом Comparable<T>, иначе говоря должен представлять тип Comparable<T>. Причем тип Comparable сам является обобщенным.

Стоит отметить, что по умолчанию ко всем параметрам типа также применяется ограничение в виде типа Any?. То есть определение параметра типа <T> фактически аналогично определению <T: Any?>

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

Java:
fun<T:Message> send(message: T){
    println(message.text)
}

interface Message{
    val text: String
}
class EmailMessage(override val text: String): Message
class SmsMessage(override val text: String): Message
Здесь определен интерфейс Message, который имеет одно свойство - text и представляет условное сообщение. И также есть два класса, которые реализуют этот интерфейс: EmailMessage и SmsMessage.

Функция send() использует ограничение <T:Message>, то есть она принимает объект некоторого типа, который должен реализовать интерфейс Message.

Далее мы можем вызвать эту функцию, передав ей соответствующий объект:

Java:
fun main() {
    val email1 = EmailMessage("Hello Kotlin")
    send(email1)
    val sms1 = SmsMessage("Привет, ты спишь?")
    send(sms1)
}
Здесь в обоих вызовах функция send() ожидает объект Message. Однако мы можем указать точный тип, используемый функцией
В примере выше мы могли передавать в функцию getBiggest() любой объект, который реализует интерфейс Comparable. Но что, если мы хотим, чтобы функция могла сравнивать только числа? Все числовые типы данных наследуются от базового класса Number. И мы можем задать еще одно ограничение - чтобы сравниваемый объект представлял тип Number:

Java:
fun <T> getBiggest(a: T, b: T): T where T: Comparable<T>,
                                        T: Number {
    return if(a > b) a
    else b
}
Если параметра типа надо установить несколько ограничений, то все они указываются после возвращаемого типа функции после слова where через запятую в форме:

Java:
параметр_типа: ограничений
И в этом случае мы сможем передать в функцию объекты, которые одновременно реализуют интерфейс Comparable и являются наследниками класса Number:

Java:
fun main() {

    val result1 = getBiggest(1, 2)
    println(result1)    // 2

    val result2 = getBiggest(1.6, -2.8)
    println(result2)    // 1.6
  
    // val result3 = getBiggest("Tom", "Sam")  // ! Ошибка - String не является производным от класса Number
    // println(result3)
}
Подобным образом мы можем использовать собственные типы в качестве ограничений:

Java:
fun main() {
    val email1 = EmailMessage("Hello Kotlin")
    send(email1)
    val sms1 = SmsMessage("Привет, ты спишь?")
    send(sms1)
}
fun<t> send(message: T) where T: Message, T: Logger{
    message.log()
}

interface Message{ val text: String }
interface Logger{ fun log() }

class EmailMessage(override val text: String): Message, Logger{
    override fun log() = println("Email: $text")
}
class SmsMessage(override val text: String): Message, Logger{
    override fun log() = println("SMS: $text")
}
</t>
Здесь для функции send() установлено два ограничения: используемый параметр типа T должен представлять одновременно оба интерфейса - Message и Logger.
Классы, как и функции, могут принимать ограничения обощений. Например, установка одного ограничения:

Java:
class Messenger<T:Message>(){
    fun send(mes: T){
        println(mes.text)
    }
}
Установка нескольких ограничений:

Java:
fun main() {
    val email1 = EmailMessage("Hello Kotlin")
    val outlook = Messenger<EmailMessage>()
    outlook.send(email1)

    val skype = Messenger<SmsMessage>()
    val sms1 = SmsMessage("Привет, ты спишь?")
    skype.send(sms1)
}
class Messenger<T>() where T: Message, T: Logger{
    fun send(mes: T){
        mes.log()
    }
}
interface Message{ val text: String }
interface Logger{ fun log() }

class EmailMessage(override val text: String): Message, Logger{
    override fun log() = println("Email: $text")
}
class SmsMessage(override val text: String): Message, Logger{
    override fun log() = println("SMS: $text")
}
Здесь стоит обратить внимание, что поскольку конструктор класса Messenger не принимает параметров типа T, то нам надо явным образом указать, какой именно тип будет использоваться:

Java:
val outlook = Messenger<EmailMessage>()
В качестве альтернативы можно было бы явным образом указать тип переменной:

Java:
val outlook: Messenger<EmailMessage> = Messenger()
Вариантность описывает, как обобщенные типы, типизированные классами из одной иерархии наследования, соотносятся друг с другом.
Инвариантность предполагает, что, если у нас есть классы Base и Derived, где Derived - производный класс от Base, то класс C<Base> не является ни базовым классом для С<Derived>, ни производным. Например, у нас есть следующие типы:

Java:
interface Messenger<T: Message>()

open class Message(val text: String)
class EmailMessage(text: String): Message(text)
В данном случае мы не можем присвоить объект Messenger<EmailMessage> переменной типа Messenger<Message> и наоборот, они никак между собой не соотносятся, несмотря на то, что EmailMessage наследуется от Message:

Java:
fun changeMessengerToEmail(obj: Messenger<EmailMessage>){
    val messenger: Messenger<Message> = obj    // ! Ошибка
}
fun changeMessengerToDefault(obj: Messenger<Message>){
    val messenger: Messenger<EmailMessage> = obj        // ! Ошибка
}
Мы можем присвоить переменным по умолчанию только объекты их типов:

Java:
fun changeMessengerToDefault(obj: Messenger<Message>){
    val messenger: Messenger<Message> = obj
}
fun changeMessengerToEmail(obj: Messenger<EmailMessage>){
    val messenger: Messenger<EmailMessage> = obj
}
Ковариантость предполагает, что, если у нас есть классы Base и Derived, где Base - базовый класс для Derived, то класс SomeClass<Base> является базовым классом для SomeClass<Derived>

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

Java:
interface Messenger<out T: Message>
open class Message(val text: String)
class EmailMessage(text: String): Message(text)
В данном случае интерфейс Messenger является ковариантным, так как его параметр определен со словом out: interface Messenger<out T>. И теперь переменной типа Messenger<Message> мы можем присвоить значение типа Messenger<EmailMessage>

Java:
fun changeMessengerToEmail(obj: Messenger<EmailMessage>){
    val messenger: Messenger<Message> = obj
}
Вообще не случайно используется именно слово out. Оно указывает, что обобщенный тип может возвращать из функции значение типа T:

Java:
fun main() {

    val messenger: Messenger<Message> = EmailMessenger()
    val message = messenger.writeMessage("Hello Kotlin")
    println(message.text)    // Email: Hello Kotlin
}
open class Message(val text: String)
class EmailMessage(text: String): Message(text)

interface Messenger<out T: Message>{
    fun writeMessage(text: String): T
}
class EmailMessenger(): Messenger<EmailMessage>{
    override fun writeMessage(text: String): EmailMessage {
        return EmailMessage("Email: $text")
    }
}
В данном случае обобщенный интерфейс Messenger определяет функцию writeMessage() для генерации объекта Message. Класс EmailMessenger применяет интерфейс Messenger и реализует эту функцию. То есть в данном случае тип EmailMessenger по сути представляет тип Messenger<EmailMessage>.

Поскольку в Messenger параметр T определен с аннотацией out, то мы можем присвоить переменной типа Messenger<Message> значение типа EmailMessenger (а по сути значение типа Messenger<EmailMessage>)

Java:
val messenger: Messenger<Message> = EmailMessenger()
В то же время тип T нельзя использовать в качестве типа входных параметров функции. Например, в следующем случае компилятор известит нас об ошибке:

Java:
interface Messenger<out T: Message>{
    fun writeMessage(text: String): T
    fun sendMessage(message: T)     // Ошибка - тип T может представлять только возвращемый тип
}
Контравариантость предполагает в какой-то степени обратную ситуацию. Контравариантость предполагает, что, если у нас есть классы Base и Derived, где Base - базовый класс для Derived, то объекту SomeClass<Derived> мы можем присвоить значение SomeClass<Base> (при ковариантности, наоборот, - объекту SomeClass<Base> можно присвоить значение SomeClass<Derived>)

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

Java:
interface Messenger<in T: Message>
open class Message(val text: String)
class EmailMessage(text: String): Message(text)
В данном случае интерфейс Messenger является контравариантным, так как его параметр определен со словом in: interface Messenger<in T>. И теперь переменной типа Messenger<EmailMessage> мы можем присвоить значение типа Messenger<Message>

Java:
fun changeMessengerToDefault(obj: Messenger<Message>){
    val messenger: Messenger<EmailMessage> = obj
}
Применение аннотации in означает, что обобщенный тип может получать значение типа T через параметр функции:

Java:
fun main() {

    val messenger: Messenger<EmailMessage> = InstantMessenger() // InstantMessenger - это Messenger<Message>

    val message = EmailMessage("Hi Kotlin")
    messenger.sendMessage(message)
}
open class Message(val text: String)
class EmailMessage(text: String): Message(text)

interface Messenger<in T: Message>{
    //fun writeMessage(text: String): T
    fun sendMessage(message: T)
}

class InstantMessenger(): Messenger<Message>{
    override fun sendMessage(message: Message){
        println("Send message: ${message.text}")
    }
}
В данном случае обобщенный интерфейс Messenger определяет функцию sendMessage(), которая принимает объект Message в качестве параметра. Класс InstantMessenger применяет интерфейс Messenger и реализует эту функцию. То есть в данном случае тип InstantMessenger по сути представляет тип Messenger<Message>.

Поскольку в интерфейсе Messenger параметр T определен с аннотацией in, то мы можем присвоить переменной типа Messenger<EmailMessage> значение типа InstantMessenger (то есть значение типа Messenger<Message>)

Java:
val messenger: Messenger<EmailMessage> = InstantMessenger()
В то же время тип T нельзя использовать в качестве типа результата функции. Например, в следующем случае компилятор известит нас об ошибке:

Java:
interface Messenger<in T: Message>{
    fun writeMessage(text: String): T   // Ошибка - тип T может представлять только параметр функции
    fun sendMessage(message: T)
}
Исключение представляет событие, которое возникает при выполнении программы и нарушает ее нормальной ход. Например, при передаче файла по сети может оборваться сетевое подключение, и в результате чего может быть сгенерировано исключение. Если исключение не обработано, то программа падает и прекращает свою работу. Поэтому при возникновении исключений их следует обрабатывать.

Для обработки исключений применяется конструкция try..catch..finally. В блок try помещаются те действия, которые потенциально могут вызвать исключение (например, передача файла по сети, открытие файла и т.д.). Блок catch перехватывает возникшее исключение и обрабатывает его. Блок finally выполняет некоторые завершающие действия.

Java:
try {
    // код, генерирующий исключение
}
catch (e: Exception) {
    // обработка исключения
}
finally {
    // постобработка
}
После оператора catch в скобках помещается параметр, который представляет тип исключения. Из этого параметра можно получить информацию о произошедшем исключении.

Блок finally является необязательным, его можно опустить. Блок catch также может отсутствовать, однако обязательно должен быть блок try и как минимум один из блоков: либо catch, либо finally. Также конструкция может содержать несколько блоков catch для обработки каждого типа исключения, которое может возникнуть.

Блок catch выполняется, если только возникло исключение. Блок finally выполняется в любом случае, даже если нет исключения.

Например, при делении на ноль Kotlin генерирует исключение:

Java:
fun main() {

   try{
       val x : Int = 0
       val z : Int = 0 / x
       println("z = $z")
   }
   catch(e: Exception){
       println("Exception")
       println(e.message)
   }
}
Действие, которое может вызвать исключение, то есть операция деления, помещается в блок try. В блоке catch перехватываем исключение. При этом каждое исключение имеет определенный тип. В данном случае используется общий тип исключений - класс Exception:
Код:
Exception
Если необходимы какие-то завершающие действия, то можно добавить блок finally (например, если при работе с файлом возникает исключение, то в блоке finally можно прописать закрытие файла):

Java:
try{
    val x : Int = 0
    val z : Int = 0 / x
    println("z = $z")
}
catch(e: Exception){
    println("Exception")
    println(e.message)
}
finally{
    println("Program has been finished")
}
В этом случае консольный вывод будет выглядеть следующим образом:

Код:
Exception
Program has been finished
Базовый класс исключений - класс Exception предоставляет ряд свойств, которые позволяют получить различную информацию об исключении:
  • message: сообщение об исключении
  • stackTrace: трассировка стека исключения - набор строк, где было сгенерировано исключение

Из функций класса Exception следует выделить функцию printStackTrace(), которая выводит ту информацию, которая обычно отображается при необработанном исключении.

Применение свойств:

Java:
fun main() {

    try{
        val x : Int = 0
        val z : Int = 0 / x
        println("z = $z")
    }
    catch(e: Exception){
        println(e.message)
        for(line in e.stackTrace) {
            println("at $line")
        }
    }
}
Консольный вывод программы:

Код:
/ by zero
at AppKt.main(app.kt:5)
at AppKt.main(app.kt)
Одна программа, один код может генерировать сразу несколько исключений. Для обработки каждого отдельного типа исключений можно определить отдельный блок catch. Например, при одном исключении мы хотим производить одни действия, при другом - другие.

Java:
try {
    val nums = arrayOf(1, 2, 3, 4)
    println(nums[6])
}
catch(e:ArrayIndexOutOfBoundsException){
    println("Out of bound of array")
}
catch (e: Exception){
    println(e.message)
}
В данном случае при доступе по недействительному индексу в массиве будет генерироваться исключение типа ArrayIndexOutOfBoundsException. С помощью блока catch(e:ArrayIndexOutOfBoundsException). Если в программе будут другие исключения, которые не представляют тип ArrayIndexOutOfBoundsException, то они будут обрабатываться вторым блоком catch, так как Exception - это общий тип, который подходит под все типы исключений. При этом стоит отметить, что в начале обрабатывается исключение более частного типа - ArrayIndexOutOfBoundsException, и только потом - более общего типа Exception.
Возможно, в каких-то ситуациях мы вручную захотим генерировать исключение. Для генерации исключения применяется оператор throw, после которого указывается объект исключения

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

Java:
fun main() {

    val checkedAge1 = checkAge(5)
    val checkedAge2 = checkAge(-115)
}
fun checkAge(age: Int): Int{
    if(age < 1 || age > 110) throw  Exception("Invalid value $age. Age must be greater than 0 and less than 110")
    println("Age $age is valid")
    return age
}
После оператора throw указан объект исключения. Для определения объекта Exception применяется конструктор, который принимает в качестве параметра сообщение об исключении. В данном случае это сообщение о некорректности введенного значения.

И если при вызове функции checkAge() в нее будет передано число меньше 1 или больше 110, то будет сгенерировано исключение. Так, в данном случае консольный вывод будет следующим:

Java:
Age 5 is valid
Exception in thread "main" java.lang.Exception: Invalid value -115. Age must be greater than 0 and less than 110
    at AppKt.checkAge(app.kt:7)
    at AppKt.main(app.kt:4)
    at AppKt.main(app.kt)
Но опять же поскольку генерируемое здесь исключение не обаботано, то программа при генерации исключения аварийно завершает работу. Чтобы этого не произошло, мы можем обработать генерируемое исключение:

Java:
fun main() {
    try {
        val checkedAge1 = checkAge(5)
        val checkedAge2 = checkAge(-115)
    }
    catch (e: Exception){
        println(e.message)
    }
}
fun checkAge(age: Int): Int{
    if(age < 1 || age > 110) throw  Exception("Invalid value $age. Age must be greater than 0 and less than 110")
    println("Age $age is valid")
    return age
}
Конструкция try может возвращать значение. Например:

Java:
fun main() {
    val checkedAge1 = try { checkAge(5) } catch (e: Exception) { null }
    val checkedAge2 = try { checkAge(-125) } catch (e: Exception) { null }
    println(checkedAge1)    // 5
    println(checkedAge2)    // null
}
fun checkAge(age: Int): Int{
    if(age < 1 || age > 110) throw  Exception("Invalid value $age. Age must be greater than 0 and less than 110")
    println("Age $age is valid")
    return age
}
В данном случае переменная checkedAge1 получает результат функцию checkAge(). Если же произойдет исключение, тогда переменная checkedAge1 получает то значение, которое указано в блоке catch, то есть в данном случае значение null.

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

Java:
fun main() {
    val checkedAge2 = try { checkAge(-125) } catch (e: Exception) { println(e.message); 18 }
    println(checkedAge2)
}
fun checkAge(age: Int): Int{
    if(age < 1 || age > 110) throw  Exception("Invalid value $age. Age must be greater than 0 and less than 110")
    println("Age $age is valid")
    return age
}
В данном случае, если будет сгенерировано исключение, то конструкция try выведет исключение и возвратит число 18. Возвращаемое значение указывается после всех остальных инструкций в блоке catch.
Ключевое слово null представляет специальный литерал, который указывает, что переменная не имеет как такового значения. То есть у нее по сути отсутствует значение.

Java:
val n = null
println(n)  // null
Подобное значение может быть полезно в ряде ситуациях, когда необходимо использовать данные, но при этом точно неизвестно, а есть ли в реальности эти данные. Например, мы получаем данные по сети, данные могут прийти или не прийти. Либо может быть ситуация, когда нам надо явным образом указать, что данные не установлены.

Однако переменным стандартных типов, например, типа Int или String или любых других классов, мы не можем просто взять и присвоить значение null:

Java:
val n : Int = null   // ! Ошибка, переменная типа Int допускает только числа
Мы можем присвоить значение null только переменной, которая представляет тип Nullable. Чтобы превратить обычный тип в тип nullable, достаточно поставить после названия типа вопросительный знак:

Java:
// val n : Int = null  //! ошибка, Int не допускает значение null
val d : Int? = null // норм, Int? допускает значение null
При этом мы можем передавать переменным nullable-типов как значение null, так и конкретные значения, которые укладываются в диапазон значений данного типа:

Java:
var age : Int? = null
age = 34              // Int? допускает null и числа
var name : String? = null
name = "Tom"        // String? допускает null и строки
Nullable-типы могут представлять и создаваемые разработчиком классы:

Java:
fun main() {
  
    var bob: Person = Person("Bob")
    // bob = null // ! Ошибка - bob представляет тип Person и не допускает null
    var tom: Person? = Person("Tom")
    tom = null  // норм - tom представляет тип Person? и допускает null
}
class Person(val name: String)
В то же время надо понимать, что String? и Int? - это не то же самое, что и String и Int. Nullable типы имеют ряд ограничений:

  • Значения nullable-типов нельзя присвоить напрямую переменным, которые не допускают значения null
    Java:
    var message : String? = "Hello"val hello: String = message     // ! Ошибка - hello не допускает значение null
  • У объектов nullable-типов нельзя вызвать напрямую те же функции и свойства, которые есть у обычных типов
    Java:
    var message : String? = "Hello"// у типа String свойство length возвращает длину строки
    println("Message length: ${message.length}") // ! Ошибка
  • Нельзя передавать значения nullable-типов в качестве аргумента в функцию, где требуется конкретное значение, которое не может представлять null
Одним из преимуществ Kotlin состоит в том, что его система типов позволяет определять проблемы, связанные с использованием null, во время компиляции, а не во время выполнения. Например, возьмем следующий код:

Java:
var name : String?  = "Tom"
val userName: String = name // ! Ошибка
Переменная name хранит строку "Tom". Переменная userName представляет тип String и тоже может хранить строки, но тем не менее напрямую в данном случае мы не можем передать значение из переменной name в userName. В данном случае для компилятора неизвестно, каким значением инициализирована переменная name. Ведь переменная name может содержать и значение null, которое недопустимо для типа String.

В этом случае мы можем использовать оператор ?:, который позволяет предоставить альтернативное значение, если присваиваемое значение равно null:

Java:
var name : String?  = "Tom"
val userName: String = name ?: "Undefined"  // если name = null, то присваивается "Undefined"

var age: Int? = 23
val userAge: Int = age ?:0  // если age равно null, то присваивается число 0
Оператор ?: принимает два операнда. Если первый операнд не равен null, то возвращается значение первого операнда. Если первый операнд равен null, то возвращается значение второго операнда.

То есть это все равно, если бы мы написали:

Java:
var name : String?  = "Tom"
val userName: String
if(name!=null){

    userName = name
}
Но оператор ?: позволяет сократить подобную конструкцию.
Оператор ?. позволяет объединить проверку значения объекта на null и обратиться к функциям или свойствам этого объекта.

Например, у строк есть свойство length, которое возвращает длину строки в символах. У объекта String? мы просто так не можем обратиться к свойству length, так как если объект String? равен null, то и строки как таковой нет, и соответственно длину строки нельзя определить. И в этом случае мы можем применить оператор ?.:

Java:
var message : String? = "Hello"
val length: Int? = message?.length
Если переменная message вдруг равна null, то переменная length получит значение null. Если переменная name содержит строку, то возвращается длина этой строки. По сути выражение val length: Int? = message?.length эквивалентно следующему коду:

Java:
val length: Int?
if(message != null)
    length = message.length
else
    length = null
С помощью оператора ?. подобным образом можно обращаться к любым свойствам и функциям объекта.

Также в данном случае мы могли совместить оба выше рассмотренных оператора:

Java:
val message : String?  = "Hello"
val length: Int = message?.length ?:0
Теперь переменная length не допускает значения null. И если переменная name не определена, то length получает число 0.

Используя этот оператор, можно создавать цепочки проверок на null:

Java:
fun main() {

    var tom: Person? = Person("Tom")
    val tomName: String? = tom?.name?.uppercase()
    println(tomName)        // TOM

    var bob: Person? = null
    val bobName: String? = bob?.name?.uppercase()
    println(bobName)        // null

    var sam: Person? = Person(null)
    val samName: String? = sam?.name?.uppercase()
    println(samName)        // null

}
class Person(val name: String?)
Здесь класс Person в первичном конструкторе принимает значение типа String?, то есть это можт быть строка, а может быть null.

Допустим, мы хотим получить переданное через конструктор имя пользователя в верхнем регистре (заглавными буквами). Для перевода текста в верхний регистр у класса String есть функция uppercase(). Однако может сложиться ситуация, когда либо объект Person равен null, либо его свойство name ( которое представляет тип String?) равно null. И в этом случае перед вызовом функции uppercase() нам надо проверять на null все эти объекты. А оператор ?. позволяет сократить код проверки:

Java:
val tomName: String? = tom?.name?.uppercase()
То есть если tom не равен null, то обращаемся к его свойству name. Далее если name не равен null, то обращаемся к ее функции uppercase(). Если какое-то звено в этой проверки возвратит null, переменная tomName тоже будет равна null.

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

Java:
val tomName: String = tom?.name?.uppercase() ?: "Undefined"
Оператор !! (not-null assertion operator) принимает один операнд. Если операнд равен null, то генерируется исключение. Если операнд не равен null, то возвращается его значение.

Java:
fun main() {
    try {
        val name : String?  = "Tom"
        val id: String = name!!
        println(id)
    } catch (e: Exception) { println(e.message)}
}
Поскольку данный оператор возвращает объект, который не представляет nullable-тип, то после применения оператора мы можем обратиться к методам и свойствам этого объекта:

Java:
val name : String?  = null
val length :Int = name!!.length





Завтра дополню.
Progress( 6.3 - 9.9 )
CopyRight: Библиотека metanit.
 
Последнее редактирование:
Сверху Снизу