- Регистрация
- 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, чрезвычайно широк - 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.
Далее определим в этом файле код, который будет выводить некоторое сообщение на консоль:
Точкой входа в программу на Kotlin является функция main. Для определения функции применяется ключевое слово fun, после которого идет название функции - то есть main. Даннуя функция не принимает никаких параметров, поэтому после названия функции указываются пустые скобки.
Далее в фигурных скобках определяются собственно те действия, которые выполняет функция main. В данном случае внутри функции main выполняется другая функция - println(), которая выводит некоторое сообщение на консоль.
Откроем командную строку. Вначале с помощью команды cd перейдем к папке, где находится файл app.kt. Затем для компиляции программы введем следующую команду:
В данном случае мы передаем компилятору c:\kotlin\bin\kotlinc для компиляции файл app.kt. (Чтобы не писать полный путь к компилятору, путь к нему можно добавить в переменную PATH в переменных среды). Далее с помощью параметра -include-runtime указывается, что создаваемый файл будет включать среду Kotlin. А параметр -d указывает, как будет называться создаваемый файл приложения, то есть в данном случае это будет app.jar.
После выполнения этой команды будет создан файл app.jar. Теперь запустим его на выполнение. Для этого введем команду:
В данном случае считается, что путь к JDK, установленном на компьютере, прописан в переменной PATH в переменных среды. Иначе вместо "java" придется писать полный путь к утилите java.
В итоге при запуске файла мы увидим на консоли строку "Hello Kotlin"
Кроме того, необходимо установить 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")
}
Далее в фигурных скобках определяются собственно те действия, которые выполняет функция main. В данном случае внутри функции main выполняется другая функция - println(), которая выводит некоторое сообщение на консоль.
Откроем командную строку. Вначале с помощью команды cd перейдем к папке, где находится файл app.kt. Затем для компиляции программы введем следующую команду:
Bash:
c:\kotlinc\bin\kotlinc app.kt -include-runtime -d app.jar
После выполнения этой команды будет создан файл app.jar. Теперь запустим его на выполнение. Для этого введем команду:
Bash:
java -jar app.jar
В итоге при запуске файла мы увидим на консоли строку "Hello Kotlin"
Точкой входа в программу на языке Kotlin является функция main. Именно с этой функции начинается выполнение программы на Kotlin, поэтому эта функция должна быть в любой программе на языке Kotlin.
Так, в прошлой теме была определена следующая функция main:
Определение функции main() (в принципе как и других функций в Kotlin) начинается с ключевого слова fun. По сути оно указывает, что дальше идет определение функции. После fun указывается имя функции. В данном случае это main.
После имени функции в скобках идет список параметров функции. Здесь функция main не принимает никаких параметров, поэтому после имени функции идут пустые скобки.
Все действия, которые выполняет функция, заключаются в фигурные скобки. В данном случае единственное, что делает функция main, - вывод на консоль некоторого сообщения с помощью другой встроенной функции println().
Стоит отметить, что до версии 1.3 в Kotlin функция main должна была принимать параметры:
Параметр args: Array<String> представляет массив строк, через который в программу можно передать различные данные.
Начиная с версии 1.3 использовать это определение функции с параметрами необязательно. Хотя мы можем его использовать.
Так, в прошлой теме была определена следующая функция main:
Java:
fun main(){
println("Hello Kotlin")
}
После имени функции в скобках идет список параметров функции. Здесь функция main не принимает никаких параметров, поэтому после имени функции идут пустые скобки.
Все действия, которые выполняет функция, заключаются в фигурные скобки. В данном случае единственное, что делает функция main, - вывод на консоль некоторого сообщения с помощью другой встроенной функции println().
Стоит отметить, что до версии 1.3 в Kotlin функция main должна была принимать параметры:
Java:
fun main(args: Array<String>) {
println("Hello Kotlin")
}
Начиная с версии 1.3 использовать это определение функции с параметрами необязательно. Хотя мы можем его использовать.
Основным строительным блоком программы на языке Kotlin являются инструкции (statement). Каждая инструкция выполняет некоторое действие, например, вызовы функций, объявление переменных и присвоение им значений. Например:
Данная строка представляет встроенной функции println(), которая выводит на консоль, некоторое сообщение (в данном случае строку "Hello Kotlin!").
Стоит отметить, что в отличие от других похожих языков программирования, например, Java, в Kotlin не обязательно ставить после инструкции точку запятой. Каждая инструкция просто размещается на новой строке:
Тем не менее, если инструкции располагаются на одной строке, то чтобы их отделить друг от друга, надо указывать после инструкции точку с запятой:
Java:
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 есть два типа комментариев: однострочный и многострочный. Однострочный комментарий размещается на одной строке после двойного слеша //. А многострочный комментарий заключается между символами /* текст комментария */. Он может размещаться на нескольких строках. Например:
В Kotlin есть два типа комментариев: однострочный и многострочный. Однострочный комментарий размещается на одной строке после двойного слеша //. А многострочный комментарий заключается между символами /* текст комментария */. Он может размещаться на нескольких строках. Например:
Java:
/*
многострочный комментарий
Функция main -
точка входа в программу
*/
fun main(){ // начало функции main
println("Hello Kotlin") // вывод строки на консоль
} // конец функции main
Для хранения данных в программе в Kotlin, как и в других языках программирования, применяются переменные. Переменная представляет именованный участок памяти, который хранит некоторое значение.
Каждая переменная характеризуется определенным именем, типом данных и значением. Имя переменной представляет поизвольный идентификатор, который может содержать алфавитно-цифровые символы или символ подчеркивания и должен начинаться либо с алфавитного символа, либо со знака подчеркивания. Для определения переменной можно использовать либо ключевое слово val, либо ключевое слово var.
Формальное определение переменной:
Вначале идет слово val или var, затем имя переменной и через двоеточие тип переменной.
Например, определим переменную age:
То есть в данном случае объявлена переменная age, которая имеет тип Int. Тип Int говорит о том, что переменная будет содержать целочисленные значения.
После определения переменной ей можно присвоить значение:
Для присвоения значения переменной используется знак равно. Затем мы можем производить с переменной различные операции. Например, в данном случае с помощью функции println() значение переменной выводится на консоль. И при запуске этой программы на консоль будет выведено число 23.
Присвоение значения переменной должно производиться только после ее объявления. И также мы можем сразу присвоить переменной начальное значение при ее объявлении. Такой прием называется инициализацией:
Однако обязательно надо присвоить переменной некоторое значение до ее использования:
Каждая переменная характеризуется определенным именем, типом данных и значением. Имя переменной представляет поизвольный идентификатор, который может содержать алфавитно-цифровые символы или символ подчеркивания и должен начинаться либо с алфавитного символа, либо со знака подчеркивания. Для определения переменной можно использовать либо ключевое слово val, либо ключевое слово var.
Формальное определение переменной:
JavaScript:
val|var имя_переменной: тип_переменной
Например, определим переменную age:
Java:
val age: Int
После определения переменной ей можно присвоить значение:
Java:
fun main() {
val age: Int
age = 23
println(age)
}
Присвоение значения переменной должно производиться только после ее объявления. И также мы можем сразу присвоить переменной начальное значение при ее объявлении. Такой прием называется инициализацией:
Java:
fun main() {
val age: Int = 23
println(age)
}
Java:
fun main() {
val age: Int
println(age)// Ошибка, переменная не инициализирована
}
Выше было сказано, что переменные могут объявляться как с помощью слова val, так и с помощью слова var. В чем же разница между двумя этими способами?
С помощью ключевого слова val определяется неизменяемая переменная (immutable variable). То есть мы можем присвоить значение такой переменной только один раз, но изменить его после первого присвоения мы уже не сможем. Например, в следующем случае мы получим ошибку:
А у переменной, которая определена с помощью ключевого слова var мы можем многократно менять значения (mutable variable):
Поэтому если не планируется изменять значение переменной в программе, то лучше определять ее с ключевым словом val.
С помощью ключевого слова val определяется неизменяемая переменная (immutable variable). То есть мы можем присвоить значение такой переменной только один раз, но изменить его после первого присвоения мы уже не сможем. Например, в следующем случае мы получим ошибку:
Java:
fun main() {
val age: Int
age = 23 // здесь норм - первое присвоение
age = 56 // здесь ошибка - переопределить значение переменной нельзя
println(age)
}
Java:
fun main() {
var age: Int
age = 23
println(age)
age = 56
println(age)
}
В Kotlin все компоненты программы, в том числе переменные, представляют объекты, которые имеют определенный тип данных. Тип данных определяет, какой размер памяти может занимать объект данного типа и какие операции с ним можно производить. В Kotlin есть несколько базовых типов данных: числа, символы, строки, логический тип и массивы.
Для передачи значений объектам, которые представляют беззнаковые целочисленные типы данных, после числа указывается суффикс U:
Кроме чисел в десятичной системе мы можем определять числа в двоичной и шестнадцатеричной системах.
Шестнадцатеричная запись числа начинается с 0x, затем идет набор символов от 0 до F, которые представляют число:
Двоичная запись числа предваряется символами 0b, после которых идет последовательность из нулей и единиц:
- Byte: хранит целое число от -128 до 127 и занимает 1 байт
- Short: хранит целое число от -32 768 до 32 767 и занимает 2 байта
- Int: хранит целое число от -2 147 483 648 (-231) до 2 147 483 647 (231 - 1) и занимает 4 байта
- Long: хранит целое число от –9 223 372 036 854 775 808 (-263) до 9 223 372 036 854 775 807 (263-1) и занимает 8 байт
- UByte: хранит целое число от 0 до 255 и занимает 1 байт
- UShort: хранит целое число от 0 до 65 535 и занимает 2 байта
- UInt: хранит целое число от 0 до 232 - 1 и занимает 4 байта
- ULong: хранит целое число от 0 до 264-1 и занимает 8 байт
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
}
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
Java:
val a: Int = 0b0101 // 5
val b: Int = 0b1011 // 11
println(a) // 5
println(b) // 11
Кроме целочисленных типов в Kotlin есть два типа для чисел с плавающей точкой, которые позволяют хранить дробные числа:
Чтобы присвоить число объекту типа Float после числа указывается суффикс f или F.
Также тип Double поддерживает экспоненциальную запись:
- 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
Также тип 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. Он представляет отдельный символ, который заключается в одинарные кавычки.
Также тип Char может представлять специальные последовательности, которые интерпретируются особым образом:
Java:
val a: Char = 'A'
val b: Char = 'B'
val c: Char = 'T'
- \t: табуляция
- \n: перевод строки
- \r: возврат каретки
- \': одинарная кавычка
- \": двойная кавычка
- \\: обратный слеш
Строки представлены типом String. Строка представляет последовательность символов, заключенную в двойные кавычки, либо в тройные двойные кавычки.
Строка может содержать специальные символы или эскейп-последовательности. Например, если необходимо вставить в текст перевод на другую строку, можно использовать эскейп-последовательность \n:
Для большего удобства при создании многострочного текста можно использовать тройные двойные кавычки:
Java:
fun main() {
val name: String = "Eugene"
println(name)
}
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) представляют удобный способ вставки в строку различных значений, в частности, значений переменных. Так, с помощью знака доллара $ мы можем вводить в строку значения различных переменных:
В данном случае вместо $firstName и $lastName будут вставляться значения этих переменных. При этом переменные необязательно должны представлять строковый тип:
Java:
fun main() {
val firstName = "Tom"
val lastName = "Smith"
val welcome = "Hello, $firstName $lastName"
println(welcome) // Hello, Tom Smith
}
Java:
val name = "Tom"
val age = 22
val userInfo = "Your name: $name Your age: $age"
Kotlin позволяет выводить тип переменной на основании данных, которыми переменная инициализируется. Поэтому при инициализации переменной тип можно опустить:
В данном случае компилятор увидит, что переменной присваивается значение типа Int, поэтому переменная age будет представлять тип Int.
Соответственно если мы присваиваем переменной строку, то такая переменная будет иметь тип String.
Любые целые числа, воспринимаются как данные типа Int.
Если же мы хотим явно указать, что число представляет значение типа Long, то следует использовать суффикс L:
Если надо указать, что объект представляет беззнаковый тип, то применяется суффикс u или U:
Аналогично все числа с плавающей точкой (которые содержат точку в качестве разделителя целой и дробной части) рассматриваются как числа типа Double:
Если мы хотим указать, что данные будут представлять тип Float, то необходимо использовать суффикс f или F:
Однако нельзя сначала объявить переменную бз указания типа, а потом где-то в программе присвоить ей какое-то значение:
Java:
val age = 5
Соответственно если мы присваиваем переменной строку, то такая переменная будет иметь тип String.
Java:
val name = "Tom"
Если же мы хотим явно указать, что число представляет значение типа Long, то следует использовать суффикс L:
Java:
val sum = 45L
Java:
val sum = 45U
Java:
val height = 1.78
Java:
val height = 1.78F
Java:
val age // Ошибка, переменная не инициализирована
age = 5
Тип данных ограничивает набор значений, которые мы можем присвоить переменной. Например, мы не можем присвоить переменной типа Double строку:
И после того, как тип переменной установлен, он не может быть изменен:
Однако в Kotlin также есть тип Any, который позволяет присвоить переменной данного типа любое значение:
Java:
val height: Double = "1.78"
Java:
fun main() {
var height: String = "1.78"
height = 1.81 // !Ошибка - переменная height хранит только строки
println(height)
}
Java:
fun main() {
var name: Any = "Tom"
println(name) // Tom
name = 6758
println(name) // 6758
}
Для вывода информации на консоль в Kotlin есть две встроенные функции:
Обе эти функции принимают некоторый объект, который надо вывести на консоль, обычно это строка. Различие между ними состоит в том, что функция println() при выводе на консоль добавляет перевод на новую строку:
Причем функция println() необязательно должна принимать некоторое значения. Так, здесь применяется пустой вызов функции, который просто перевод консольный вывод на новую строку:
Консольный вывод программы:
Java:
print()
println()
Java:
fun main() {
print("Hello ")
print("Kotlin ")
print("on Metanit.com")
println()
println("Kotlin is a fun")
}
Java:
println()
Код:
Hello Kotlin on Metanit.com
Kotlin is a fun
Для ввода с консоли применяется встроенная функция readLine(). Она возвращает введенную строку. Стоит отметить, что результат этой функции всегда представляет объект типа String. Соответственно введеную строку мы можем передать в переменную типа String:
Здесь сначала выводится приглашение к вводу данных. Далее введенное значение передается в переменную name. Результат работы программы:
Подобным образом можно вводить разные данные:
Java:
fun main() {
print("Введите имя: ")
val name = readLine()
println("Ваше имя: $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 поддерживает базовые арифметические операции:
Так в данном случае, хотя если согласно стандартной математике разделить 11 на 5, то получится 2.2. Однако поскольку оба операнда представляют целочисленный тип, а именно тип Int, то дробная часть - 0.2 отрабрасывается, поэтому результатом будет число 2, а переменная z будет представлять тип Int.
Чтобы результатом было дробное число, один из операндов должен представлять число с плавающей точкой:
В данном случае переменная y представляет тип Double, поэтому результатом деления будет число 2.2, а переменная z также будет представлять тип Double
- + (сложение): возвращает сумму двух чисел.
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
}
Чтобы результатом было дробное число, один из операндов должен представлять число с плавающей точкой:
Java:
fun main() {
val x = 11
val y = 5.0
val z = x / y // z =2.2
println(z) // 2.2
}
- %: возвращает остаток от целочисленного деления двух чисел
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):
Есть следующие поразрядные операторы (они применяются только к данным типов 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
- shr: сдвиг битов числа со знаком вправо
Java:
val z = 12 shr 2 // z = 1100 >> 2 = 11println(z) // z = 3 val d = 0b1100 shr 2 println(d) // d = 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
- 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
А выражение 11..15 создает последовательность чисел от 11 до 15. И поскольку значение переменной с в эту последовательность не входит, поэтому возвращается false.
Если нам, наоборот, хочется возвращать true, если числа нет в указанной последовательности, то можно применить комбинацию операторов !in:
Java:val a = 8 val b = a !in 1..6 // true - число 8 не входит в последовательность от 1 до 6
Условные конструкции позволяют направить выполнение программы по одному из путей в зависимости от условия.
Конструкция if принимает условие, и если это условие истинно, то выполняется последующий блок инструкций.
В данном случае в конструкции if проверяется истинность выражения a == 10, если оно истинно, то выполняется последующий блок кода в фигурных скобках, и на консоль выводится сообщение "a равно 10". Если же выражение ложно, тогда блок кода не выполняется.
Если необходимо задать альтернативный вариант, то можно добавить блок else:
Таким образом, если условное выражение после оператора if истинно, то выполняется блок после if, если ложно - выполняется блок после else.
Если блок кода состоит из одного выражения, то в принципе фигурные скобки можно опустить:
Если необходимо проверить несколько альтернативных вариантов, то можно добавить выражения else if:
Java:
val a = 10
if(a == 10) {
println("a равно 10")
}
Если необходимо задать альтернативный вариант, то можно добавить блок else:
Java:
val a = 10
if(a == 10) {
println("a равно 10")
}
else{
println("a НЕ равно 10")
}
Если блок кода состоит из одного выражения, то в принципе фигурные скобки можно опустить:
Java:
val a = 10
if(a == 10)
println("a равно 10")
else
println("a НЕ равно 10")
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 в других языках. Формальное определение:
Если значение объекта равно одному из значений в блоке кода when, то выполняются соответствующие действия, которые идут после оператора -> после соответствующего значения.
Например:
Здесь в качестве объекта в конструкцию when передается переменная isEnabled. Далее ее значение по порядку сравнивается со значениями в false и true. В данном случае переменная isEnabled равна true, поэтому будет выполняться код
Java:
when(объект){
значение1 -> действия1
значение2 -> действия2
значениеN -> действияN
}
Например:
Java:
fun main() {
val isEnabled = true
when(isEnabled){
false -> println("isEnabled off")
true -> println("isEnabled on")
}
}
println("isEnabled on")
В примере выше переменная isEnabled имела только два возможных варианта: true и false. Однако чаще бывают случаи, когда значения в блоке when не покрывают все возможные значения объекта. Дополнительное выражение else позволяет задать действия, которые выполняются, если объект не соответствует ни одному из значений. Например:
То есть в данном случае если переменная a равна 30, поэтому она не соответствует ни одному из значений в блоке when. И соответственно будут выполняться инструкции из выражения else.
Если надо, чтобы при совпадении значений выполнялось несколько инструкций, то для каждого значения можно определить блок кода:
Код:
val a = 30
when(a){
10 -> println("a = 10")
20 -> println("a = 20")
else -> println("неопределенное значение")
}
Если надо, чтобы при совпадении значений выполнялось несколько инструкций, то для каждого значения можно определить блок кода:
Java:
var a = 10
when(a){
10 -> {
println("a = 10")
a *= 2
}
20 -> {
println("a = 20")
a *= 5
}
else -> { println("неопределенное значение")}
}
println(a)
Можно определить одни и те же действия сразу для нескольких значений. В этом случае значения перечисляются через запятую:
Также можно сравнивать с целым диапазоном значений с помощью оператора in:
Если оператор in позволяет узнать, есть ли значение в определенном диапазоне, то связка операторов !in позволяет проверить отсутствие значения в определенной последовательности.
Java:
val a = 10
when(a){
10, 20 -> println("a = 10 или a = 20")
else -> println("неопределенное значение")
}
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("неопределенное значение")
}
Выражение в when также может сравниваться с динамически вычисляемыми значениями:
Так, в данном случае значение переменной a сравнивается с результатом операций b - c и b + 5.
Кроме того, when также может может принимать динамически вычисляемый объект:
Можно даже определять переменные, которые будут доступны внутри блока 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("неопределенное значение")
}
}
Кроме того, 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")
}
}
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 может возвращать значение. Возвращаемое значение указывается после оператора ->:
Таким образом, если значение переменной sum располагается в определенном диапазоне, то возвращается то значение, которое идет после стрелки.
Java:
val sum = 1000
val rate = when(sum){
in 100..999 -> 10
in 1000..9999 -> 15
else -> 20
}
println(rate) // 15
Циклы представляют вид управляющих конструкций, которые позволяют в зависимости от определенных условий выполнять некоторое действие множество раз.
Цикл for пробегается по всем элементам коллекции. В этом плане цикл for в Kotlin эквивалентен циклу for-each в ряде других языков программирования. Его формальная форма выглядит следующим образом:
Например, выведем все квадраты чисел от 1 до 9, используя цикл for:
В данном случае перебирается последовательность чисел от 1 до 9. При каждом проходе цикла (итерации цикла) из этой последовательности будет извлекаться элемент и помещаться в переменную n. И через переменную n можно манипулировать значением элемента. То есть в данном случае мы получим следующий консольный вывод:
Циклы могут быть вложенными. Например, выведем таблицу умножения:
В итоге на консоль будет выведена следующая таблица умножения:
Java:
for(переменная in последовательность){
выполняемые инструкции
}
Java:
for(n in 1..9){
print("${n * n} \t")
}
Код:
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 повторяет определенные действия пока истинно некоторое условие:
Здесь пока переменная i больше 0, будет выполняться цикл, в котором на консоль будет выводиться квадрат значения i.
В данном случае вначале проверяется условие (i > 0) и если оно истинно (то есть возвращает true), то выполняется цикл. И вполне может быть ситуация, когда к началу выполнения цикла условие не будет выполняться. Например, переменная i изначально меньше 0, тогда цикл вообще не будет выполняться.
Но есть и другая форма цикла while - do..while:
В данном случае вначале выполняется блок кода после ключевого слова do, а потом оценивается условие после while. Если условие истинно, то повторяется выполнение блока после do. То есть несмотря на то, что в данном случае переменная i меньше 0 и она не соответствует условию, тем не менее блок do выполнится хотя бы один раз.
Java:
var i = 10
while(i > 0){
println(i*i)
i--;
}
В данном случае вначале проверяется условие (i > 0) и если оно истинно (то есть возвращает true), то выполняется цикл. И вполне может быть ситуация, когда к началу выполнения цикла условие не будет выполняться. Например, переменная i изначально меньше 0, тогда цикл вообще не будет выполняться.
Но есть и другая форма цикла while - do..while:
Java:
var i = -1
do{
println(i*i)
i--;
}
while(i > 0)
Иногда при использовании цикла возникает необходимость при некоторых условиях не дожидаться выполнения всех инструкций в цикле, перейти к новой итерации. Для этого можно использовать оператор continue:
В данном случае когда n будет равно 5, сработает оператор continue. И последующая инструкция, которая выводит на консоль квадрат числа, не будет выполняться. Цикл перейдет к обработке следующего элемента в массиве
Бывает, что при некоторых условиях нам вовсе надо выйти из цикла, прекратить его выполнение. В этом случае применяется оператор break:
В данном случае когда n окажется равен 5, то с помощью оператора break будет выполнен выход из цикла. Цикл полностью завершится.
Java:
for(n in 1..8){
if(n == 5) continue;
println(n * n)
}
Бывает, что при некоторых условиях нам вовсе надо выйти из цикла, прекратить его выполнение. В этом случае применяется оператор break:
Java:
for(n in 1..5){
if(n == 5) break;
println(n * n)
}
Диапазон представляет набор значений или неокторый интервал. Для создания диапазона применяется оператор ..:
Этот оператор принимает два значения - границы диапазона, и все элементы между этими значениями (включая их самих) составляют диапазон.
Диапазон необязательно должна представлять числовые данные. Например, это могут быть строки:
Оператор .. позволяет создать диапазон по нарастающей, где каждый следующий элемент будет больше предыдущего. С помощью специальной функции downTo можно построить диапазон в обратном порядке:
Еще одна специальная функция step позволяет задать шаг, на который будут изменяться последующие элементы:
Еще одна функция until позволяет не включать верхнюю границу в диапазон:
С помощью специальных операторов можно проверить наличие или отсутствие элементов в диапазоне:
Java:
val range = 1..5 // диапазон [1, 2, 3, 4, 5]
Диапазон необязательно должна представлять числовые данные. Например, это могут быть строки:
Java:
val range = "a".."d"
Java:
val range1 = 1..5 // 1 2 3 4 5
val range2 = 5 downTo 1 // 5 4 3 2 1
Java:
val range1 = 1..10 step 2 // 1 3 5 7 9
val range2 = 10 downTo 1 step 3 // 10 7 4 1
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 в угловых скобках необходимо указать, объекты какого типа могут храниться в массиве. Например, определим массив целых чисел:
С помощью встроенной функции arrayOf() можно передать набор значений, которые будут составлять массив:
То есть в данном случае в массиве 5 чисел от 1 до 5.
С помощью индексов мы можем обратиться к определенному элементу в массиве. Индексация начинается с нуля, то есть первый элемент будет иметь индекс 0. Индекс указывается в квадратных скобках:
Также инициализировать массив значениями можно следующим способом:
Здесь применяется конструктор класса Array. В этот конструктор передаются два параметра. Первый параметр указывает, сколько элементов будет в массиве. В данном случае 3 элемента. Второй параметр представляет выражение, которое генерирует элементы массива. Оно заключается в фигурные скобки. В данном случае в фигурных скобках стоит число 5, то есть все элементы массива будут представлять число 5. Таким образом, массив будет состоять из трех пятерок.
Но выражение, которое создает элементы массива, может быть и более сложным. Например:
В данном случае элемент массива является результатом умножения переменной i на 2. При этом при каждом обращении к переменой i ее значение увеличивается на единицу.
При определении массива после типа Array в угловых скобках необходимо указать, объекты какого типа могут храниться в массиве. Например, определим массив целых чисел:
Java:
val numbers: Array<Int>
Java:
val numbers: Array<Int> = arrayOf(1, 2, 3, 4, 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]
Но выражение, которое создает элементы массива, может быть и более сложным. Например:
Java:
var i = 1;
val numbers = Array(3, { i++ * 2}) // [2, 4, 6]
Для перебора массивов можно применять цикл for:
В данном случае переменная numbers представляет массив чисел. При переборе этого массива в цикле каждый его элемент оказывается в переменной number, значение которой, к примеру, можно вывести на консоль. Консольный вывод программы:
Подобным образом можно перебирать массивы и других типов:
Консольный вывод программы:
Можно применять и другие типы циклов для перебора массива. Например, используем цикл while:
Здесь определена дополнительная переменная i, которая представляет индекс элемента массива. У массива есть специальное свойство indices, которое содержит набор всех индексов. А выражение i in people.indices возвращает true, если значение переменной i входит в набор индексов массива.
В самом цикле по индексу обащаемся к элементу массива: println(people). И затем переходим к следующему индексу, увеличивая счетчик: i++.
То же самое мы могли написать с помощью цикла for:
Java:
fun main() {
val numbers = arrayOf(1, 2, 3, 4, 5)
for(number in numbers){
print("$number \t")
}
}
Код:
1 2 3 4 5
Java:
fun main() {
val people = arrayOf("Tom", "Sam", "Bob")
for(person in people){
print("$person \t")
}
}
Код:
Tom Sam Bob
Java:
fun main() {
val people = arrayOf("Tom", "Sam", "Bob")
var i = 0
while( i in people.indices){
println(people[i])
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:
Для определения данных для этих массивов можно применять функции, которые начинаются на название типа в нижнем регистре, например, int, и затем идет ArrayOf.
Аналогично для инициализации подобных массивов также можно применять конструктор соответствуюшего класса:
Java:
val numbers: IntArray = intArrayOf(1, 2, 3, 4, 5)
val doubles: DoubleArray = doubleArrayOf(2.4, 4.5, 1.2)
Аналогично для инициализации подобных массивов также можно применять конструктор соответствуюшего класса:
Java:
val numbers = IntArray(3, {5})
val doubles = DoubleArray(3, {1.5})
Выше рассматривались одномерные массивы, которые можно представить в виде ряда или строки значений. Но кроме того, мы можем использовать многомерные массивы. К примеру, возьмем двухмерный массив - то есть такой массив, каждый элемент которого в свою очередь сам является массивом. Двухмерный массив еще можно представить в виде таблицы, где каждая строка - это отдельный массив, а ячейки строки - это элементы вложенного массива.
Определение двухмерных массивов менее интуитивно понятно и может вызывать сложности. Например, двухмерный массив чисел:
В данном случае двухмерный массив будет иметь три элемента - три строки. Каждая строка будет иметь по пять элементов, каждый из которых равен 0.
Используя индексы, можно обращаться к подмассивам в подобном массиве, в том числе переустанавливать их значения:
Для обращения к элементам подмассивов двухмерного массива необходимы два индекса. По первому индексу идет получение строки, а по второму индексу - столбца в рамках этой строки:
Используя два цикла, можно перебирать двухмерные массивы:
С помощью внешнего цикла for(row in table) пробегаемся по всем элементам двухмерного массива, то есть по строкам таблицы. Каждый из элементов двухмерного массива сам представляет массив, поэтому мы можем пробежаться по этому массиву и получить из него непосредственно те значения, которые в нем хранятся. В итоге на консоль будет выведено следующее:
Определение двухмерных массивов менее интуитивно понятно и может вызывать сложности. Например, двухмерный массив чисел:
Java:
val table: Array<Array<Int>> = Array(3, { Array(5, {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()
}
}
Код:
1 2 3
4 5 6
7 8 9
Одним из строительных блоков программы являются функции. Функция определяет некоторое действие. В Kotlin функция объявляется с помощью ключевого слова fun, после которого идет название функции. Затем после названия в скобках указывается список параметров. Если функция возвращает какое-либо значение, то после списка параметров через запятую можно указать тип возвращаемого значения. И далее в фигурных скобках идет тело функции.
Параметры необязательны.
Например, определим и вызовем функцию, которая просто выводит некоторую строку на консоль:
Функции можно определять в файле вне других функций или классов, сами по себе, как например, определяется функция main. Такие функции еще называют функциями верхнего уровня (top-level functions).
Здесь кроме главной функции main также определена функция hello, которая не принимает никаких параметров и ничего не возвращает. Она просто выводит строку на консоль.
Функция hello (и любая другая определенная функция, кроме main) сама по себе не выполняется. Чтобы ее выполнить, ее надо вызвать. Для вызова функции указывается ее имя (в данном случае "hello"), после которого идут пустые скобки.
Таким образом, если необходимо в разных частях программы выполнить одни и те же действия, то можно эти действия вынести в функцию, и затем вызывать эту функцию.
Java:
fun имя_функции (параметры) : возвращаемый_тип{
выполняемые инструкции
}
Например, определим и вызовем функцию, которая просто выводит некоторую строку на консоль:
Java:
fun main() {
hello() // вызов функции hello
hello() // вызов функции hello
hello() // вызов функции hello
}
// определение функции hello
fun hello(){
println("Hello")
}
Здесь кроме главной функции main также определена функция hello, которая не принимает никаких параметров и ничего не возвращает. Она просто выводит строку на консоль.
Функция hello (и любая другая определенная функция, кроме main) сама по себе не выполняется. Чтобы ее выполнить, ее надо вызвать. Для вызова функции указывается ее имя (в данном случае "hello"), после которого идут пустые скобки.
Таким образом, если необходимо в разных частях программы выполнить одни и те же действия, то можно эти действия вынести в функцию, и затем вызывать эту функцию.
Через параметры функция может получать некоторые значения извне. Параметры указываются после имени функции в скобках через запятую в формате имя_параметра : тип_параметра. Например, определим функцию, которая просто выводит сообшение на консоль:
Функция showMessage() принимает один параметр типа String. Поэтому при вызове функции в скобках необходимо передать значение для этого параметра: showMessage("Hello Kotlin"). Причем это значение должно представлять тип String, то есть строку. Значения, которые передаются параметрам функции, еще назвают аргументами.
Консольный вывод программы:
Другой пример - функция, которая выводит данные о пользователе на консоль:
Функция displayUser() принимает два параметра - name и age. При вызове функции в скобках ей передаются значения для этих параметров. При этом значения передаются параметрам по позиции и должны соответствовать параметрам по типу. Так как вначале идет параметр типа String, а потом параметр типа Int, то при вызове функции в скобках вначале передается строка, а потом число.
Java:
fun main() {
showMessage("Hello Kotlin")
showMessage("Привет Kotlin")
showMessage("Salut Kotlin")
}
fun showMessage(message: String){
println(message)
}
Консольный вывод программы:
Код:
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")
}
В примере выше при вызове функций showMessage и displayUser мы обязательно должны предоставить для каждого их параметра какое-то определенное значение, которое соответствует типу параметра. Мы не можем, к примеру, вызвать функцию displayUser, не передав ей аргументы для параметров, это будет ошибка:
Однако мы можем определить какие-то параметры функции как необязательные и установить для них значения по умолчанию:
В данном случае функция displayUser имеет три параметра для передачи имени, возраста и должности. Для первого параметр name значение по умолчанию не установлено, поэтому для него значение по-прежнему обязательно передавать значение. Два последующих - age и position являются необязательными, и для них установлено значение по умолчанию. Если для этих параметров не передаются значения, тогда параметры используют значения по умолчанию. Поэтому для этих параметров в принципе нам необязательно передавать аргументы. Но если для какого-то параметра определено значение по умолчанию, то для всех последующих параметров тоже должно быть установлено значение по умолчанию.
Консольный вывод программы
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")
}
Консольный вывод программы
Код:
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-переменным, поэтому их значение нельзя изменить. Например, в случае следующей функции при компиляции мы получим ошибку:
Однако если параметр предствляет какой-то сложный объект, то можно изменять отдельные значения в этом объекте. Например, возьмем функцию, которая в качестве параметра принимает массив:
Здесь функция double принимает числовой массив и увеличивает значение его первого элемента в два раза. Причем изменение элемента массива внутри функции приведет к тому, что также будет изменено значение элемента в том массиве, который передается в качестве аргумента в функцию, так как этот один и тот же массив. Консольный вывод:
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: 8
Значение в функции main: 8
Функция может принимать переменное количество параметров одного типа. Для определения таких параметров применяется ключевое слово vararg. Например, нам необходимо передать в функцию несколько строк, но сколько именно строк, мы точно не знаем. Их может быть пять, шесть, семь и т.д.:
Функция printStrings принимает неопределенное количество строк. В самой функции мы можем работать с параметром как с последовательностью строк, например, перебирать элементы последовательности в цикле и производить с ними некоторые действия.
При вызове функции мы можем ей передать любое количество строк.
Другой пример - подсчет суммы неопределенного количества чисел:
Если функция принимает несколько параметров, то обычно vararg-параметр является последним.
Однако это необязательно, но если после vararg-параметра идут еще какие-нибудь параметры, то при вызове функции значения этим параметрам передаются через именованные аргументы:
Здесь функция printUserGroup принимает три параметра. Значения параметрам до vararg-параметра передаются по позициям. То есть в данном случае "KT-091" будет представлять значение для параметра group. Последующие значения интерпретируются как значения для 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++")
}
При вызове функции мы можем ей передать любое количество строк.
Другой пример - подсчет суммы неопределенного количества чисел:
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)
}
Java:
fun printUserGroup(count:Int, vararg users: String){
println("Count: $count")
for(user in users)
println(user)
}
fun main() {
printUserGroup(3, "Tom", "Bob", "Alice")
}
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)
}
Оператор * (spread operator) (не стоит путать со знаком умножения) позволяет передать параметру в качестве значения элементы из массива:
Обратите внимание на звездочку перед nums при вызове функции: changeNumbers(*nums, koef=2). Без применения данного оператора мы столкнулись бы с ошибкой, поскольку параметры функции представляют не массив, а неопределенное количество значений типа Int.
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)
}
Функция может возвращать некоторый результат. В этом случае после списка параметров через двоеточие указывается возвращаемый тип. А в теле функции применяется оператор return, после которого указывается возвращаемое значение.
Например, определим функцию, которая возвращает сумму двух чисел:
В объявлении функции sum после списка параметров через двоеточие указывается тип 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")
}
Java:
fun sum(x:Int, y:Int): Int
Java:
return x + y
Java:
val a = sum(4, 3)
Если функция не возвращает какого-либо результата, то фактически неявно она возвращает значение типа Unit. Этот тип аналогичен типу void в ряде языков программирования, которое указывает, что функция ничего не возвращает. Например, следующая функция
будет аналогична следующей:
Формально мы даже можем присвоить результат такой функции переменной:
Однако практического смысла это не имеет, так как возвращаемое значение представляет объект Unit, который больше никак не применяется.
Если функция возвращает значение Unit, мы также можем использовать оператор return для возврата из функции:
В данном случае если значение параметра age выходит за пределы диапазона от 0 до 110, то с помощью оператора return осуществляется выход из функции, и последующие инструкции не выполняются. При этом если функция возвращает значение Unit, то после оператора return можно не указывать никакого значения.
Java:
fun hello(){
println("Hello")
}
Java:
fun hello() : Unit{
println("Hello")
}
Java:
val d = hello()
val e = hello()
Если функция возвращает значение 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)
}
Однострочные функции (single expression function) используют сокращенный синтаксис определения функции в виде одного выражения. Эта форма позволяет опустить возвращаемый тип и оператор return.
Функция также определяется с помощью ключевого слова fun, после которого идет имя функции и список параметров. Но после списка параметров не указывается возвращаемый тип. Возвращаемый тип будет выводится компилятором. Далее через оператор присвоения = определяется тело функции в виде одного выражения.
Например, функция возведения числа в квадрат:
В данном случае функция square возводит число в квадрат. Она состоит из одного выражения x * x. Значение этого выражения и будет возвращаться функцией. При этом оператор return не используется.
Такие функции более лаконичны, более читабельны, но также опционально можно и указывать возвращаемый тип явно:
Java:
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")
}
Такие функции более лаконичны, более читабельны, но также опционально можно и указывать возвращаемый тип явно:
Java:
fun square(x: Int) : Int = x * x
Одни функции могут быть определены внутри других функций. Внутренние или вложенные функции еще называют локальными.
Локальные функции могут определять действия, которые используются только в рамках какой-то конкретной функции и нигде больше не применяются.
Например, у нас есть функция, которая сравнивает два возраста:
Однако извне могут быть переданы некорректные данные. Имеет ли смысл сравнивать возраст меньше нуля с другим? Очевидно нет. Для этой цели в функции определена локальная функция ageIsValid(), которая возвращает true, если возраст является допустимым. Больше в программе эта функция нигде не используется, поэтому ее можно сделать локальной.
При этом локальная может использоваться только в той функции, где она определена.
Причем в данном случае удобнее сделать локальную функцию однострочной:
Локальные функции могут определять действия, которые используются только в рамках какой-то конкретной функции и нигде больше не применяются.
Например, у нас есть функция, которая сравнивает два возраста:
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)
}
При этом локальная может использоваться только в той функции, где она определена.
Причем в данном случае удобнее сделать локальную функцию однострочной:
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) представляет определение нескольких функций с одним и тем же именем, но с различными параметрами. Параметры перегруженных функций могут отличаться по количеству, типу или по порядку в списке параметров.
В данном случае для одной функции sum() определено пять перегруженных версий. Каждая из версий отличается либо по типу, либо количеству, либо по порядку параметров. При вызове функции sum компилятор в зависимости от типа и количества параметров сможет выбрать для выполнения нужную версию:
При этом при перегрузке не учитывает возвращаемый результат функции. Например, пусть у нас будут две следующие версии функции sum:
Они совпадают во всем за исключением возвращаемого типа. Однако в данном случае мы сталкивамся с ошибкой, так как перегруженные версии должны отличаться именно по типу, порядку или количеству параметров. Отличие в возвращаемом типе не имеют значения.
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
}
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)
}
Java:
fun sum(a: Double, b: Int) : Double{
return a + b
}
fun sum(a: Double, b: Int) : String{
return "$a + $b"
}
В Kotlin все является объектом, в том числе и функции. И функции, как и другие объекты, имеют определенный тип. Тип функции определяется следующим образом:
Возьмем функцию которая не принимает никаких параметров и ничего не возвращает:
Она имеет тип
Если функция не принимает параметров, в определении типа указываются пустые скобки. Если не указан возвращаемый тип, то фактически а в качестве типа возвращаемого значения применяется тип Unit.
Возьмем другую функцию:
Эта функция принимает два параметра типа Int и возвращает значение типа Int, поэтому она имеет тип
Что дает нам знание типа функции? Используя тип функции, мы можем определять переменные и параметры других функций, которые будут представлять функции.
Java:
(типы_параметров) -> возвращаемый_тип
Java:
fun hello(){
println("Hello Kotlin")
}
Java:
() -> Unit
Возьмем другую функцию:
Java:
fun sum(a: Int, b: Int): Int{
return a + b
}
Java:
(Int, Int) -> Int
Переменная может представлять функцию. С помощью типа функции можно определить, какие именно функции переменная может представлять:
Здесь переменная message представляет функцию с типом () -> Unit, то есть функцию без параметров, которая ничего не возвращает. Далее определена как раз такая функция - hello(), соответственно мы можем передать функцию hello переменной message.
Чтобы передать функцию, перед названием функции ставится оператор ::
Затем мы можем обращаться к переменной message() как к обычной функции:
Так как переменная message ссылается на функцию hello, то при вызове message() фактически будет вызываться функция hello().
При этом тип функции также может выводится исходя из присваемого переменной значения:
Рассмотрим другой пример, когда переменная ссылается на функцию с параметрами:
Переменная operation представляет функцию с типом (Int, Int) -> Int, то есть функцию с двумя параметрами типа Int и возвращаемым значением типа Int. Соответственно такой переменной мы можем присвоить функцию sum, которая соответствует этому типу.
Затем через имя переменной фактически можно обращаться к функции sum(), передавая ей значения для параметров и получая ее результат:
При этом динамически можно менять значение, главное чтобы оно соответствовало типу переменной:
Java:
fun main() {
val message: () -> Unit
message = ::hello
message()
}
fun hello(){
println("Hello Kotlin")
}
Чтобы передать функцию, перед названием функции ставится оператор ::
Java:
message = ::hello
Java:
message()
При этом тип функции также может выводится исходя из присваемого переменной значения:
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
}
Затем через имя переменной фактически можно обращаться к функции 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) - это функции, которые либо принимают функцию в качестве параметра, либо возвращают функцию, либо и то, и другое.
Чтобы функция могла принимать другую функцию через параметр, этот параметр должен представлять тип функции:
В данном случае функция displayMessage() через параметр mes принимает функцию типа () -> Unit, то есть такую функцию, которая не имеет параметров и ничего не возвращает.
При вызове этой функции мы можем передать этому параметру функцию, которая соответствует этому типу:
Рассмотрим пример параметра-функции, которая принимает параметры:
Здесь функция action принимает три параметра. Первые два параметра - значения типа Int. А третий параметр представляет функцию, которая имеет тип (Int, Int)-> Int, то есть принимает два числа и возвращает некоторое число.
В самой функции action вызываем эту параметр-функцию, передавая ей два числа, и полученный результат выводим на консоль.
При вызове функции action мы можем передать для ее третьего параметра конкретную функцию, которая соответствует этому параметру по типу:
Java:
fun main() {
displayMessage(::morning)
displayMessage(::evening)
}
fun displayMessage(mes: () -> Unit){
mes()
}
fun morning(){
println("Good Morning")
}
fun evening(){
println("Good Evening")
}
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 вызываем эту параметр-функцию, передавая ей два числа, и полученный результат выводим на консоль.
При вызове функции action мы можем передать для ее третьего параметра конкретную функцию, которая соответствует этому параметру по типу:
Java:
action(5, 3, ::sum) // 8
action(5, 3, ::multiply) // 15
action(5, 3, ::subtract) // 2
В более редких случаях может потребоваться возвратить функцию из другой функции. В этом случае для функции в качестве возвращаемого типа устанавливается тип другой функции. А в теле функции возвращается лямбда выражение. Например:
Здесь функция 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 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.
Далее в функции main определяется переменная action1 хранит результат функции selectAction. Так как selectAction() возвращает функцию, то и переменная action1 будет хранить эту функцию. Затем через переменную action1 можно вызвать эту функцию.
Поскольку возвращаемая функция соответствует типу (Int, Int) -> Int, то при вызове в action1 необходимо передать два числа, и соответственно мы можем получить результат и вывести его на консоль.
Анонимные функции выглядят как обычные за тем исключением, что они не имеют имени. Анонимная функция может иметь одно выражение:
Либо может представлять блок кода:
Анонимную функцию можно передавать в качестве значения переменной:
Здесь переменной message передается анонимная функция fun()=println("Hello"). Эта анонимная функция не принимает параметров и просто выводит на консоль строку "Hello". Таким образом, переменная message будет представлять тип () -> Unit.
Далее мы можем вызывать эту функцию через имя переменной как обычную функцию: message().
Другой пример - анонимная функция с параметрами:
В данном случае переменной sum присваивается анонимная функция, которая принимает два параметра - два целых числа типа Int и возвращает их сумму.
Также через имя переменной мы можем вызвать эту анонимную функцию, передав ей некоторые значения для параметров и получить ее результат: val result = sum(5, 4)
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().
Другой пример - анонимная функция с параметрами:
Java:
fun main() {
val sum = fun(x: Int, y: Int): Int = x + y
val result = sum(5, 4)
println(result) // 9
}
Также через имя переменной мы можем вызвать эту анонимную функцию, передав ей некоторые значения для параметров и получить ее результат: 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)
}
И также фунция может возвращать анонимную функцию в качестве результата:
Здесь функция selectAction() в зависимости от переданного значения возвращает одну из четырех анонимных функций. Последняя анонимная функция fun(x: Int, y: Int): Int = 0 просто возвращает число 0.
При обращении к selectAction() переменная получит определенную анонимную функцию:
То есть в данном случае переменная action1 хранит ссылку на функцию fun(x: Int, y: Int): Int = x + y
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() переменная получит определенную анонимную функцию:
Java:
val action1 = selectAction(1)
Лямбда-выражения представляют небольшие кусочки кода, которые выполняют некоторые действия. Фактически лямбды преставляют сокращенную запись функций. При этом лямбды, как и обычные и анонимные функции, могут передаваться в качестве значений переменным и параметрам функции.
Лямбда-выражения оборачиваются в фигурные скобки:
В данном случае лямбда-выражение выводит на консоль строку "hello".
Лямбда-выражение можно сохранить в обычную переменную и затем вызывать через имя этой переменной как обычную функцию.
В данном случае лямбда сохранена в переменную hello и через эту переменную вызывается два раза. Поскольку лямбда-выражение представляет сокращенную форму функции, то переменная hello имеет тип функции () -> Unit.
Также лямбда-выражение можно запускать как обычную функцию, используя круглые скобки:
Следует учитывать, что если до подобной записи идут какие-либо инструкции, то Kotlin автоматически может не определять, что определения лямбда-выражения составляет новую инструкцию. В этом случае предыдущую инструкции можно завершить точкой с запятой:
Лямбда-выражения оборачиваются в фигурные скобки:
Java:
{println("hello")}
Лямбда-выражение можно сохранить в обычную переменную и затем вызывать через имя этой переменной как обычную функцию.
Java:
fun main() {
val hello = {println("Hello Kotlin")}
hello()
hello()
}
Java:
val hello: ()->Unit = {println("Hello Kotlin")}
Java:
fun main() {
{println("Hello Kotlin")}()
}
Java:
fun main() {
{println("Hello Kotlin")}();
{println("Kotlin on Metanit.com")}()
}
Лямбды как и функции могут принимать параметры. Для передачи параметров используется стрелка ->. Параметры указываются слева от стрелки, а тело лямбда-выражения, то есть сами выполняемые действия, справа от стрелки.
Здесь лямбда-выражение принимает один параметр типа String, значение которого выводится на консоль. Переменная printer в данном случае имеет тип (String) -> Unit.
При вызове лямбда-выражения сразу при его определении в скобках передаются значения для его параметров:
Если параметров несколько, то они передаются слева от стрелки через запятую:
Если в лямбда-выражении надо выполнить не одно, а несколько действий, то эти действия можно размещать на отдельных строках после стрелки:
Java:
fun main() {
val printer = {message: String -> println(message)}
printer("Hello")
printer("Good Bye")
}
При вызове лямбда-выражения сразу при его определении в скобках передаются значения для его параметров:
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:
В обоих случаях используется функция println, которая формально не возвращает никакого значения (точнее возвращает объект типа Unit).
Но также может возвращаться конкретное значение:
Здесь выражение справа от стрелки x + y продуцирует новое значение - сумму чисел, и при вызове лямбда-выражения это значение можно передать переменной. В данном случае лямбда-выражение имеет тип (Int, Int) -> Int.
Если лямбда-выражение многострочное, состоит из нескольких инструкций, то возвращается то значение, которое генерируется последней инструкцией:
Последнее выражение по сути представляет число - сумму чисел x и y и оно будет возвращаться в качестве результата лямбда-выражения.
Если лямбда-выражение формально не возвращает никакого результата, то фактически, как и в функциях, возвращается значение типа Unit:
Java:
val hello = { println("Hello")}
val h = hello() // h представляет тип Unit
val printer = {message: String -> println(message)}
val p = printer("Welcome") // p представляет тип 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")
}
Если лямбда-выражение многострочное, состоит из нескольких инструкций, то возвращается то значение, которое генерируется последней инструкцией:
Java:
val sum = {x:Int, y:Int ->
val result = x + y
println("$x + $y = $result")
result
}
Лямбда-выражения можно передавать параметрам функции, если они представляют один и тот же тип функции:
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)
}
При передаче лямбды параметру или переменной, для которой явным образом указан тип, мы можем опустить в лямбда-выражении типы параметров:
Здесь в случае с переменной sum Kotlin видит, что ее тип (Int, Int) -> Int, то есть и первый, и второй параметр представляют тип Int. Поэтому при присвоении переменной лямбды {x, y -> x + y } Kotlin автоматически поймет, что параметры x и y представляют именно тип Int.
То же самое касается и вызова функции doOperation() - при передаче в него лямбды Kotlin автоматически поймет какой параметр какой тип представляет.
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)
}
То же самое касается и вызова функции doOperation() - при передаче в него лямбды Kotlin автоматически поймет какой параметр какой тип представляет.
Если параметр, который принимает функцию, является последним в списке, то при передачи ему лямбда-выражения, саму лямбду можно прописать после списка параметров. Например, возьмем выше использованную функцию doOperation():
Здесь параметр, который представляет функцию - параметр op, является последним в списке параметров. Поэтому вместо того, чтобы написать так:
Мы также можем написать так:
То есть вынести лямбду за список параметров. Это так называемая конечная лямбда или trailing lambda
Java:
fun doOperation(x: Int, y: Int, op: (Int, Int) ->Int){
val result = op(x, y)
println(result)
}
Java:
doOperation(3, 4, {a, b -> a * b}) // 12
Java:
doOperation(3, 4) {a, b -> a * b} // 12
Также фукция может возвращать лямбда-выражение, которое соответствует типу ее возвращаемого результата:
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 }
}
}
Обратим внимание на предыдущий пример на последнюю лямбду:
Если в функцию selectAction() передается число, отличное от 1, 2, 3, то возвращается лямбда-выражение, которое просто возвращает число 0. С одной стороны, это лямбда-выражение должно соответствовать типу возвращаемого результата функции selectAction() - (Int, Int) -> Int
С другой стороны, оно не использует параметры, эти параметры не нужны. В этом случае вместо неиспользуемых параметров можно указать прочерки:
Java:
else -> return {x, y -> 0 }
С другой стороны, оно не использует параметры, эти параметры не нужны. В этом случае вместо неиспользуемых параметров можно указать прочерки:
Java:
else -> return {_, _ -> 0 }
Kotlin поддерживает объектно-ориентированную парадигму программирования, а это значит, что программу на данном языке можно представить в виде взаимодействующих между собой объектов.
Представлением объекта является класс. Класс фактически представляет определение объекта. А объект является конкретным воплощением класса. Например, у всех есть некоторое представление о машине, например, кузов, четыре колеса, руль и т.д. - некоторый общий набор характеристик, присущих каждой машине. Это представление фактически и является классом. При этом есть разные машины, у которых отличается форма кузова, какие-то другие детали, то есть есть конкретные воплощения этого класса, конкретные объекты или экземпляры класса.
Для определения класса применяется ключевое слово class, после которого идет имя класса. А после имени класса в фигурных скобках определяется тело класса. Если класс не имеет тела, то фигурные скобки можно опустить. Например, определим класс, который представляет человека:
Класс фактически представляет новый тип данных, поэтому мы можем определять переменные этого типа:
В функции main определены три переменных типа Person. Стоит также отметить, что в отличие от других объектно-ориентированных языков (как C# или Java), функция main в Kotlin не помещается в отдельных класс, а всегда определяется вне какого-либо класса.
Для создания объекта класса необходимо вызвать конструктор данного класса. Конструктор фактически представляет функцию, которая называется по имени класса и которая выполняет инициализацию объекта. По умолчанию для класса компилятор генерирует пустой конструктор, который мы можем использовать:
Часть кода после знака равно Person() как раз и представляет вызов конструктора, который создает объект класса Person. До вызова конструктора переменная класса не указывает ни на какой объект.
Например, создадим три объекта класса Person:
Представлением объекта является класс. Класс фактически представляет определение объекта. А объект является конкретным воплощением класса. Например, у всех есть некоторое представление о машине, например, кузов, четыре колеса, руль и т.д. - некоторый общий набор характеристик, присущих каждой машине. Это представление фактически и является классом. При этом есть разные машины, у которых отличается форма кузова, какие-то другие детали, то есть есть конкретные воплощения этого класса, конкретные объекты или экземпляры класса.
Для определения класса применяется ключевое слово class, после которого идет имя класса. А после имени класса в фигурных скобках определяется тело класса. Если класс не имеет тела, то фигурные скобки можно опустить. Например, определим класс, который представляет человека:
Java:
class Person
// либо можно так
class Person { }
Java:
fun main() {
val tom: Person
val bob: Person
val alice: Person
}
class Person
Для создания объекта класса необходимо вызвать конструктор данного класса. Конструктор фактически представляет функцию, которая называется по имени класса и которая выполняет инициализацию объекта. По умолчанию для класса компилятор генерирует пустой конструктор, который мы можем использовать:
Java:
val tom: 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, то значение этого свойства можно многократно изменять.
Свойство должно быть инициализировано, то есть обязательно должно иметь начальное значение. Например, определим пару свойств:
В данном случае в классе Person, который представляет человека, определены свойства name (имя человека) и age (возраст человека). И эти свойства инициализированы начальными значениями.
Поскольку эти свойства определены с var, то мы можем изменить их начальные значения:
Для обращения к свойствам используется имя переменной, которая предствляет объект, и после точки указывается имя свойства. Например, получение значения свойства:
Установка значения свойства:
Свойство должно быть инициализировано, то есть обязательно должно иметь начальное значение. Например, определим пару свойств:
Java:
class Person{
var name: String = "Undefined"
var age: Int = 18
}
Поскольку эти свойства определены с 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 или функции-члены класса. Например, определим класс с функциями:
Функции класса определяется также как и обычные функции. В частности, здесь в классе Person определена функция sayHello(), которая выводит на консоль строку "Hello" и эмулирует приветствие объекта Person. Вторая функция - go() эмулирует движение объекта Person к определенному местоположению. Местоположение передается через параметр location. И третья функция personToString() возвращает информацию о текущем объекте в виде строки.
В функциях, которые определены внутри класса, доступны свойства этого класса. Так, в данном случае в функциях можно обратиться к свойствам name и age, которые определены в классе Person.
Для обращения к функциям класса необходимо использовать имя объекта, после которого идет название функции и в скобках значения для параметров этой функции:
Консольный вывод программы:
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"
}
}
В функциях, которые определены внутри класса, доступны свойства этого класса. Так, в данном случае в функциях можно обратиться к свойствам 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).
Классы в Kotlin могут иметь один первичный конструктор (primary constructor) и один или несколько вторичных конструкторов (secondary constructor).
Первичный конструктор является частью заголовка класса и определяется сразу после имени класса:
Конструкторы, как и обычные функции, могут иметь параметры. Так, в данном случае конструктор имеет параметр _name, который представляет тип String. Через параметры конструктора мы можем передать извне данные и использовать их для инициализации объекта. При этом первичный конструктор в отличие от функций не определяет никаких действий, он только может принимать данные извне через параметры.
Если первичный конструктор не имеет никаких аннотаций или модификаторов доступа, как в данном случае, то ключевое слово constructor можно опустить:
Java:
class Person constructor(_name: String){
}
Если первичный конструктор не имеет никаких аннотаций или модификаторов доступа, как в данном случае, то ключевое слово constructor можно опустить:
Java:
class Person(_name: String){
}
Что делать с полученными через конструктор данными? Мы их можем использовать для инициализации свойств класса. Для этого применяются блоки инициализаторов:
В классе Person определено свойство name, которое хранит имя человека. Чтобы передать эту свойству значение параметра _name из первичного конструктора, применяется блок инициализатора. Блок инициализатора определяется после ключевого слова init.
Цель инициализатора состоит в инициализации объекта при его создании. Стоит отметить, что здесь свойству name не задается начальное значение, потому это свойство в любом случае будет инициализировано в блоке инициализатора, и при создании объекта оно в любом случае получит значение.
Теперь мы можем использовать первичный конструктор класса для создания объекта:
Важно учитывать, что если мы определили первичный конструктор, то мы не можем использовать конструктор по умолчанию, который генерируется компилятором. Для создания объекта обязательно надо использовать первичный конструктор, если он определен в классе.
Стоит отметить, что в классе может быть определено одновременно несколько блоков инициализатора.
Также стоит отметить, что в данном случае в инициализаторе нет смысла, так как параметры первичного конструктора можно нарямую передавать свойствам:
Java:
class Person(_name: String){
val name: String
init{
name = _name
}
}
Цель инициализатора состоит в инициализации объекта при его создании. Стоит отметить, что здесь свойству 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
}
Первичный конструктор также может использоваться для определения свойств:
Свойства определяются как и параметры, при этом их определение начинается с ключевого слова val (если их не планируется изменять) и var (если свойства должны быть изменяемыми). И в этом случае нам уже необязательно явным образом определять эти свойства в теле класса, так как их уже определяет конструктор. И при вызове конструктора этим свойствам автоматически передаются значения: Person("Bob", 23)
Java:
fun main() {
val bob: Person = Person("Bob", 23)
println("Name: ${bob.name} Age: ${bob.age}")
}
class Person(val name: String, var age: Int){
}
Класс также может определять вторичные конструкторы. Они применяются в основном, чтобы определить дополнительные параметры, через которые можно передавать данные для инициализации объекта.
Вторичные конструкторы определяются в теле класса. Если для класса определен первичный конструктор, то вторичный конструктор должен вызывать первичный с помощью ключевого слова this:
Здесь в классе Person определен первичный конструктор, который принимает значение для установки свойства name:
И также добавлен вторичный конструктор. Он принимает два параметра: _name и _age. С помощью ключевого слова this вызывается первичный конструктор, поэтому через этот вызов необходимо передать значения для параметров первичного конструктора. В частности, в первичный конструктор передается значение параметра _name. В самом вторичном конструкторе устанавливается значение свойства age.
Таким образом, при вызове вторичного конструктора вначале вызывается первичный конструктор, срабатывает блок инициализатора, который устанавливает свойство name. Затем выполняются собственно действия вторичного конструктора, который устанавливает свойство age.
Используем данную модификацию класса Person:
В функции main создаются два объекта Person. Для создания объекта tom применяется первичный конструктор, который принимает один параметр. Для создания объекта bob применяется вторичный конструктор с двумя параметрами.
Консольный вывод программы:
При необходимости мы можем определять и больше вторичных конструкторов:
Здесь в класс Person добавлено новое свойство - company, которое описывает компании, в которой работает человек. И также добавлен еще один конструктор, который принимает три параметра:
Чтобы не дублировать код установки свойств name и age, этот вторичный конструктор передает установку этих свойств другому вторичному конструктору, который принимает два параметра, через вызов this(_name, _age). То есть данный вызов по сути будет вызывать первый вторичный конструктор с двумя параметрами.
Консольный вывод программы:
Вторичные конструкторы определяются в теле класса. Если для класса определен первичный конструктор, то вторичный конструктор должен вызывать первичный с помощью ключевого слова this:
Java:
class Person(_name: String){
val name: String = _name
var age: Int = 0
constructor(_name: String, _age: Int) : this(_name){
age = _age
}
}
Java:
class Person(_name: String)
Java:
constructor(_name: String, _age: Int) : this(_name){
age = _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
}
}
Консольный вывод программы:
Код:
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
}
}
Java:
constructor(_name: String, _age: Int, _comp: String) : this(_name, _age){
company = _comp
}
Консольный вывод программы:
Код:
Name: Tom Age: 0 Company: Undefined
Name: Bob Age: 41 Company: Undefined
Name: Sam Age: 32 Company: JetBtains
Пакеты в Kotlin представляют логический блок, который объединяет функционал, например, классы и функции, используемые для решения близких по характеру задач. Так, классы и функции, которые предназначены для решения одной задачи, можно поместить в один пакет, классы и функции для других задач можно поместить в другие пакеты.
Для определения пакета применяется ключевое слово package, после которого идет имя пакета:
Определение пакета помещается в самое начало файла. И все содержимое файла рассматривается как содержимое этого пакета.
Например, добавим в проект новый файл email.kt:
И определим в нем следующий код:
Пакет называется "email". Он содержит класс Message, который содежит одно свойство text. Условно говоря, это класс представляет email-сообщение, а свойство text - его текст.
Также в этом пакете определена функция send(), которая условно отправляет сообшение на некоторый адрес.
Допустим, мы хотим использовать функционал этого пакета в другом файле. Для подключения сущностей из пакета необходимо применить директиву import. Здесь возможны различные способы подключения функционала из пакета. Можно подключить в целом весь пакет:
После названия пакета ставится точка и звездочка, тем самым импортируются все типы из этого пакета. Например, возьмем другой файл проекта - app.kt, который определяет функцию main, и используем в нем функционал пакета email:
Поскольку в начале файла импортированы все типы из пакета email, то мы можем использовать класс Message и функцию send в функции main.
Консольный вывод данной программы:
Также можно импортировать типы, определенные в пакете, по отдельности:
Для определения пакета применяется ключевое слово 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")
}
Также в этом пакете определена функция send(), которая условно отправляет сообшение на некоторый адрес.
Допустим, мы хотим использовать функционал этого пакета в другом файле. Для подключения сущностей из пакета необходимо применить директиву import. Здесь возможны различные способы подключения функционала из пакета. Можно подключить в целом весь пакет:
Код:
import email.*
Java:
import email.*
fun main() {
val myMessage = Message("Hello Kotlin")
send(myMessage, "tom@gmail.com")
}
Консольный вывод данной программы:
Код:
Message `Hello Kotlin` has been sent to tom@gmail.com
Java:
import email.send
import email.Message
С помощью оператора as можно определять псевдоним для подключаемого типа и затем обращаться к этому типу через его псевдоним:.
Здесь для функции send() определен псевдоним sendEmail. И далее для обращения к этой функции надо использовать ее псевдоним:
Также для класса Message определен псевдоним EmailMessage. Соответственно при использовании класса необходимо применять его псевдоним, а не оригинальное имя:
Псевдонимы могут нам особенно пригодится, если у нас импортируются из разных пакетов типы с одним и тем же именем. Например, пусть в проекте есть файл sms.kt:
Здесь определен пакет sms также с классом Message и функцией send для отправке сообшения по sms.
Допустим, в файле app.kt мы одновременно хотим использовать класс Message и функцию send и из файла email.kt, и из файла sms.kt:
Java:
import email.send as sendEmail
import email.Message as EmailMessage
fun main() {
val myMessage = EmailMessage("Hello Kotlin")
sendEmail(myMessage, "tom@gmail.com")
}
Java:
sendEmail(myMessage, "tom@gmail.com")
Java:
val myMessage = EmailMessage("Hello Kotlin")
Java:
package sms
class Message(val text: String)
fun send(message: Message, phoneNumber: String){
println("Message `${message.text}` has been sent to $phoneNumber")
}
Допустим, в файле 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. По умолчанию без этой аннотации класс не может быть унаследован.
Для установки наследования после названия производного класса идет двоеточие и затем указывает класс, от которого идет наследование.
Например:
Например, в данном случае класс Person представляет человека, который имеет свойство name (имя человека) и метод printName() для вывода информации о человеке. Класс Employee представляет условного работника. Поскольку работник является человеком, то класс работника будет разделять общий функционал с классом человека. Поэтому вместо того, чтобы заново определять в классе Employee свойство name, лучше уснаследовать весь функционал класса Person. То есть в данном случае класс Person является базовым или суперклассом, а класс Employee - производным классом или классом-наследником.
Но стоит учитывать, что при наследовании производный класс должен вызывать первичный конструктор (а если такого нет, то конструктор по умолчанию) базового класса.
Здесь класс Person явным образом не определяет первичных конструкторов, поэтому в классе Employee надо вызывать конструктор по умолчанию для класса Person
Вызвать конструктор базового класса в производном классе можно двумя способами. Первый способ - после двоеточия сразу указать вызов конструктора базового класса:
Здесь запись Person() как раз представляет вызов конструктора по умолчанию класса Person.
Второй способ вызвать конструктор базового класса - определить в производном классе вторичный конструктор и в нем вызвать конструктор базового класса с помощью ключевого слова super:
Здесь с помощью ключевого слова constructor в классе Employee определяется вторичный конструктор. А после списка его параметров после двоеточия идет обращение к конструктору базового класса: constructor() : super(). То есть здесь вызов super() - это и есть вызов конструктора базового класса.
Вне зависимости какой способ будет выбран, далее мы сможем создавать объекты класса Employee и использовать для него уснаследованный от класса Person функционал:
Чтобы функциональность класса можно было унаследовать, необходимо определить для этого класса аннотацию open. По умолчанию без этой аннотации класс не может быть унаследован.
Java:
open class базовый_класс
class производный_класс: базовый_класс
Например:
Java:
open class Person{
var name: String = "Undefined"
fun printName(){
println(name)
}
}
class Employee: Person()
Но стоит учитывать, что при наследовании производный класс должен вызывать первичный конструктор (а если такого нет, то конструктор по умолчанию) базового класса.
Здесь класс Person явным образом не определяет первичных конструкторов, поэтому в классе Employee надо вызывать конструктор по умолчанию для класса Person
Вызвать конструктор базового класса в производном классе можно двумя способами. Первый способ - после двоеточия сразу указать вызов конструктора базового класса:
Java:
class Employee: Person()
Второй способ вызвать конструктор базового класса - определить в производном классе вторичный конструктор и в нем вызвать конструктор базового класса с помощью ключевого слова super:
Java:
open class Person{
var name: String = "Undefined"
fun printName(){
println(name)
}
}
class Employee: Person{
constructor() : 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()
Если базовый класс явным образом определяет конструктор (первичный или вторичный), то производный класс должен вызывать этот конструктор. Для вызова конструктора базового в производном применяются те ж способы.
Первый способ - вызвать конструктор после названия класса через двоеточие:
В данном случае класс Person через конструктор устанавливает свойство name. Поэтому в классе Employee тоже определен конструктор, который принимает стороковое значение и передает его в конструктор Person.
Если производный класс не имеет явного первичного конструктора, тогда при вызове вторичного конструктора должен вызываться конструктор базового класса через ключевое слово super:
Опять же, поскольку конструктор Person принимает один параметр, то в super() нам надо передать значение для этого параметра.
Применение классов:
Выше рассматривался случай, когда в базовом классе определен первичный конструктор.Но все то же действует и в том случае, если в базовом классе есть только вторичные конструкторы:
Первый способ - вызвать конструктор после названия класса через двоеточие:
Java:
open class Person(val name: String){
fun printName(){
println(name)
}
}
class Employee(empName: String): Person(empName)
Если производный класс не имеет явного первичного конструктора, тогда при вызове вторичного конструктора должен вызываться конструктор базового класса через ключевое слово super:
Java:
open class Person(val name: String){
fun printName(){
println(name)
}
}
class Employee: Person{
constructor(empName: String) : super(empName){}
}
Применение классов:
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)
Производный класс наследует функционал от базового класса, но также может определять и свой собственный функционал:
В данном случае класс Employee добаваляет к унаследованному функционалу свойство company, которое хранит компанию работника, и функцию printCompany().
Стоит отметить, что в Kotlin мы можем унаследовать класс только от одного класса, множественное наследование не поддерживается.
Также, стоит отметить, что все классы по умолчанию наследуются от класса Any, даже если класс Any явным образом не указан в качестве базового. Поэтому любой класс уже по умолчанию будет иметь все свойства и функции, которые определены в классе Any. Поэтому все классы по умолчанию уже будут иметь такие функции как equals, toString, hashcode.
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)
}
}
Стоит отметить, что в Kotlin мы можем унаследовать класс только от одного класса, множественное наследование не поддерживается.
Также, стоит отметить, что все классы по умолчанию наследуются от класса Any, даже если класс Any явным образом не указан в качестве базового. Поэтому любой класс уже по умолчанию будет иметь все свойства и функции, которые определены в классе Any. Поэтому все классы по умолчанию уже будут иметь такие функции как equals, toString, hashcode.
Все используемые типы, а также компоненты типов (классы, объекты, интерфейсы, конструкторы, функции, свойства) имеют определеннй уровень видимости, определяемый модификатором видимости (модификатором доступа). Модификатор видимости определяет, где те или иные типы и их компоненты доступны и где их можно использовать. В Kotlin есть следующие модификаторы видимости:
Для установки уровня видимости модификатор ставится перед ключевыми словами var/val/fun в самом начале определения свойства или функции.
Если модификатор видимости явным образом не указан, то применяется модификатор public. То есть следующий класс
Будет эквивалентен следующему определению класса:
Если свойства объявляются через первичный конструктор и для них явным образом не указан модификатор видимости:
То также к таким свойствам автоматически применяется public:
Соответственно мы можем обращаться к подобным компонентам класса в любом месте программы:
- 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")
}
}
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, то к ним нельзя будет обратиться извне - вне данного класса.
В данном случае класс Person определяет два свойства name (имя человека) и age (возраст человека). Чтобы было более показательно, одно свойство определено через конструктор, а второе как переменная класса. И поскольку эти свойства определены с модификатором private, то мы можем к ним обращаться только внутри этого класса. Вне класса обращаться к ним нельзя:
Также в классе определены три функции printPerson(), printAge() и printName(). Последние две функции выводят значения свойств. А функция printPerson выводит информацию об объекте, вызывая предыдущие две функции.
Однако функции printAge() и printName() определены как приватные, поэтому их можно использовать только внутри класса:
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
}
Java:
println(tom.name) // Ошибка! - свойство name - private
Однако функции printAge() и printName() определены как приватные, поэтому их можно использовать только внутри класса:
Java:
tom.printAge() // Ошибка! - функция printAge - private
Модификатор protected определяет свойства и функции, которые из вне класса видны только в классах-наследниках:
Здесь в классе Person свойство name определенно как protected, поэтому оно доступно в классе-наследнике Employee (однако вне базового и производного класса - например, в функции main оно недоступно). А вот свойство age - приватное, поэтому оно доступно только внутри класса Person.
Также в классе Employee будет доступна функция printPerson(), так как она имеет модификатор protected, а функции printAge() и printName() с модификатором private будут недоступны.
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
}
}
Также в классе Employee будет доступна функция printPerson(), так как она имеет модификатор protected, а функции printAge() и printName() с модификатором private будут недоступны.
Конструкторы как первичные, так и вторичные также могут иметь модификаторы. Модификатор указывается перед ключевым словом constructor. По умолчанию они имеют модификатор public. Если для первичного конструктора необходимо явным образом установить модификатор доступа, то конструктор определяется с помощью ключевого слова constructor:
Стоит отметить, что в данном случае, поскольку конструктор приватный мы не можем его использовать вне класса ни для создания объекта класса в функции main, ни при наследовании. Но мы можем использовать такой конструктор в других конструкторах внутри класса:
Здесь вторичный конструктор класса Person, который имеет модификатор protected (то есть доступен в текущем классе и классах-наследниках) вызывает первичный конструктор класса Person, который имеет модификатор private.
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
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)
Классы, а также переменные и функции, которые определены вне других классов, также могут иметь модификаторы public, private и internal.
Допустим, у нас есть файл base.kt, который определяет одноименный пакет:
Внутри данного файла мы можем использовать его приватные переменные, функции классы. Однако при подключении этого пакета в другие файлы, приватные переменные, функции и классы будут недоступны:
Однако даже внутри одного файла есть ограничения на использование приватных классов:
Здесь мы столкнемся с ошибкой, так как публичная функция не может принимать параметр приватного класса. И в данном случае нам надо либо сделать класс Message публичным, либо функцию send приватной.
Допустим, у нас есть файл 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")
}
Геттеры (getter) и сеттеры (setter) (еще их называют методами доступа) позволяют управлять доступом к переменной. Их формальный синтаксис:
Инициализатор, геттер и сеттер свойства необязательны. Указывать тип свойства также необязательно, если он может быть выведен их значения инициализатора или из возвращаемого значения геттера.
Геттеры и сеттеры необязательно определять именно для свойств внутри класса, они могут также применяться к переменным верхнего уровня.
Java:
var имя_свойства[: тип_свойства] [= инициализатор_свойства]
[getter]
[setter]
Геттеры и сеттеры необязательно определять именно для свойств внутри класса, они могут также применяться к переменным верхнего уровня.
Сеттер определяет логику установки значения переменной. Он определяется с помощью слова set. Например, у нас есть переменная age, которая хранит возраст пользователя и представляет числовое значение.
Но теоретически мы можем установить любой возраст: 2, 6, -200, 100500. И не все эти значения будут корректными. Например, у человека не может быть отрицательного возраста. И для проверки входных значений можно использовать сеттер:
Блок set определяется сразу после свойства, к которому оно относится - в данном случае после свойства age. При этом блок set фактически представляет собой функцию, которая принимает один параметр - value, через этот параметр передается устанавливаемое значение. Например, в выражении age = 45 число 45 и будет представлять тот объект, который будет храниться в value.
В блоке set проверяем, входит ли устанавливаемое значение в диапазон допустимых значений. Если входит, то есть если значение корректно, то передаем его объекту field. Если значение некорректно, то свойство просто сохраняет свое предыдущее значение.
Идентификатор field представляет автоматически генерируемое поле, которое непосредственно хранит значение свойства. То есть свойства фактически представляют надстройку над полями, но напрямую в классе мы не можем определять поля, мы можем работать только со свойствами. Стоит отметить, что к полю через идентификатор field можно обратиться только в геттере или в сеттере, и в каждом конкретном свойстве можно обращаться только к своему полю.
В функции main при втором обращении к сеттеру (age = -345) можно заметить, что значение свойства age не изменилось. Так как новое значение -345 не входит в диапазон от 0 до 110.
Java:
var age: Int = 18
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 проверяем, входит ли устанавливаемое значение в диапазон допустимых значений. Если входит, то есть если значение корректно, то передаем его объекту field. Если значение некорректно, то свойство просто сохраняет свое предыдущее значение.
Идентификатор field представляет автоматически генерируемое поле, которое непосредственно хранит значение свойства. То есть свойства фактически представляют надстройку над полями, но напрямую в классе мы не можем определять поля, мы можем работать только со свойствами. Стоит отметить, что к полю через идентификатор field можно обратиться только в геттере или в сеттере, и в каждом конкретном свойстве можно обращаться только к своему полю.
В функции main при втором обращении к сеттеру (age = -345) можно заметить, что значение свойства age не изменилось. Так как новое значение -345 не входит в диапазон от 0 до 110.
Геттер управляет получением значения свойства и определяется с помощью ключевого слова get:
Справа от выражения get() через знак равно указывается возвращаемое значение. В данном случае возвращается значения поля field, которое хранит значение свойства name. Хотя в таком геттер большого смысла нет, поскольку получить подобное значение мы можем и без геттера.
Если геттер должен содержать больше инструкций, то геттер можно оформить в блок с кодом внутри фигурных скобок:
Если геттер оформлен в блок кода, то для возвращения значения необходимо использовать оператор return. И, таким образом, каждый раз, когда мы будем получать значение переменной age (например, в случае с вызовом println(age)), будет срабатывать геттер, когда возвращает значение. Например:
Консольный вывод программы
Java:
var age: Int = 18
set(value){
if((value>0) and (value <110))
field = value
}
get() = field
Если геттер должен содержать больше инструкций, то геттер можно оформить в блок с кодом внутри фигурных скобок:
Java:
var age: Int = 18
set(value){
println("Call setter")
if((value>0) and (value <110))
field = value
}
get(){
println("Call getter")
return field
}
Java:
fun main() {
println(age) // срабатывает get
age = 45 // срабатывает set
println(age) // срабатывает get
}
Код:
Call getter
18
Call setter
Call getter
45
Хотя геттеры и сеттеры могут использоваться к глобальным переменным, как правило, они применяются для опосредования доступа к свойствам класса.
Используем сеттер:
При втором обращении к сеттеру (bob.age = -8) можно заметить, что значение свойства age не изменилось. Так как новое значение -8 не входит в диапазон от 0 до 110.
Используем сеттер:
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
}
}
Геттер может возвращать вычисляемые значения, которые могут задействовать несколько свойств:
Здесь свойство fullname определяет блок get, который возвращает полное имя пользователя, созданное на основе его свойств firstname и lastname. При этом значение самого свойства fullname напрямую мы изменить не можем - оно определено доступно только для чтения. Однако если изменятся значения составляющих его свойств - firstname и lastname, то также изменится значение, возвращаемое из fullname.
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"
}
Выше уже рассматривалось, что с помощью специального поля field в сеттере и геттере можно обращаться к непосредственному значению свойства, которое хранится в специальном поле. Однако мы сами можем явным образом определить подобное поле. Нередко это приватное поле:
Можно использовать одновременно и геттер, и сеттер:
Здесь для свойства age добавлены геттер и сеттер, которые фактически являются надстройкой над полей _age, которое собственно хранит значение.
Можно использовать одновременно и геттер, и сеттер:
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
}
Kotlin позволяет переопределять в производном классе функции и свойства, которые определенны в базовом классе. Чтобы функции и свойства базового класа можно было переопределить, к ним применяется аннотация open. При переопределении в производном классе к этим функциям применяется аннотация override.
Чтобы указать, что свойство можно переопределить в производном классе, перед его определением указывается ключевое слово open:
В данном случае свойство age доступно для переопределения.
Если свойство определяется через первичный конструктор, то также перед его определением ставится аннотация open:
В производном классе для переопределения свойства перед ним указывается аннотация override.
Здесь переопределение заключается в изменении начального значения для свойства age.
Также переопределить свойство можно сразу в первичном конструкторе:
Применение:
Консольный вывод:
Java:
open class Person(val name: String){
open var age: Int = 1
}
Если свойство определяется через первичный конструктор, то также перед его определением ставится аннотация open:
Java:
open class Person(val name: String, open var age: Int = 1){
}
Java:
open class Person(val name: String, open var age: Int = 1){
}
open class Employee(name: String): Person(name){
override var age: Int = 18
}
Также переопределить свойство можно сразу в первичном конструкторе:
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
Также можно переопределять геттеры и сеттеры свойств:
Здесь класс Employee переопределяет геттер свойства fullInfo и сеттер свойства age
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)
}
Чтобы функции базового класа можно было переопределить, к ним применяется аннотация open. При переопределении в производном классе к этим функциям применяется аннотация override:
Функция display определена в классе Person с аннотацией open, поэтому в производных классах его можно переопределить. В классе Employee эта функция переопределена с применением аннотации 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
}
Стоит учитывать, что переопределить функции можно по всей иерархии наследования. Например, у нас может быть класс Manager, унаследованный от Employee:
В данном случае класс Manager переопределяет функцию display, поскольку среди его базовых классов есть класс Person, который определяет эту функцию с ключевым словом open.
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")
}
}
В это же время иногда бывает необходимо запретить дальнейшее переопределение функции в классах-наследниках. Для этого применяется ключевое слово 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 в производном классе можно обращаться к реализации из базового класса.
В данном случае производный класс Employee при переопределении свойства и функции применяет реализацию из базового класса Person. Например, через super.fullInfo возвращается значение свойства из базового класса (то есть значение свойства name), а с помощью вызова super.display() вызывается реализация функции display из класса Person.
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")
}
}
Абстрактные классы - это классы, определенные с модификатором abstract. Отличительной особенностью абстрактных классов является то, что мы не можем создать объект подобного класса. Например, определим абстрактный класс Human:
Абстрактный класс, как и обычный, может иметь свойства, функции, конструкторы, но создать его объект напрямую вызвав его конструктор мы не можем:
Такой класс мы можем только унаследовать:
Стоит отметить, что в данном случае перед абстрактным классом не надо указывать аннотацию open, как при наследовании неабстрактных классов.
Абстрактные классы могут иметь абстрактные методы и свойства. Это такие функции и свойства, которые определяются с ключевым словом abstract. Абстрактные методы не содержат реализацию, то есть у них нет тела. А для абстрактных свойств не указывается значение. При этом абстрактные методы и свойства можно определить только в абстрактных классах:
Если класс наследуется от абстрактного класса, то он должен либо реализовать все его абстрактные методы и свойства, либо также быть абстрактным.
Так, в данном случае класс Person должен обязательно определить реализацию для функции hello() и свойства age. При этом, как и при переопределении обычных методов и свойств, применяется аннотация override.
Абстрактные свойства также можно реализовать в первичном конструкторе:
Зачем нужны абстрактные классы? Классы обычно отражают какие-то сущности реального мира. Но некоторые из этих сущностей представляют абстракцию, которая непосредственного воплощения не имеет. Например, возьмем систему геометрических фигур. В реальности не существует геометрической фигуры как таковой. Есть круг, прямоугольник, квадрат, но просто фигуры нет. Однако же и круг, и прямоугольник имеют что-то общее и являются фигурами. В этом случае мы можем определить абстрактный класс фигуры и затем от него унаследовать все остальные классы фигур:
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)
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
}
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. Например:
Например, в данном случае интерфейс Movable представляет функцонал транспортного средства. Он содержит две функции и одно свойство. Функция move() представляет абстрактный метод - она не имеет реализации. Вторая функция stop() имеет реализацию по умолчанию.
При определении свойств в интерфейсе им не присваиваются значения.
Мы не можем напрямую создать объект интерфейса, так как интерфейс не поддерживает конструкторы и просто представляет шаблон, которому класс должен соответствовать.
Определим два класса, которые применяют интерфейс:
Для применения интерфейса после имени класса ставится двоеточие, за которым следует название интерфейса. При применении интерфейса класс должен реализовать все его абстрактные методы и свойства, а также может предоставить свою реализацию для тех свойств и методов, которые уже имеют реализацию по умолчанию. При реализации функций и свойств перед ними ставится ключевое слово override.
Так, класс Car представляет машину и применяет интерфейс Movable. Так как интерфейс содержит абстрактный метод move(), то класс Car обязательно должен его реализовать.
Тоже касается свойства speed - класс Car должен его определить. Здесь реализация свойства заключается в установке для него начального значения.
А вот функцию stop() класс Car может не реализовать, так как она уже содержит реализацию по умолчанию.
Класс Aircraft представляет самолет и тоже применяет интерфейс Movable. При этом класс Aircraft реализует обе функции интерфейса.
В последствии в программе мы можем рассматривать объекты классом Car и Aircraft как объекты Movable:
Консольный вывод программы:
Для определения интерфейса применяется ключевое слово interface. Например:
Java:
interface Movable{
var speed: Int // объявление свойства
fun move() // определение функции без реализации
fun stop(){ // определение функции с реализацией по умолчанию
println("Остановка")
}
}
При определении свойств в интерфейсе им не присваиваются значения.
Мы не можем напрямую создать объект интерфейса, так как интерфейс не поддерживает конструкторы и просто представляет шаблон, которому класс должен соответствовать.
Определим два класса, которые применяют интерфейс:
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("Приземление")
}
}
Так, класс 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, который объявляет ряд свойств:
Первое свойство имеет геттер, а это значит, что оно имеет реализацию по умолчанию. При применении интерфейса такое свойство необязательно реализовать. Второе свойство - number является абстрактным, оно не имеет ни геттера, ни сеттера, то есть не имеет реализации по умолчанию, поэтому классы его обязаны реализовать.
Для реализации интерфейса возьмем выше определенный класс Car:
Теперь класс Car применяет два интерфейса. Класс может применять несколько интерфейсов, в этом случае они указываются через запятую, и все эти интерфейсы класс должен реализовать. Класс Car реализует оба свойства. При этом при реализации свойств в классе необязательно указывать геттер или сеттер. Кроме того, можно реализовать свойства в первичном конструкторе, как это сделано в случае со свойствами model и number
Применение класса:
Java:
interface Info{
val model: String
get() = "Undefined"
val number: String
}
Для реализации интерфейса возьмем выше определенный класс Car:
Java:
class Car(override val model: String, override var number: String) : Movable, Info{
override var speed = 60
override fun move(){
println("Машина едет со скоростью $speed км/ч")
}
}
Применение класса:
Java:
fun main() {
val tesla: Car = Car("Tesla", "2345SDG")
println(tesla.model)
println(tesla.number)
tesla.move()
tesla.stop()
}
В Kotlin мы можем наследовать класс и применять интерфейсы. При этом мы можем одновременно и наследоваться от класса, и применять один или несколько интерфейсов. Однако что, если переопределяемая функция из базового класса имеет то же имя, что и функция из применяемого интерфейса:
Здесь класс Video и интерфейс AudioPlayable определяют функцию play. В этом случае класс MediaPlayer, который наследуется от Video и применяет интерфейс AudioPlayable, обязательно должен определить функцию с тем же именем, то есть play. С помощью конструкции super<имя_типа>.имя_функции можно обратиться к опредленной реализации либо из базового класса, либо из интерфейса.
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()
}
}
В Kotlin классы и интерфейсы могут быть определены в других классах и интерфейсах. Такие классы (вложенные классы или nested classes) обычно выполняют какую-то вспомогательную роль, а определение их внутри класса или интерфейса позволяет разместить их как можно ближе к тому месту, где они непосредственно используются.
Например, в следующем случае определяется вложенный класс:
В данном случае класс Account является вложенным, а класс Person{ - внешним.
По умолчанию вложенные классы имеют модификатор видимости public, то есть они видимы в любой части программы. Но для обращения к вложенному классу надо использовать имя внешнего класса. Например, создание объекта вложенного класса:
Если необходимо ограничить область применения вложенного класса только внешним классом, то следует определить вложенный класс с модификатором private:
Классы также могут содержать вложенные интерфейсы. Кроме того, интерфейсы тоже могут содержать вложенные классы и интерфейсы:
Например, в следующем случае определяется вложенный класс:
Java:
class Person{
class Account(val username: String, val password: String){
fun showDetails(){
println("UserName: $username Password: $password")
}
}
}
По умолчанию вложенные классы имеют модификатор видимости public, то есть они видимы в любой части программы. Но для обращения к вложенному классу надо использовать имя внешнего класса. Например, создание объекта вложенного класса:
Java:
fun main() {
val userAcc = Person.Account("qwerty", "123456");
userAcc.showDetails()
}
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) класс по умолчанию не имеет доступа к свойствам и функциям внешнего класса. Например, в следующем случае при попытке обратиться к свойству внешнего класса мы получим ошибку:
В данном случае у нас определен класс банковского счета BankAccount, который определяет свойство sum - сумма на счете и функцию display() для вывода информации о счете.
Кроме того, в классе BankAccount определен вложенный класс Transaction, который представляет операцию по счету. В данном случае класс Transaction определяет функцию pay() для оплаты со счета. Однако в нем мы не можем обратиться в свойствам и функциям внешнего класса BankAccount.
Чтобы вложенный класс мог иметь доступ к свойствам и функциям внешнего класса, необходимо определить вложенный класс с ключевым словом inner. Такой класс еще называют внутренним классом (inner class), чтобы отличать от обычных вложенных классов. Например:
Теперь класс Transaction определен с ключевым словом inner, поэтому имеет полный доступ к свойствам и функциям внешнего класса BankAccount. Но теперь если мы хотим использовать объект подобного вложенного класса, то необходимо создать объект внешнего класса:
Java:
class BankAccount(private var sum: Int){
fun display(){
println("sum = $sum")
}
class Transaction{
fun pay(s: Int){
sum -= s
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()
}
}
}
Java:
val acc = BankAccount(3400);
acc.Transaction().pay(2500)
Но что если свойства и функции внутреннего класса называются также, как и свойства и функции внешнего класса? В этом случае внутренний класс может обратиться к свойствам и функциям внешнего через конструкцию this@название_класса.имя_свойства_или_функции:
Например, перепишем случай выше с классами Account и Transaction следующим образом:
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
}
}
}
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:
При компиляции такого класса компилятор автоматически добавляет в класс функции с определенной реализацией, которая учитывает свойства класса, которые определены в первичном конструкторе:
Например, возьмем функцию toString(), которая возвращает строковое представление объекта:
Результатом программы будет следующий вывод:
По умолчанию строковое представление объекта нам практически ни о чем не говорит. Как правило, данная функция предназначена для вывода состояния объекта, но для этого ее надо переопределять. Однако теперь добавим модификатор data к определению класса:
И результат будет отличаться:
[CODO=java]Person(name=Alice, age=24)
В этом случае для функции toString() компилятор не будет определять реализацию.
Другим показательным примером является копирование данных:
Опять же компилятор генерирует функцию копирования по умолчанию, которую мы можем использовать. Если мы хотим, чтобы некоторые данные у объкта отличались, то мы их можем указать в функции copy в виде именованных арументов, как в случае со свойством name в примере выше.
При этом чтобы класс определить как data-класс, он должен соответствовать ряду условий:
Также стоит отметить, что несмотря на то, что мы можем определять свойства в первичном конструкторе и через val, и через var, например:
Но вообще в ряде ситуаций рекомендуется определять свойства через val, то есть делать их неизменяемыми, поскольку на их основании вычисляет хеш-код, который используется в качестве ключа объекта в такой коллекции как HashMap.
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
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"
}
}
Другим показательным примером является копирование данных:
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)
При этом чтобы класс определить как data-класс, он должен соответствовать ряду условий:
- Первичный конструктор должен иметь как минимум один параметр
- Все параметры первичного конструктора должны предваряться ключевыми словами val или var, то есть определять свойства
- Свойства, которые определяются вне первичного конструктора, не используются в функциях toString, equals и hashCode
- Класс не должен определяться с модификаторами open, abstract, sealed или inner.
Также стоит отметить, что несмотря на то, что мы можем определять свойства в первичном конструкторе и через val, и через var, например:
Java:
data class Person(var name: String, var age: Int)
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. Например, определим перечисление:
Данное перечисление Day представляет день недели. Внутри перечисления определяются константы. В данном случае это названия семи дней недели. Константы определяются через запятую. Каждая константа фактически представляет объект данного перечисления.
Классы перечислений как и обычные классы также могут иметь конструктор. Кроме того, для констант перечисления также может вызываться конструктор для их инициализации.
В примере выше у класса перечисления через конструктор определяется свойство value. Соответственно при определении констант перечисления необходимо каждую из этих констант инициализировать, передав значение для свойства value.
При этом перечисления - это не просто список значений. Они могут определять также свойства и функции. Но если класс перечисления содержит свойства или функции, то константы должны быть отделены точкой с запятой.
В данном случае в перечислении определена функция getDuration(), которая вычисляет разницу в днях между двумя днями недели.
Java:
enum class Day{
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
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
}
При этом перечисления - это не просто список значений. Они могут определять также свойства и функции. Но если класс перечисления содержит свойства или функции, то константы должны быть отделены точкой с запятой.
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
}
Все перечисления обладают двумя встроенными свойствами:
Кроме того, в Kotlin нам доступны вспомогательные функции:
- 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
}
- valueOf(value: String): возвращает объект перечисления по названию константы
- values(): возвращает массив констант текущего перечисления
Java:
fun main() {
for(day in Day.values())
println(day)
println(Day.valueOf("FRIDAY"))
}
Константы перечисления могут определять анонимные классы, которые могут иметь собственные методы и свойства или реализовать абстрактные методы класса перечисления:
В данном случае класс перечисления DayTime определяет абстрактный метод printName() и две переменных - startHour (начальный час) и endHour (конечный час). А константы определяют анонимные классы, которые реализуют эти свойства и функцию.
Также, классы перечислений могут применять интерфейсы. Для этого для каждой константы определяется анонимный класс, который содержат все реализуемые свойства и функции:
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}")
}
Также, классы перечислений могут применять интерфейсы. Для этого для каждой константы определяется анонимный класс, который содержат все реализуемые свойства и функции:
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() // Ночь
}
Нередо перечисления применяются для хранения состояния в программе. И в зависимоси от этого состояния мы можем направить действие программы по определенному пути. Например, определим перечисление, которое представляет арифметические операции, и функцию, которая в зависимости от переданной операции выполняет то или иное действие:
Функция operate() принимает два числа - операнды операции и тип операции в виде перечисления Operation. И в зависимоси от значения перечисления возвращает либо сумму, либо разность, либо произведение двух чисел.
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
}
}
Делегирование представляет паттерн объектно-ориентированного программирования, который позволяет одному объекту делегировать/перенаправить все запросы другому объекту. В определенной степени делегирование может выступать альтернативой наследованию. И преимуществом Kotlin в данном случае состоит в том, что Kotlin нативно поддерживает данный паттерн, предоставляя необходимый инструментарий.
Формальный синтаксис:
Есть некоторый интерфейс - Base, который определяет некоторый функционал. Есть его реализация в виде класса BaseImpl.
И есть еще один класс - Derived, который также применяет интерфейс Base. Причем после указания применяемого интерфейса идет ключевое слово by, а после него - объект, которому будут делегироваться вызовы.
То есть в данной схеме класс Derived будет делегировать вызовы объекту someBase, который представляет интерфейс Base и передается через первичный конструктор. При этом Derived может не реализовать интерфейс Base или реализовать неполностью - какие-то отдельные свойства и функции.
Например, рассмотрим следующие классы:
Здесь определен интерфейс Messenger, который представляет условно программу для отправки сообщений. Для условной отправки сообщений определена функция send().
Также есть класс InstantMessenger - программа мгновенных сообщений или проще говоря мессенджер, который применяет интерфейс Messenger, реализуя его функцию send()
Далее определен класс SmartPhone, который представляет смартфон и также применяет интерфейс Messenger, но не реализует его. Вместо этого он принимает через первичный конструктор объект Messenger и делегирует ему обращение к функции send().
Применим классы:
Здесь создан объект pixel, который представляет класс SmartPhone. Поскольку SmartPhone применяет интерфейс Messenger, то мы можем вызвать у объекта pixel функцию send() для отправки условного сообщения. Однако сам класс SmartPhone НЕ реализует функцию send - само выполнение этой функции делегируется объекту telegram, который в реальности выполняет отправку сообщения. Соответственно при выполнении программы мы увидим следующий консольный вывод:
Формальный синтаксис:
Java:
interface Base {
fun someFun()
}
class BaseImpl() : Base {
override fun someFun() { }
}
class Derived(someBase: Base) : Base by someBase
И есть еще один класс - Derived, который также применяет интерфейс Base. Причем после указания применяемого интерфейса идет ключевое слово by, а после него - объект, которому будут делегироваться вызовы.
Java:
class Derived(someBase: Base) : Base by someBase
Например, рассмотрим следующие классы:
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
Также есть класс 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")
}
Код:
Message `Hello Kotlin` has been sent
Message `Learn Kotlin on Metanit.com` has been sent
Подобным образом один объект может делегировать выполнение различных функций разным объектам. Например:
Здесь класс SmartPhone также реализует интерфейс PhotoDevice, который предоставляет функцию takePhoto() для съемки фото. Но выполнение этой функции он делегирует параметру p, который представляет интерфейс PhotoDevice и в роли которого выступает объект PhotoCamera.
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 реализует функцию sendTextMessage(), поэтому ее выполнение не делегируется. Консольный вывод программы:
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")
}
Код:
Send sms
Send video message
По аналогии с функциями объект может делегировать обращение к свойствам:
Здесь при интерфейс Messenger определяет свойство programName - название программы отправки. Класс SmartPhone не реализует это свойство, поэтому обращение к этому свойству делегируется объекту m.
Если бы класс SmartPhone сам реализовал это свойство, то делегирования бы не было:
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
Если бы класс 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:
После ключевого слова object идет блок кода в фигурных скобках, в которые помещается определение объекта. Как и в обычном классе, анонимный объект может содержать свойства, функции. И далее по имени переменной мы можем обращаться к свойствам и функциям этого объекта.
Анонимные классы не используют ключевое слово 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 через двоеточия указывается имя наследуемого класса или его первичный конструктор:
Здесь класс анонимного объекта наследует класс Person и переопределяет его функцию sayHello().
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, мы можем передавать этот анонимный объект параметру функции, который имеет тип Person.
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")
}
Функция может возвращать анонимный объект:
Однако тут есть нюансы. Чтобы мы могли обращаться к свойствам и функциям анонимного объекта, функция, которая возвращает этот объект, должна быть приватной, как в примере выше.
Если функция имеет модификатор public или private inline, то в этом случае свойства и функции анонимного класса (за исключением унаследованных) недоступны:
В данном случае функция createPerson() имеет модификатор private inline, поэтому у анонимного объекта будут доступны только унаследованные свойства и функции от класса 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)
Обобщенные типы (generic types) представляют типы, в которых типы объектов параметризированы. Что это значит? Рассмотрим следующий класс:
Класс Person использует параметр T. Параметры указываются после имени класса в угловых скобках. Данный параметр будет представлять некоторый тип данных, который на момент определения класса неизвестен.
В первичном конструкторе определяется свойство id, которое представляет идентификатор. Оно представляет тип, который передается через параметр T. На момент определения класса Person мы не знаем, что это будет за тип.
Само название параметра произвольное (если оно не совпадает с ключевыми словами). Но нередко используется T как сокращение от слова type.
При использовании типа Person необходимо его типизировать определенным типом, то есть указать, какой тип будет передаваться через параметр T:
Для типизации объекта после названия типа в угловых скобках указывается конкретный тип:
В данном случае мы говорим, что параметр T фактически будет представлять тип Int. Поэтому в конструктор объекта Person для свойства id необходимо передать числовое значение Int:
Второй объект типизируется типом String, поэтому в конструкторе для свойства id передается строка:
Если конструктор использует параметр T, то в принципе мы можем не указывать, каким типом типизируется объект - данный тип будет выводиться из типа параметра конструктора:
При этом параметры типа могут широко применяться внутри класса, не только при определении свойств, но и в функциях:
Здесь класс Person определяет функцию checkId(), которая проверяет, равен ли id значению параметра _id. При этом параметр _id имеет тип T - то есть он будет представлять тот же тип, что и свойство id.
Стоит отметить, что generic-типы широко используются в Kotlin. Самый показательный пример, который представлен классом - Array<T>. Параметр класса определяет, элементы какого типа массив будет хранить:
Java:
class Person<T>(val id: T, val name: String)
В первичном конструкторе определяется свойство 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>
Java:
Person(367, "Tom")
Java:
val bob: Person<String> = Person("A65", "Bob")
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")
}
}
}
Стоит отметить, что generic-типы широко используются в Kotlin. Самый показательный пример, который представлен классом - Array<T>. Параметр класса определяет, элементы какого типа массив будет хранить:
Java:
val people: Array<String> = arrayOf("Tom", "Bob", "Sam")
val numbers: Array<Int> = arrayOf(1, 2, 3, 4)
Можно одновременно использовать несколько параметров:
В данном случае класс Word применяет два параметра - K и V. При создании объекта Word эти параметры могут представлять один и тот же тип, а могут представлять и разные типы.
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)
Функции, как и классы, могут быть обобщенными.
Функция display() параметризирована параметром T. Параметр также указывается в угловых скобках после слова fun и перед названием функции. Функция принимает один параметр типа T и выводит его значение на консоль. И при использовании функции мы можем передавать в нее данные любых типов.
Другой более практический пример - определим функцию, которая будет возвращать наибольший массив:
Здесь функция getBiggest() в качестве параметров принимает два массива. При этом мы точно не значем, объекты какого типа эти массивы будут содержать. Однако оба массива типизированы параметром T, что гарантирует, что оба массива будут хранить объекты одного и того же типа. Внутри функции сравниваем размер массивов с помощью их свойства size и возвращаем наибольший массив.
Java:
fun main() {
display("Hello Kotlin")
display(1234)
display(true)
}
fun <T> display(obj: T){
println(obj)
}
Другой более практический пример - определим функцию, которая будет возвращать наибольший массив:
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
}
Ограничения обобщений (generic constraints) ограничивают набор типов, которые могут передаваться вместо параметра в обобщениях.
Например, мы хотим определить универсальную функцию для сравнения двух объектов и возвращать из функции наибольший объект. На первый взгляд мы можем просто определить обобщенную функцию:
Но компилятор не скомпилирует эту функцию, потому что вместо параметра типа T могут передаваться самые различные типы, в том числе такие, которые не поддерживают операцию сравнения.
Однако все типы, которые по умолчанию поддерживают эту операцию сравнения, применяют интерфейс Comparable. То есть нам надо, чтобы два параметра представляли один и тот же тип, который реализует тип Comparable. И в этом случае можно определить одну обобщенную функцию, которая будет ограниченна типом Comparable:
Ограничение указывается после названия параметра через двоеточие: <T: Comparable<T>> - то есть в данном случае тип T ограничен типом Comparable<T>, иначе говоря должен представлять тип Comparable<T>. Причем тип Comparable сам является обобщенным.
Стоит отметить, что по умолчанию ко всем параметрам типа также применяется ограничение в виде типа Any?. То есть определение параметра типа <T> фактически аналогично определению <T: Any?>
Подобным образом мы можем использовать в качестве ограничений собственные типы. Например, нам надо определить функцию для условной отправки сообщения:
Здесь определен интерфейс Message, который имеет одно свойство - text и представляет условное сообщение. И также есть два класса, которые реализуют этот интерфейс: EmailMessage и SmsMessage.
Функция send() использует ограничение <T:Message>, то есть она принимает объект некоторого типа, который должен реализовать интерфейс Message.
Далее мы можем вызвать эту функцию, передав ей соответствующий объект:
Здесь в обоих вызовах функция send() ожидает объект Message. Однако мы можем указать точный тип, используемый функцией
Например, мы хотим определить универсальную функцию для сравнения двух объектов и возвращать из функции наибольший объект. На первый взгляд мы можем просто определить обобщенную функцию:
Java:
fun <T> getBiggest(a: T, b: T): T{
if(a > b) return a // ! Ошибка
else return b
}
Однако все типы, которые по умолчанию поддерживают эту операцию сравнения, применяют интерфейс 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
}
Стоит отметить, что по умолчанию ко всем параметрам типа также применяется ограничение в виде типа 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
Функция send() использует ограничение <T:Message>, то есть она принимает объект некоторого типа, который должен реализовать интерфейс Message.
Далее мы можем вызвать эту функцию, передав ей соответствующий объект:
Java:
fun main() {
val email1 = EmailMessage("Hello Kotlin")
send(email1)
val sms1 = SmsMessage("Привет, ты спишь?")
send(sms1)
}
В примере выше мы могли передавать в функцию getBiggest() любой объект, который реализует интерфейс Comparable. Но что, если мы хотим, чтобы функция могла сравнивать только числа? Все числовые типы данных наследуются от базового класса Number. И мы можем задать еще одно ограничение - чтобы сравниваемый объект представлял тип Number:
Если параметра типа надо установить несколько ограничений, то все они указываются после возвращаемого типа функции после слова where через запятую в форме:
И в этом случае мы сможем передать в функцию объекты, которые одновременно реализуют интерфейс Comparable и являются наследниками класса Number:
Подобным образом мы можем использовать собственные типы в качестве ограничений:
Здесь для функции send() установлено два ограничения: используемый параметр типа T должен представлять одновременно оба интерфейса - Message и Logger.
Java:
fun <T> getBiggest(a: T, b: T): T where T: Comparable<T>,
T: Number {
return if(a > b) a
else b
}
Java:
параметр_типа: ограничений
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>
Классы, как и функции, могут принимать ограничения обощений. Например, установка одного ограничения:
Установка нескольких ограничений:
Здесь стоит обратить внимание, что поскольку конструктор класса Messenger не принимает параметров типа T, то нам надо явным образом указать, какой именно тип будет использоваться:
В качестве альтернативы можно было бы явным образом указать тип переменной:
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")
}
Java:
val outlook = Messenger<EmailMessage>()
Java:
val outlook: Messenger<EmailMessage> = Messenger()
Вариантность описывает, как обобщенные типы, типизированные классами из одной иерархии наследования, соотносятся друг с другом.
Инвариантность предполагает, что, если у нас есть классы Base и Derived, где Derived - производный класс от Base, то класс C<Base> не является ни базовым классом для С<Derived>, ни производным. Например, у нас есть следующие типы:
В данном случае мы не можем присвоить объект Messenger<EmailMessage> переменной типа Messenger<Message> и наоборот, они никак между собой не соотносятся, несмотря на то, что EmailMessage наследуется от Message:
Мы можем присвоить переменным по умолчанию только объекты их типов:
Java:
interface Messenger<T: Message>()
open class Message(val text: String)
class EmailMessage(text: String): Message(text)
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:
В данном случае интерфейс Messenger является ковариантным, так как его параметр определен со словом out: interface Messenger<out T>. И теперь переменной типа Messenger<Message> мы можем присвоить значение типа Messenger<EmailMessage>
Вообще не случайно используется именно слово out. Оно указывает, что обобщенный тип может возвращать из функции значение типа T:
В данном случае обобщенный интерфейс Messenger определяет функцию writeMessage() для генерации объекта Message. Класс EmailMessenger применяет интерфейс Messenger и реализует эту функцию. То есть в данном случае тип EmailMessenger по сути представляет тип Messenger<EmailMessage>.
Поскольку в Messenger параметр T определен с аннотацией out, то мы можем присвоить переменной типа Messenger<Message> значение типа EmailMessenger (а по сути значение типа Messenger<EmailMessage>)
В то же время тип T нельзя использовать в качестве типа входных параметров функции. Например, в следующем случае компилятор известит нас об ошибке:
Для определения обобщенного типа как ковариантного параметр обощения определяется с ключевым словом out:
Java:
interface Messenger<out T: Message>
open class Message(val text: String)
class EmailMessage(text: String): Message(text)
Java:
fun changeMessengerToEmail(obj: Messenger<EmailMessage>){
val messenger: Messenger<Message> = obj
}
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 параметр T определен с аннотацией out, то мы можем присвоить переменной типа Messenger<Message> значение типа EmailMessenger (а по сути значение типа Messenger<EmailMessage>)
Java:
val messenger: Messenger<Message> = EmailMessenger()
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:
В данном случае интерфейс Messenger является контравариантным, так как его параметр определен со словом in: interface Messenger<in T>. И теперь переменной типа Messenger<EmailMessage> мы можем присвоить значение типа Messenger<Message>
Применение аннотации in означает, что обобщенный тип может получать значение типа T через параметр функции:
В данном случае обобщенный интерфейс Messenger определяет функцию sendMessage(), которая принимает объект Message в качестве параметра. Класс InstantMessenger применяет интерфейс Messenger и реализует эту функцию. То есть в данном случае тип InstantMessenger по сути представляет тип Messenger<Message>.
Поскольку в интерфейсе Messenger параметр T определен с аннотацией in, то мы можем присвоить переменной типа Messenger<EmailMessage> значение типа InstantMessenger (то есть значение типа Messenger<Message>)
В то же время тип T нельзя использовать в качестве типа результата функции. Например, в следующем случае компилятор известит нас об ошибке:
Для определения обобщенного типа как контравариантного параметр обобщения определяется с ключевым словом in:
Java:
interface Messenger<in T: Message>
open class Message(val text: String)
class EmailMessage(text: String): Message(text)
Java:
fun changeMessengerToDefault(obj: Messenger<Message>){
val messenger: Messenger<EmailMessage> = obj
}
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 параметр T определен с аннотацией in, то мы можем присвоить переменной типа Messenger<EmailMessage> значение типа InstantMessenger (то есть значение типа Messenger<Message>)
Java:
val messenger: Messenger<EmailMessage> = InstantMessenger()
Java:
interface Messenger<in T: Message>{
fun writeMessage(text: String): T // Ошибка - тип T может представлять только параметр функции
fun sendMessage(message: T)
}
Исключение представляет событие, которое возникает при выполнении программы и нарушает ее нормальной ход. Например, при передаче файла по сети может оборваться сетевое подключение, и в результате чего может быть сгенерировано исключение. Если исключение не обработано, то программа падает и прекращает свою работу. Поэтому при возникновении исключений их следует обрабатывать.
Для обработки исключений применяется конструкция try..catch..finally. В блок try помещаются те действия, которые потенциально могут вызвать исключение (например, передача файла по сети, открытие файла и т.д.). Блок catch перехватывает возникшее исключение и обрабатывает его. Блок finally выполняет некоторые завершающие действия.
После оператора catch в скобках помещается параметр, который представляет тип исключения. Из этого параметра можно получить информацию о произошедшем исключении.
Блок finally является необязательным, его можно опустить. Блок catch также может отсутствовать, однако обязательно должен быть блок try и как минимум один из блоков: либо catch, либо finally. Также конструкция может содержать несколько блоков catch для обработки каждого типа исключения, которое может возникнуть.
Блок catch выполняется, если только возникло исключение. Блок finally выполняется в любом случае, даже если нет исключения.
Например, при делении на ноль Kotlin генерирует исключение:
Действие, которое может вызвать исключение, то есть операция деления, помещается в блок try. В блоке catch перехватываем исключение. При этом каждое исключение имеет определенный тип. В данном случае используется общий тип исключений - класс Exception:
Если необходимы какие-то завершающие действия, то можно добавить блок finally (например, если при работе с файлом возникает исключение, то в блоке finally можно прописать закрытие файла):
В этом случае консольный вывод будет выглядеть следующим образом:
Для обработки исключений применяется конструкция try..catch..finally. В блок try помещаются те действия, которые потенциально могут вызвать исключение (например, передача файла по сети, открытие файла и т.д.). Блок catch перехватывает возникшее исключение и обрабатывает его. Блок finally выполняет некоторые завершающие действия.
Java:
try {
// код, генерирующий исключение
}
catch (e: Exception) {
// обработка исключения
}
finally {
// постобработка
}
Блок 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)
}
}
Код:
Exception
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 предоставляет ряд свойств, которые позволяют получить различную информацию об исключении:
Из функций класса Exception следует выделить функцию printStackTrace(), которая выводит ту информацию, которая обычно отображается при необработанном исключении.
Применение свойств:
Консольный вывод программы:
- 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. Например, при одном исключении мы хотим производить одни действия, при другом - другие.
В данном случае при доступе по недействительному индексу в массиве будет генерироваться исключение типа ArrayIndexOutOfBoundsException. С помощью блока catch(e:ArrayIndexOutOfBoundsException). Если в программе будут другие исключения, которые не представляют тип ArrayIndexOutOfBoundsException, то они будут обрабатываться вторым блоком catch, так как Exception - это общий тип, который подходит под все типы исключений. При этом стоит отметить, что в начале обрабатывается исключение более частного типа - ArrayIndexOutOfBoundsException, и только потом - более общего типа Exception.
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)
}
Возможно, в каких-то ситуациях мы вручную захотим генерировать исключение. Для генерации исключения применяется оператор throw, после которого указывается объект исключения
Например, в функции проверки возраста мы можем генерировать исключение, если возраст не укладывается в некоторый диапазон:
После оператора throw указан объект исключения. Для определения объекта Exception применяется конструктор, который принимает в качестве параметра сообщение об исключении. В данном случае это сообщение о некорректности введенного значения.
И если при вызове функции checkAge() в нее будет передано число меньше 1 или больше 110, то будет сгенерировано исключение. Так, в данном случае консольный вывод будет следующим:
Но опять же поскольку генерируемое здесь исключение не обаботано, то программа при генерации исключения аварийно завершает работу. Чтобы этого не произошло, мы можем обработать генерируемое исключение:
Например, в функции проверки возраста мы можем генерировать исключение, если возраст не укладывается в некоторый диапазон:
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
}
И если при вызове функции 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 может возвращать значение. Например:
В данном случае переменная checkedAge1 получает результат функцию checkAge(). Если же произойдет исключение, тогда переменная checkedAge1 получает то значение, которое указано в блоке catch, то есть в данном случае значение null.
При необрабходимости в блок catch можно добавить и другие выражения или возвратить другое значение:
В данном случае, если будет сгенерировано исключение, то конструкция try выведет исключение и возвратит число 18. Возвращаемое значение указывается после всех остальных инструкций в блоке catch.
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
}
При необрабходимости в блок 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
}
Ключевое слово null представляет специальный литерал, который указывает, что переменная не имеет как такового значения. То есть у нее по сути отсутствует значение.
Подобное значение может быть полезно в ряде ситуациях, когда необходимо использовать данные, но при этом точно неизвестно, а есть ли в реальности эти данные. Например, мы получаем данные по сети, данные могут прийти или не прийти. Либо может быть ситуация, когда нам надо явным образом указать, что данные не установлены.
Однако переменным стандартных типов, например, типа Int или String или любых других классов, мы не можем просто взять и присвоить значение null:
Мы можем присвоить значение null только переменной, которая представляет тип Nullable. Чтобы превратить обычный тип в тип nullable, достаточно поставить после названия типа вопросительный знак:
При этом мы можем передавать переменным nullable-типов как значение null, так и конкретные значения, которые укладываются в диапазон значений данного типа:
Nullable-типы могут представлять и создаваемые разработчиком классы:
В то же время надо понимать, что String? и Int? - это не то же самое, что и String и Int. Nullable типы имеют ряд ограничений:
Java:
val n = null
println(n) // null
Однако переменным стандартных типов, например, типа Int или String или любых других классов, мы не можем просто взять и присвоить значение null:
Java:
val n : Int = null // ! Ошибка, переменная типа Int допускает только числа
Java:
// val n : Int = null //! ошибка, Int не допускает значение null
val d : Int? = null // норм, Int? допускает значение null
Java:
var age : Int? = null
age = 34 // Int? допускает null и числа
var name : String? = null
name = "Tom" // String? допускает null и строки
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)
- Значения 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, во время компиляции, а не во время выполнения. Например, возьмем следующий код:
Переменная name хранит строку "Tom". Переменная userName представляет тип String и тоже может хранить строки, но тем не менее напрямую в данном случае мы не можем передать значение из переменной name в userName. В данном случае для компилятора неизвестно, каким значением инициализирована переменная name. Ведь переменная name может содержать и значение null, которое недопустимо для типа String.
В этом случае мы можем использовать оператор ?:, который позволяет предоставить альтернативное значение, если присваиваемое значение равно null:
Оператор ?: принимает два операнда. Если первый операнд не равен null, то возвращается значение первого операнда. Если первый операнд равен null, то возвращается значение второго операнда.
То есть это все равно, если бы мы написали:
Но оператор ?: позволяет сократить подобную конструкцию.
Java:
var name : String? = "Tom"
val userName: String = name // ! Ошибка
В этом случае мы можем использовать оператор ?:, который позволяет предоставить альтернативное значение, если присваиваемое значение равно 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
То есть это все равно, если бы мы написали:
Java:
var name : String? = "Tom"
val userName: String
if(name!=null){
userName = name
}
Оператор ?. позволяет объединить проверку значения объекта на null и обратиться к функциям или свойствам этого объекта.
Например, у строк есть свойство length, которое возвращает длину строки в символах. У объекта String? мы просто так не можем обратиться к свойству length, так как если объект String? равен null, то и строки как таковой нет, и соответственно длину строки нельзя определить. И в этом случае мы можем применить оператор ?.:
Если переменная message вдруг равна null, то переменная length получит значение null. Если переменная name содержит строку, то возвращается длина этой строки. По сути выражение val length: Int? = message?.length эквивалентно следующему коду:
С помощью оператора ?. подобным образом можно обращаться к любым свойствам и функциям объекта.
Также в данном случае мы могли совместить оба выше рассмотренных оператора:
Теперь переменная length не допускает значения null. И если переменная name не определена, то length получает число 0.
Используя этот оператор, можно создавать цепочки проверок на null:
Здесь класс Person в первичном конструкторе принимает значение типа String?, то есть это можт быть строка, а может быть null.
Допустим, мы хотим получить переданное через конструктор имя пользователя в верхнем регистре (заглавными буквами). Для перевода текста в верхний регистр у класса String есть функция uppercase(). Однако может сложиться ситуация, когда либо объект Person равен null, либо его свойство name ( которое представляет тип String?) равно null. И в этом случае перед вызовом функции uppercase() нам надо проверять на null все эти объекты. А оператор ?. позволяет сократить код проверки:
То есть если tom не равен null, то обращаемся к его свойству name. Далее если name не равен null, то обращаемся к ее функции uppercase(). Если какое-то звено в этой проверки возвратит null, переменная tomName тоже будет равна null.
Но здсь мы также можем избежать финального возвращения null и присвоить значение по умолчанию:
Например, у строк есть свойство length, которое возвращает длину строки в символах. У объекта String? мы просто так не можем обратиться к свойству length, так как если объект String? равен null, то и строки как таковой нет, и соответственно длину строки нельзя определить. И в этом случае мы можем применить оператор ?.:
Java:
var message : String? = "Hello"
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
Используя этот оператор, можно создавать цепочки проверок на 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?)
Допустим, мы хотим получить переданное через конструктор имя пользователя в верхнем регистре (заглавными буквами). Для перевода текста в верхний регистр у класса String есть функция uppercase(). Однако может сложиться ситуация, когда либо объект Person равен null, либо его свойство name ( которое представляет тип String?) равно null. И в этом случае перед вызовом функции uppercase() нам надо проверять на null все эти объекты. А оператор ?. позволяет сократить код проверки:
Java:
val tomName: String? = tom?.name?.uppercase()
Но здсь мы также можем избежать финального возвращения null и присвоить значение по умолчанию:
Java:
val tomName: String = tom?.name?.uppercase() ?: "Undefined"
Оператор !! (not-null assertion operator) принимает один операнд. Если операнд равен null, то генерируется исключение. Если операнд не равен null, то возвращается его значение.
Поскольку данный оператор возвращает объект, который не представляет nullable-тип, то после применения оператора мы можем обратиться к методам и свойствам этого объекта:
Java:
fun main() {
try {
val name : String? = "Tom"
val id: String = name!!
println(id)
} catch (e: Exception) { println(e.message)}
}
Java:
val name : String? = null
val length :Int = name!!.length
Завтра дополню.
Progress( 6.3 - 9.9 )
CopyRight: Библиотека metanit.
Последнее редактирование: