Вы находитесь на странице: 1из 304

Сергей Пименов

ЯЗЫК
ПРОГРАММИРОВАНИЯ
KOTLIN

Киев

2017
УДК 004.4*Kotlin
П32

Пименов, Сергей
П32 Язык программирования Kotlin / Сергей Пименов — К. : «Агентство
«IPIO», 2017. — 304 с.
ISBN 978-617-7453-28-3
Книга представляет собой полное справочное пособие по язы-
ку программирования Kotlin. В книге подробно рассмотрены такие
вопросы как: типы данных, базовые синтаксические конструкции
языка, вопросы объектно-ориентированного программирования,
классы и интерфейсы, исключения. Книга изобилует примерами
кода, который можно загрузить из репозитория автора. Книга рас-
считана на разработчиков разной квалификации и будет полезна
как новичкам в программировании, так и опытным программи-
стам, решившим освоить новый отличный язык программирования
Kotlin.

УДК 004.4*Kotlin

Все права защищены.


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

ISBN 978-617-7453-28-3 © Сергей Пименов


© Издательство «Агентство «IPIO»
Предисловие

ПРЕДИСЛОВИЕ

Kotlin относительно молодой язык программирования, на момент на-


писания книги ему исполнилось 7 лет. Несмотря на свою молодость,
Kotlin признан компанией Google в качестве официального языка про-
граммирования для Android с first-class поддержкой.
Основные причины успеха Kotlin — его простота, краткость и выра-
зительность, безопасность и полная совместимость с Java.

Книга для всех

Эта книга предназначена для всех, кто занимается программирова-


нием, независимо от опыта и стажа. Цель этой книги — познакомить
читателя с отличным универсальным языком программирования Kotlin.
Автор надеется, что каждый найдет в этой книге что-либо полезное
для себя.

Структура книги

Эта книга служит справочным пособием по языку программирова-


ния Kotlin, в котором описываются его синтаксис, ключевые слова и ос-
новополагающие принципы программирования на языке Kotlin.

Исходный код примеров

Исходный код всех примеров, приведенных в этой книге, до-


ступен на GitHub по адресу: https://github.com/olton/
kotlin-examples
Также большое количество примеров вы найдете в репозито-
рии Egorand/kotlin-playground по адресу https://github.com/
Egorand/kotlin-playground и на официальном сайте Kotlin
по адресу https://kotlinlang.org/docs/tutorials/

3
Язык программирования Kotlin

АВТОР ВЫРАЖАЕТ
БЛАГОДАРНОСТЬ
Любимой жене Татьяне за любовь и поддержку.

Другу и шефу Александру Ольшанскому (https://www.facebook.com/


olshanskiy) за помощь в издании книги и за ту энергию, которую он
проецирует на других людей.

Компании Jetbrains за отличную IDE IntelliJ IDEA и другие продукты.

Разработчикам Kotlin за отличный язык программирования.

Всем, кто принимал участие в рождении и развитии языка Kotlin за их


участие.

Сайту kotlinlang.ru и Олегу Дуброву (https://github.com/phplego)

Отдельное спасибо Денису Седченко (https://www.facebook.com/


denissedchenko) за помощь в тестировании текста книги.

КНИГА ИЗДАНА И НАПЕЧАТАНА


ПРИ ПОДДЕРЖКЕ КОМПАНИЙ
ХОЛДИНГА INTERNET INVEST:
Imena.UA

Mirohost

Olshansky & Partners

4
СОДЕРЖАНИЕ

Глава 1. История и развитие языка . . . . . . . . . . . . . . . . . . . . . 8


Jetbrains . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Kotlin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Применение Kotlin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Глава 2. Краткий обзор Kotlin . . . . . . . . . . . . . . . . . . . . . . . . . 15
ООП . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . 15
Программа «Привет, мир!» . . . . . . . . . . . . . . . . . . . . . . . 19
Установка компилятора . . .
. . . . . . . . . . . . . . . . . . . . . . . 21
Компиляция программы . . . . . . . . . . . . . . . . . . . . . . . . . 22
Лексика . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . 24
Базовый синтаксис . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . 26
Ключевые слова . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . 35
Пакеты . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . 36
Глава 3. Типы данных и переменные . . . . . . . . . . . . . . . . . . . . 41
Типы данных . . . . .
.......... . . . . . . . . . . . . . . . . . . . 41
Числа . . . . . . . . . . .
.......... . . . . . . . . . . . . . . . . . . . 42
Символы . . . . . . . .
.......... . . . . . . . . . . . . . . . . . . . 48
Строки . . . . . . . . .
.......... . . . . . . . . . . . . . . . . . . . 49
Массивы . . . . . . . .
.......... . . . . . . . . . . . . . . . . . . . 51
Логический тип . . .
.......... . . . . . . . . . . . . . . . . . . . 54
Приведение типов .
.......... . . . . . . . . . . . . . . . . . . . 55
Псевдонимы типов.......... . . . . . . . . . . . . . . . . . . . 58
Глава 4. Операции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
Операции в Kotlin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
Арифметические операции . . . . . . . . . . . . . . . . . . . . . . . 60
Операции отношения . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Равенство . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . 69
Логические операции . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
Поразрядные операции . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Операция присваивания . . . . . . . . . . . . . . . . . . . . . . . . . 76
Тернарная операция . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Приоритет операций . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Перегрузка операторов . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Язык программирования Kotlin

Глава 5. Управляющие операторы . . . . . . . . . . . . . . . . . . . . . . 83


Управляющие операторы .
.. . . . . . . . . . . . . . . . . . . . . . . 83
Операторы выбора . . . . .
.. . . . . . . . . . . . . . . . . . . . . . . 83
Выражение if . . . . . . . . . .
.. . . . . . . . . . . . . . . . . . . . . . . 84
Оператор ?: . . . . . . . . . . .
.. . . . . . . . . . . . . . . . . . . . . . . 86
Выражение when . . . . . . .
.. . . . . . . . . . . . . . . . . . . . . . . 87
Операторы цикла . . . . . .
.. . . . . . . . . . . . . . . . . . . . . . . 90
Цикл for . . . . . . . . . . . . .
.. . . . . . . . . . . . . . . . . . . . . . . 90
Цикл while и do-while . . .
.. . . . . . . . . . . . . . . . . . . . . . . 92
Вложенные циклы . . . . . .
.. . . . . . . . . . . . . . . . . . . . . . . 94
Операторы перехода . . . .
.. . . . . . . . . . . . . . . . . . . . . . . 95
Глава 6. Функции и лямбды . . . . . . . . . . . . . . . . . . . . . . . . . . 102
Функции в Kotlin . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . 102
Применение функций . . . . . . . . . . . . . . . . .
. . . . . . . . . 104
Инфиксное обозначение . . . . . . . . . . . . . . .. . . . . . . . . 106
Параметры функции . . . . . . . . . . . . . . . . . .
. . . . . . . . . 106
Имена в названиях параметров . . . . . . . . . . . . . . . . . . . 108
Функции, возвращающие Unit . . . . . . . . . . . . . . . . . . . . 109
Функции с одним выражением . . . . . . . . . . . . . . . . . . . 110
Явные типы возвращаемых значений . . . . . . . . . . . . . . 110
Переменное число аргументов . . . . . . . . . . . . . . . . . . . . 110
Область действия функций . . . . . . . . . . . . . . . . . . . . . . 111
Функции с хвостовой рекурсией . . . . . . . . . . . . . . . . . . 114
Лямбда-выражения и анонимные функции . . . . . . . . . . 115
Высокоуровневые функции . . . . . . . . . . . . . . . . . . . . . . 120
Встроенные (inline) функции . . . . . . . . . . . . . . . . . . . . 123
Глава 7. Классы и объекты . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
Введение в классы . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . 129
Общая форма класса . . . . . . .
. . . . . . . . . . . . . . . . . . . . 129
Объявление класса . . . . . . . .
. . . . . . . . . . . . . . . . . . . . 130
Конструкторы . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . 135
Свойства и поля . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . 137
Методы и перегрузка методов . . . . . . . . . . . . . . . . . . . . 142
Класс Stack . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . 145
Модификаторы доступа . . . . . . . . . . . . . . . . . . . . . . . . 147
Интерфейсы . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . 151
Наследование . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . 160
Абстрактные классы . . . . . . .
. . . . . . . . . . . . . . . . . . . . 166
Классы данных . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . 167
Изолированные классы . . . . . . . . . . . . . . . . . . . . . . . . . 172
Перечисления . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . 174

6
Содержание

Вложенные классы . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179


Объекты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
Делегирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
Обобщения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Расширения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Улучшаем класс Stack . . . . . . . . . . . . . . . . . . . . . . . . . . 214
Глава 8. Обработка исключений . . . . . . . . . . . . . . . . . . . . . . . 216
Исключения в Kotlin . . . . . . . . . . . .
. . . . . . . . . . . . . . . 216
Классы исключений . . . . . . . . . . . . .
. . . . . . . . . . . . . . . 217
Необрабатываемые исключения . . . . . . . . . . . . . . . . . . 218
Обработка исключений . . . . . . . . . .
. . . . . . . . . . . . . . . 219
try — это выражение . . . . . . . . . . . .
. . . . . . . . . . . . . . . 221
Несколько операторов catch . . . . . .
. . . . . . . . . . . . . . . 221
Вложенные операторы try . . . . . . . .
. . . . . . . . . . . . . . . 223
Оператор throw . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . 224
Оператор finally . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . 226
Встроенные исключения . . . . . . . . .
. . . . . . . . . . . . . . . 227
Создание собственных исключений . . . . . . . . . . . . . . . 228
Цепочки исключений . . . . . . . . . . .
. . . . . . . . . . . . . . . 229
Глава 9. Рефлексия и аннотации . . . . . . . . . . . . . . . . . . . . . . 231
Рефлексия . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
Аннотации . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
Глава 10. Сопрограммы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
Введение в сопрограммы . . . . . . . . . . . . . . . . . . . . . . . . 246
Глава 11. Коллекции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
Введение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
Collection и MutableCollection . . . . . . . . . . . . . . . . . . . . 257
List и MutableList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Set и MutableSet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
Использование коллекций . . . . . . . . . . . . . . . . . . . . . . . 268
Map и MutableMap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
Глава 12. Другие особенности языка . . . . . . . . . . . . . . . . . . . . 277
Ключевое слово this . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
Интервалы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278
NULL-безопасность . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
Глава 13. Грамматика языка . . . . . . . . . . . . . . . . . . . . . . . . . . . 287
Грамматика . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287

7
Язык программирования Kotlin

ГЛАВА 1.
ИСТОРИЯ И РАЗВИТИЕ
ЯЗЫКА

JETBRAINS
Компания Jetbrains была основана в 2000 году тремя программистами:
Сергеем Дмитриевым, Евгением Беляевым и Валентином Кипятковым
с основной целью — создать мощную, полноценную IDE (интегриро-
ванная среда разработки) для Java. Штаб-квартира компании находит-
ся в Чехии, но JetBrains имеет множество представительств и в других
странах. В штате компании более 500 разработчиков, которые рабо-
тают в Санкт-Петербурге, Мюнхене, Праге, Бостоне и Москве и соз-
дают интеллектуальные инструменты, понимающие семантику кода
и повышающие продуктивность работы программистов. На данный
момент JetBrains сотрудничает с более чем 3000 компаний по всему
миру и в различных сферах деятельности: банки, финансирование, IT-
индустрия, биотехнологии, промышленность, программные продукты
и многое другое.
Первым продуктом компании был Renamer — небольшая програм-
ма, которая позволяла делать простой рефакторинг-переименование
для программ на языке Java. Программа давала возможность безопасно
переименовывать класс, пакет, метод или переменную в проекте. Вто-
рым продуктом стал CodeSearch — плагин для популярной в то время
IDE от Borland JBuilder, который позволял быстро и точно находить все
использования символа, метода или класса во всей программе.

8
Глава 1. История и развитие языка

Следующим продуктом стала IDE — IntelliJ IDEA, которая до сих пор


остается флагманом компании. Первая версия появилась в январе 2001
года и быстро приобрела популярность как первая среда для Java с широ-
ким набором интегрированных инструментов для рефакторинга, которые
позволяли программистам быстро реорганизовывать исходные тексты
программ. Дизайн среды ориентирован на продуктивность работы про-
граммистов позволяя сконцентрироваться на функциональных задачах,
в то время как IntelliJ IDEA берет на себя выполнение рутинных операций.
В последующие годы компания выпустила на базе IDEA IDE для та-
ких языков программирования, как C#, Ruby, Python, PHP, C/C++, Swift
и Objective-C, JavaScript, Go.
Помимо сред разработки компания также создает и другие полез-
ные инструменты, призванные упростить весь цикл — от идеи до реа-
лизации и внедрения программных продуктов. Среди таких инструмен-
тов стоить отметить: ReSharper, DataGrip, dotPeek, dotTrace, dotMemory,
dotCover, Youtrack, TeamCity, Upsource, Hub и MPS.
Весь перечень продуктов компании доступен по адресу: https://www.
jetbrains.com/products.html

KOTLIN
В 2010 году компания Jetbrains приступила к созданию нового языка
программирования. Необходимость такого решения была обусловле-
на несколькими факторами, в том числе и тем, что стало понятно, что
язык Java в некоторых вопросах уже не устраивает компанию, а суще-
ствующие альтернативные языки не соответствуют тем требованиям,
которые выдвигались к языку.
В компании собралось очень много людей с большим экспертным
опытом в области языков программирования. Это также способствова-
ло принятию решения о необходимости создать свой новый язык про-
граммирования, который бы удовлетворял требованиям текущего мо-
мента и мог бы решать задачи будущего.

Основными требованиями к новому языку были:


• Простота и эффективность
• Максимальная безопасность
• Полная совместимость с Java
• Статическая типизация
• Качественная инструментальная поддержка

9
Язык программирования Kotlin

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


ность и демократичность.
Языки бывают разные: некоторые выходят из академической среды,
другие созданы для конкретных платформ. В компании Jetbrains были
нацелены на практичный язык для максимально широкой аудитории.
Он должен был быть демократичным, то есть без заумных вещей. Бы-
вают ситуации, когда программист знает все тонкости языка и благода-
ря этому пишет хитрый код, — и в этот код никто из джуниор-програм-
мистов не может лезть. Компании был нужен язык, который одинаково
хорош как для начинающих программистов, так и для продвинутых.

Три причины начать дружить с Kotlin.

В первую очередь разработчики любят Kotlin за его краткость и вы-


разительность. Раньше людей это не очень смущало, потом размер про-
грамм стал больше — люди поняли, что пишут очень много совершен-
но бессмысленных кусков кода только потому, что от них это требует
синтаксис языка программирования.
Вторая причина в том, что он полностью совместим с Java и позво-
ляет постепенно мигрировать с Java на Kotlin.
Третья причина — Kotlin безопасен: в семантику языка заложе-
ны принципы, предотвращающие целый ряд очень распространенных
ошибок, которые обычно случаются в момент исполнения программы.
Это позволяет писать более безопасный код, что в конечном итоге по-
могает сэкономить деньги и снизить затраты на тестирование.

Сегодня с помощью Kotlin вы можете разрабатывать приложения


для:
• JVM
• Android
• Browser и NodeJS

Четыре кита, на которых стоит Kotlin:


• Краткость — меньшее количество шаблонов кода.
• Безопасность — позволяет избежать множества ошибок на этапе
разработки, таких как исключения нулевого указателя.
• Interoperable — позволяет использовать существующие библио-
теки Java, Android, JavaScript.
• Tool-friendly — используйте качественную инструментальную
поддержку языка на уровне IDE и других инструментов.

10
Глава 1. История и развитие языка

Итак, что же такое Kotlin?

Kotlin — современный статически типизированный объектно-ори-


ентированный язык программирования, компилируемый для платформ
Java и JavaScript. При полной совместимости с Java Kotlin предоставля-
ет дополнительные возможности, упрощающие повседневную работу
программиста и повышающие продуктивность. Он сочетает в себе ла-
коничность, выразительность, производительность и простоту в изуче-
нии. Kotlin компилируется в байткод, работающий поверх JVM. Также
он умеет компилироваться в JavaScript и на другие платформы через ин-
фраструктуру LLVM. Язык назван в честь острова Котлин в Финском
заливе, на котором расположен город Кронштадт.

Ключевые возможности Kotlin


• Полная совместимость с Java в обе стороны (код на Java и Kotlin
можно безболезненно смешивать в одном проекте)
• Автоматический вывод типов переменных и функций
• Анонимные функции (лямбда-выражения) позволяют писать бо-
лее компактный код
• Возможности создания проблемно-ориентированных языков
(DSL)
• Внешние функции позволяют расширять интерфейс существую-
щих классов, не меняя их
• Выразительная система типов позволяет обнаруживать многие
ошибки на этапе компиляции
• Конструкции, сокращающие лишние повторения кода: свойства,
значения параметров по умолчанию, мультиприсваивания, клас-
сы данных, автоматическое приведение типов и пр.

Авторы ставили целью создать язык более лаконичный и типобезо-


пасный, чем Java, и более простой, чем Scala. Следствием упрощения
по сравнению со Scala стали также более быстрая компиляция и луч-
шая поддержка языка в IDE.
Впервые публично Kotlin был представлен широкой публике в июле
2011 года. Исходный код реализации языка был открыт в феврале 2012.
В феврале был выпущен milestone 1, включающий плагин для IDEA.
В июне — milestone 2 с поддержкой Android. В декабре 2012 года вышел
milestone 4, включающий, в частности, поддержку Java 7.
В феврале 2016 года вышел официальный релиз языка Kotlin. На мо-
мент написания книги Kotlin достиг версии 1.1.2, анонс которой состо-
ялся 25 апреля 2017 года.

11
Язык программирования Kotlin

В мае 2017 года компания Google сообщила, что инструменты языка


Kotlin, основанные на JetBrains IDE, будут по стандарту включены в Android
Studio 3.0 — официальный инструмент разработки для ОС Android.
Kotlin позиционируется разработчиками как объектно-ориентиро-
ванный язык промышленного уровня, а также как язык, который смо-
жет заменить Java. При этом он полностью совместим с Java, что позво-
ляет разработчикам постепенно перейти с Java на Kotlin. В частности,
в Android язык интегрируется с помощью Gradle, что позволяет для су-
ществующего Android-приложения внедрять новые функции на Kotlin
без переписывания приложения целиком.
Среди компаний, которые так или иначе применяют язык в сво-
их разработках, можно назвать Google (часть компилятора Android
DataBindings), Expedia (мобильное приложение), Square (SQLDelight
compiler), Prezi (использование на сервере).
В 2016 году около 40 тыс. программистов использовали Kotlin, а ко-
личество кода на нем в открытых репозиториях GitHub удваивается
каждые несколько месяцев и уже перевалило за 2 млн строк. Ну и в са-
мой JetBrains, конечно, интенсивно используют Kotlin как для новых
продуктов, так и для развития старых, например, IntelliJ IDEA.
Kotlin — это очень простой язык, призванный решать серьезные
задачи.

ПРИМЕНЕНИЕ KOTLIN

Kotlin для Server-side

Kotlin отлично подходит для разработки приложений на стороне


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

• Выразительность: инновационные языковые функции Kotlin, та-


кие как поддержка type-safe builders и делегированных свойств, по-
могают создавать мощные и простые в использовании абстракции.
• Масштабируемость: поддержка Kotlin для coroutines дает воз-
можность создавать серверные приложения, которые масштаби-
руются до огромного количества клиентов со скромными требо-
ваниями к оборудованию.

12
Глава 1. История и развитие языка

• Взаимодействие: Kotlin полностью совместим со всеми осно-


ванными на Java фреймворками, что позволяет вам оставаться
в привычном технологическом стеке, наслаждаясь преимуще-
ствами более современного языка.
• Миграция: Котлин поддерживает постепенную, пошаговую ми-
грацию больших кодовых баз с Java на Kotlin. Вы можете начать
писать новый код в Kotlin, сохраняя старые части вашей систе-
мы на Java.
• Инструментарий: в дополнение к большой поддержке IDE в це-
лом Kotlin предлагает инструментарий, специфичный для кон-
кретной платформы (например, для Spring) в плагине для IntelliJ
IDEA Ultimate.

Приложения Kotlin могут быть развернуты на любом хосте, который


поддерживает Java Web-приложения, включая Amazon Web Services,
Google Cloud Platform и другие.

Kotlin для Android

Kotlin отлично подходит для разработки приложений для Android,


привнося все преимущества современного языка в платформу Android
без введения каких-либо новых ограничений.

• Совместимость: Kotlin полностью совместим с JDK 6. Это гаран-


тирует, что приложения Kotlin могут работать на старых устрой-
ствах Android без проблем. Инструментарий Kotlin полностью
поддерживается в Android Studio и совместим с системой сбор-
ки Android.
• Производительность: приложение Kotlin работает так же бы-
стро, как эквивалент Java, благодаря очень похожей структу-
ре байт-кода. Благодаря поддержке встроенных функций Kotlin
код с использованием лямбд часто работает даже быстрее, чем
тот же код, написанный на Java.
• Совместимость: Kotlin на 100% совместим с Java, что позволяет
использовать все существующие библиотеки Android в приложе-
нии Kotlin. Это включает обработку аннотаций, поэтому привяз-
ка данных и Dagger тоже работают.
• Footprint: у Kotlin очень компактная библиотека времени ис-
полнения, которая может быть дополнительно уменьшена
за счет использования ProGuard. В реальном приложении среда

13
Язык программирования Kotlin

исполнения Kotlin добавляет всего несколько сотен методов и ме-


нее 100 Кбайт к размеру файла .apk.
• Время компиляции: Kotlin поддерживает эффективную инкре-
ментную компиляцию. Поэтому, хотя для чистых сборок есть до-
полнительные накладные расходы, инкрементные сборки обыч-
но бывают быстрыми и быстрее, чем с Java.

Kotlin для JavaScript

Kotlin поддерживает трансляцию кода в JavaScript. Текущая реализа-


ция нацелена на ECMAScript 5.1, но есть планы в конечном итоге также
нацелиться на ECMAScript 2015.
Когда вы выбираете целью компиляции JavaScript, любой код Kotlin,
который является частью проекта, а также стандартная библиотека, по-
ставляемая вместе с Kotlin, компилируется в JavaScript. Однако это ис-
ключает JDK и любую используемую JVM или Java-инфраструктуру или
библиотеку. Любой файл, который не является Kotlin, будет игнориро-
ваться во время компиляции.
Компилятор Kotlin старается выполнить следующие задачи:

• Обеспечить оптимальный размер получаемого кода JavaScript


• Обеспечить генерацию читабельного кода JavaScript
• Обеспечить взаимодействие с существующими модульными
системами
• Обеспечить такую же функциональность в стандартной библи-
отеке, будь то таргетинг JavaScript или JVM (в максимально воз-
можной степени)

Kotlin может использоваться совместно с существующими сторон-


ними библиотеками и фреймворками, такими как JQuery или ReactJS.
Чтобы получить доступ к сторонним инфраструктурам с помощью
строго типизированного API, вы можете конвертировать определения
TypeScript из репозитория определений типизированного типа в Kotlin
с помощью инструмента ts2kt. Кроме того, вы можете использовать
динамический тип для доступа к любой инфраструктуре без строгой
типизации.
Kotlin также совместим с CommonJS, AMD и UMD, что делает взаи-
модействие с различными модульными системами простым.

14
Глава 2. Краткий обзор Kotlin

ГЛАВА 2.
КРАТКИЙ ОБЗОР KOTLIN

ООП
Как и в остальных языках программирования, элементы Kotlin суще-
ствуют не сами по себе. Они, скорее, действуют совместно, образуя язык
в целом. Иногда такая взаимосвязанность может затруднять описание
какого-то одного аспекта, не затрагивая ряда других. Зачастую для по-
нимания одного языкового средства необходимо знать другое. В этой
главе представлен краткий обзор языка Kotlin. Приведенный матери-
ал послужит отправной точкой в понимании этого языка. Большинство
рассмотренных здесь вопросов будут подробнее обсуждаться в осталь-
ных главах этой книги.

Объектно-ориентированное программирование

Объектно-ориентированное программирование (ООП) является


основой Kotlin. По сути, все программы на Kotlin являются в какой-
то степени объектно-ориентированными. Язык Kotlin очень тесно
связан с ООП, поэтому необходимо рассмотреть основные принци-
пы ООП.
Все компьютерные программы состоят из двух элементов: кода и дан-
ных. Программа может быть концептуально организована либо вокруг
своего кода, либо вокруг данных. Иными словами, организация одних
программ определяется тем, что происходит, а других — тем, на что
оказывается влияние. Существуют две методики создания программ.

15
Язык программирования Kotlin

Первая из них называется моделью, ориентированной на процессы,


и характеризует программу как последовательность линейных ша-
гов — кода. Модель, ориентированную на процессы, можно рассматри-
вать в качестве кода, воздействующего на данные. Такая модель успеш-
но применяется в процедурных языках вроде С. Но подобный подход
порождает ряд трудностей в связи с увеличением размеров и сложно-
сти программ.
С целью преодолеть увеличение сложности программ был разра-
ботан подход, называемый объектно-ориентированным программи-
рованием. ООП позволяет организовать программу вокруг ее данных
(т.е. объектов) и набора вполне определенных интерфейсов с этими дан-
ными. ООП-программу можно охарактеризовать как данные, управля-
ющие доступом к коду. Передавая функции управления данными, мож-
но получить несколько организационных преимуществ.

Абстракция

Важным элементом ООП является абстракция. Человеку свойствен-


но представлять сложные элементы в виде абстракций. Например,
люди представляют себе автомобиль не в виде набора десятков тысяч
отдельных деталей, а в виде совершенно определенного объекта, имею-
щего свое собственное особое поведение. Такая абстракция позволяет
не задумываться о сложности деталей, составляющих автомобиль, на-
пример, при поездке на дачу. Можно не обращать внимания на подроб-
ности работы двигателя, коробки передач, тормозной системы. Вместо
этого объект можно использовать как единое целое.
Эффективным средством применения абстракции служат иерархи-
ческие классификации. Это позволяет упрощать описание и поведение
систем, разбивая их на более простые части. Например, автомобиль
внешне выглядит как единое целое, но стоит заглянуть внутрь, и сра-
зу становится ясно, что он состоит из множества узлов, поведение ко-
торых можно описать отдельно.
Благодаря абстракции данные традиционной, ориентированной
на процессы, программы можно преобразовать в составляющие ее
объекты, а последовательность этапов выполнения — в совокупность
сообщений, передаваемых между этими объектами. Таким образом,
каждый из этих объектов описывает свое особое поведение, и эти
объекты можно считать конкретными отдельными сущностями, реа-
гирующими на сообщения, предписывающие им выполнение конкрет-
ного действия.

16
Глава 2. Краткий обзор Kotlin

ООП стоит на трех принципах: инкапсуляция, наследование и поли-


морфизм. Эти принципы лежат в основе языка Kotlin. Рассмотрим их
подробнее.

Инкапсуляция

Механизм, связывающий код и данные, которыми он манипулиру-


ет, защищая оба эти компонента от внешнего воздействия, называет-
ся инкапсуляцией. Инкапсуляцию можно считать защитной оболоч-
кой, которая предохраняет код и данные от произвольного доступа
со стороны другого кода, находящегося снаружи оболочки. Доступ
к коду и данным, находящимся внутри оболочки, строго контролирует-
ся определенным интерфейсом. Сильная сторона инкапсулированного
кода состоит в следующем: всем известно, как получить доступ к нему,
а следовательно, его можно использовать независимо от подробностей
реализации и не опасаясь неожиданных побочных эффектов.
Основу инкапсуляции в Kotlin составляет класс. Класс определяет
структуру и поведение, которые будут использоваться набором объек-
тов. Каждый объект (экземпляр) данного класса содержит структуру
и поведение, которые определены классом. Таким образом класс — это
логическая конструкция, а объект (экземпляр) — физическое воплоще-
ние класса.
При создании класса определяются код и данные, которые образуют
этот класс. Совместно эти элементы называются членами класса. При
этом определенные в классе данные называются свойствами, а код, опе-
рирующий данными — методами.
Поскольку назначение класса состоит в инкапсуляции сложной
структуры программы, существуют механизмы сокрытия структуры
реализации в самом классе. Каждый метод или свойство в классе мо-
гут быть помечены как закрытые или открытые. Открытый интерфейс
класса представляет все, что должны или могут знать внешние пользо-
ватели класса. Закрытые методы и свойства могут быть доступны толь-
ко для кода, реализованного внутри класса.

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

Процесс, в результате которого один объект получает свойства дру-


гого объекта, называется наследованием. Наследование обеспечива-
ет принцип иерархической классификации. Например, золотистый

17
Язык программирования Kotlin

ретривер — часть классификации собак, которая, в свою очередь, от-


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

Полиморфизм

Полиморфизм — это принцип ООП, позволяющий использовать


один и тот же интерфейс для общего класса действий. Каждое действие
зависит от конкретной ситуации. Рассмотрим в качестве примера стек,
действующий как список обратного магазинного типа. Допустим, в про-
грамме требуются стеки трех типов: для целочисленных значений, зна-
чений с плавающей точкой и символьных. Алгоритм остается неизмен-
ным, несмотря на отличия в данных, которые в них хранятся. В языке,
не являющемся объектно-ориентированным, пришлось бы создавать
три разных ряда служебных программ под разными именами. В Kotlin
благодаря принципу полиморфизма можно определить общий ряд слу-
жебных программ под одними и теми же общими именами.
В общем смысле принцип полиморфизма можно выразить фразой:
один интерфейс, несколько методов. Это означает, что можно разра-
ботать общий интерфейс для группы связанных вместе действий. Та-
кой подход позволяет уменьшить сложность программы, поскольку
один и тот же интерфейс служит для указания общего класса действий.
А выбор конкретного действия делается исходя из ситуации.
Например, можно считать нюх собаки полиморфным свойством.
Если собака почует кошку, она скорее всего погонется за ней, а если
ощутит запах своего корма, то у нее начнется слюноотделение и она по-
бежит к своей миске. В обоих случаях работает одно и то же чувство.
Отличия лишь в том, что именно издает запах, т.е. в типе данных, воз-
действующих на нос собаки.

18
Глава 2. Краткий обзор Kotlin

Итог

Если принципы полиморфизма, инкапсуляции и наследования при-


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

ПРОГРАММА «ПРИВЕТ, МИР!»


Описание любого языка принято начинать с примера программы, пе-
чатающей «Привет, мир!». Не будем делать исключение в этом плане
по отношению к Kotlin:

/**
* Это простая программа на Kotlin
*/
// Любая программа на Kotlin стартует
с функции main
fun main(args: Array<String>) {
println(“Привет, мир!”)
}

Хотя программа выше по тексту небольшая и простая, с ней связа-


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

/**
* Это простая программа на Kotlin
*/

19
Язык программирования Kotlin

Эти строки кода содержат многострочный комментарий. Подобно


большинству других языков программирования, Kotlin позволяет встав-
лять примечания к коду программы в ее исходный файл. Такие приме-
чания называются комментарии. Компилятор игнорирует содержимое
комментариев. Как правило, комментарии описывают или поясняют
действия программы. Особенно это важно для тех, кто просматривает
исходный код вашей программы. В данном случае комментарий описы-
вает программу в целом. В реальных прикладных программах коммен-
тарии служат главным образом для пояснения работы отдельных ча-
стей программы или действий, выполняемых отдельными языковыми
средствами и инструментами.
Далее идет однострочный комментарий, который сообщает нам, что
любая программа начинает свое выполнение с функции main. Это вто-
рой вид комментариев, поддерживаемых в Kotlin.

// Любая программа на Kotlin стартует


с функции main

Следующая строка является определением функции main. Как сле-


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

fun main(args: Array<String>) {

Для передачи любой информации, требующейся функции служат


переменные, указанные в скобках вслед за именем функции. Эти пе-
ременные называются параметрами. У функции main имеется един-
ственный, хотя и сложный параметр. В отличие от Java в Kotlin пара-
метр функции main является обязательным. Так, в выражении args:
Array<String> объявляется параметр args, обозначающий массив
строк (массивы — это коллекции похожих объектов). В данном случае
параметр args принимает любые аргументы командной строки, опре-
деленные во время запуска программы.
Последним элементом в рассматриваемой здесь строке кода оказы-
вается символ открывающей фигурной скобки {. Он обозначает нача-
ло тела функции main. Весь код, составляющий тело функции, должен
располагаться между открывающей и закрывающей фигурными скоб-
ками в определении этой функции.
Еще один важный момент: функция main служит всего лишь на-
чалом программы. Сложная программа может включать в себя десят-
ки функций и состоять из множества файлов. Но в некоторых случаях

20
Глава 2. Краткий обзор Kotlin

функция main не требуется, например при разработке под Android, по-


тому что тут используется другой механизм запуска на выполнение.
Перейдем к следующей строке. Эта строка находится внутри тела
функции main.

println(«Привет, мир!»)

В этой строке кода на экран выводится текстовая строка “Привет,


мир”, с последующим переходом на новую строку. Вывод текста осу-
ществляется стандартной функцией println().
Стоит обратить внимание, что в отличие от Java, например, оператор
НЕ заканчивается точкой с запятой. В языке Kotlin использование точ-
ки с запятой не обязательно. Есть несколько мест, где ее целесообразно
использовать, например: если вы в одной строке указываете несколько
операторов или, в обязательном порядке, при разделении определения
перечисляемых констант и методов в перечислении.

УСТАНОВКА КОМПИЛЯТОРА

Ручная установка

Для ручной установки вам необходимо скачать последнюю вер-


сию компилятора по адресу: https://github.com/JetBrains/
kotlin/releases/latest
Разархивируйте автономный компилятор в каталог и добавьте ката-
лог bin в системный путь. Каталог bin содержит скрипты, необходимые
для компиляции и запуска Kotlin в Windows, OS X и Linux.

Установка с помощью SDKMAN!

Более простой способ установки Kotlin в системах на базе UNIX,


таких как OS X, Linux, Cygwin, FreeBSD и Solaris — это использова-
ние SDKMAN !. Просто запустите следующее в терминале и следуйте
инструкциям:
Установить, если еще не установлен SDKMAN!

$ curl -s https://get.sdkman.io | bash

21
Язык программирования Kotlin

Затем откройте новый терминал и установите Kotlin с помощью сле-


дующей команды:

$ sdk install kotlin

Установка с помощью Homebrew

В качестве альтернативы в OS X вы можете установить компилятор


через Homebrew:

$ brew update
$ brew install kotlin

Установка с помощью MacPorts

Если вы являетесь пользователем MacPorts, вы можете установить


компилятор с помощью следующей команды:

$ sudo port install kotlin

КОМПИЛЯЦИЯ ПРОГРАММЫ

Создание, компиляция и запуск


первого приложения

Шаг 1. Откройте ваш любимый текстовый редактор, создайте файл


с именем hello.kt и напишите в нем следующий код:

fun main(args: Array<String>) {


println(“Hello, World!”)
}

Шаг 2. Скомпилируйте приложение с использованием компилятора


Kotlin. Для этого выполните в командной строке следующую команду:

22
Глава 2. Краткий обзор Kotlin

kotlinc hello.kt -include-runtime -d hello.


jar

Описание всех параметров компилятора можно получить с помо-


щью команды:

kotlinc -help

Шаг 3. Запустите приложение на выполнение с помощью команды:

java -jar hello.jar

Компиляция библиотеки

Если вы разрабатываете библиотеку, которая будет использоваться


другими приложениями Kotlin, вы можете создать файл .jar, не вклю-
чив в него runtime Kotlin. Это можно сделать следующей командой:

kotlinc hello.kt -d hello.jar

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


висят от runtime Kotlin, вы должны убедиться, что последний присут-
ствует в пути к классам всякий раз, когда используется ваша скомпили-
рованная библиотека.
Вы также можете использовать скрипт kotlin для запуска исполняе-
мых файлов, созданных компилятором Kotlin:

kotlin -classpath hello.jar HelloKt

В данном случае HelloKt — это основное имя класса, которое компи-


лятор Kotlin генерирует для файла с именем hello.kt.

Интерактивная оболочка Kotlin

Чтобы запустить интерактивную оболочку Kotlin, достаточно вы-


полнить kotlinc без параметров:

kotlinc

23
Язык программирования Kotlin

Использование командной строки


для запуска скриптов

Kotlin может использоваться в качестве скриптового языка. Напри-


мер, у нас есть скрипт:

import java.io.File
val folders = File(args[0]).listFiles { file
-> file.isDirectory() }
folders?.forEach { folder -> println(folder)
}

Чтобы запустить скрипт, мы просто передаем параметр -script


компилятору с соответствующим файлом сценария. Мы можем выпол-
нить его командой:

kotlinc -script list_folders.kts


<path_to_folder_to_inspect>

ЛЕКСИКА
Исходный текст программы на языке Kotlin состоит из совокупности
пробелов, идентификаторов, литералов, комментариев, операторов,
разделителей и ключевых слов. Ниже кратко описывается основной
синтаксис языка Kotlin.
Пробелы. Kotlin — язык свободной формы. Это означает, что при
написании программы не нужно следовать каким-либо специальным
правилам в отношении отступов. Программу «Привет, мир!» можно
написать и в одну строку, и любым другим способом. Единственное

24
Глава 2. Краткий обзор Kotlin

обязательное требование — наличие по меньшей мере одного пробела


между всем лексемами, которые еще не разграничены оператором или
разделителем. В языке Kotlin пробелами считаются символы пробела,
табуляции и новой строки.
Идентификаторы. Для именования классов, методов и переменных
служат идентификаторы. Идентификатором может быть любая после-
довательность строчных и прописных букв, цифр или символов под-
черкивания. Идентификаторы не должны начинаться с цифры, чтобы
компилятор не путал их с числовыми константами. В Kotlin
учитывается регистр символов, и поэтому VALUE и Value считаются
разными идентификаторами.
Литералы. В Kotlin постоянное значение задается литеральным
представлением (литералом). Ниже представлены несколько литералов:

100
98.7
‘X’
“Это строковый литерал”

Первый литерал в этом примере обозначает целочисленное значе-


ние, следующий — числовое с плавающей точкой, третий — символь-
ную константу и четвертый — строковое значение. Литерал можно ис-
пользовать везде, где допустимо применение значений данного типа.
Разделители. В Kotlin допускается применение нескольких символов
в качестве разделителей:

• ( ) круглые скобки — используются для передачи списков пара-


метров в определениях и вызовах методов (функций), для изме-
нения приоритета в выражениях
• { } фигурные скобки — используются для определения блоков
кода, классов, методов, локальных областей действия и лямбд
• [ ] квадратные скобки — используются для обращения к элемен-
там массива или строки
• ; точка с запятой, для нескольких операторов, определенных
в одной строке
• , запятая — используется для разделения параметров в определе-
ниях методов (функций)
• . точка — используется для отделения имен пакетов от подпаке-
тов и классов, а также для определения переменной или метода
от ссылочной переменной

25
Язык программирования Kotlin

БАЗОВЫЙ СИНТАКСИС
Цель данного раздела — ознакомить вас с базовым синтаксисом языка
Kotlin. В дальнейшем все аспекты языка будут рассмотрены подробнее.

Определение имени пакета

Имя пакета указывается в начале исходного файла

package my.demo
import java.util.*
// ...

В отличие от Java совпадения структуры каталогов и пакетов не тре-


буется: исходные файлы могут быть размещены произвольно в файло-
вой системе.

Определение функции

Функция начинается с ключевого слова fun, далее следует имя, за-


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

Ниже приведены варианты


определения функции

Функция принимает два аргумента Int и возвращает Int:

fun sum(a: Int, b: Int): Int {


return a + b
}

Функция с выражением в качестве тела и автоматически выведен-


ным типом возвращаемого значения:

26
Глава 2. Краткий обзор Kotlin

fun sum(a: Int, b: Int) = a + b

Функция, не возвращающая никакого значения (тип Unit аналоги-


чен void в Java):

fun printSum(a: Int, b: Int): Unit {


print(a + b)
}

Тип возвращаемого значения Unit может быть опущен:

fun printSum(a: Int, b: Int) {


print(a + b)
}

Определение вну тренних переменных

Переменная — это именованная ячейка памяти, которой может быть


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

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

Изменяемая переменная:

var x = 5 // Тип `Int` выведен


автоматически
x += 1

27
Язык программирования Kotlin

Комментарии

Комментарии служат для пояснения работы отдельных частей про-


граммы или действий, выполняемых отдельными языковыми средства-
ми. Kotlin поддерживает однострочные и блочные комментарии. Блоч-
ные комментарии могут быть вложенными.

// однострочный комментарий
/* Блочный комментарий
из нескольких строк. */

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

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


под названием строковые шаблоны. Это значит, что вы можете исполь-
зовать переменные внутри строк с указанием их в определенном фор-
мате $name и ${name}.

var a = 1
// простое имя переменной в шаблоне:
val s1 = «a is $a»
a = 2
// Произвольное выражение в шаблоне:
val s2 = “${s1.replace(“is”, “was”)}, but
now is $a”

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

В Kotlin языковая конструкция if является выражением, т.е. вы-


ражением, которое всегда возвращает значение. Более подробно кон-
струкция if рассматривается в главе 5 – «Управляющие операторы».

fun maxOf(a: Int, b: Int): Int {


if (a > b) {
return a
} else {
return b
}
}

28
Глава 2. Краткий обзор Kotlin

Конструкция if может быть записана как выражение в правой


части.

fun maxOf(a: Int, b: Int) = if (a > b) a


else b

Nullable-значения и проверка на null

Kotlin безопасный язык, если вы используете его правильно. Напри-


мер, ссылка должна быть явно объявлена как nullable (символ ?)
когда она может принимать значение null.
Возвращает null, если str не содержит числа:

fun parseInt(str: String): Int? {


// ...
}

Использование функции, возвращающей null:

fun main(args: Array<String>) {


if (args.size < 2) {
print(«Ожидается два целых числа»)
return
}
val x = parseInt(args[0])
val y = parseInt(args[1])
// Использование `x * y` приведет
к ошибке, потому что они могут содержать
null
if (x != null && y != null) {
// x и y автоматически приведены к не-
nullable после проверки на null
print(x * y)
}
}

Или

// ...
if (x == null) {

29
Язык программирования Kotlin

print(“Неверный формат числа


у ‘${args[0]}’”)
return
}
if (y == null) {
print(«Неверный формат числа
у ‘${args[1]}’»)
return
}
// x и y автоматически приведены к не-
nullable после проверки на null
print(x * y)

Подробнее NULL-безопасность будет рассмотрена в главе 12 — Дру-


гие особенности языка.

Проверка типов и автоматическое


приведение типов

Ребята из Jetbrains, которые разрабатывали язык Kotlin, учли неудоб-


ство языка Java в части проверки типов и их автоматического приведе-
ния и значительно улучшили этот механизм. Оператор is проверяет,
является ли выражение экземпляром заданного типа. Если неизменя-
емая внутренняя переменная или свойство уже проверены на опреде-
ленный тип, то в дальнейшем нет необходимости явно приводить к это-
му типу:

fun getStringLength(obj: Any): Int? {


if (obj is String) {
// в этом блоке obj автоматически
преобразован в `String`
return obj.length
}
// obj имеет тип `Any` вне блока проверки
типа
return null
}

Или

30
Глава 2. Краткий обзор Kotlin

fun getStringLength(obj: Any): Int? {


if (obj !is String)
return null
// в этом блоке obj автоматически
преобразован в String
return obj.length
}

Или даже

fun getStringLength(obj: Any): Int? {


// obj автоматически преобразован
в String справа от оператора &&
if (obj is String && obj.length > 0)
return obj.length
return null
}

Цикл for

Цикл for в языке Kotlin несколько отличается от других языков, по-


тому что имеет одну форму:

for (item in [items or range]) {


//..
}
val items = listOf(“apple”, “banana”,
“kiwi”)
for (item in items) {
println(item)
}

Или

for (index in 1..10) {


println(”Index: ” + index)
}

Более подробно цикл for рассматривается в главе 5 – «Управляю-


щие операторы».

31
Язык программирования Kotlin

Цикл while и do-while

val items = listOf(“apple”, “banana”,


“kiwi”)
var index = 0
while (index < items.size) {
println(“item at $index is
${items[index]}”)
index++
}
do {
println(“item at $index is
${items[index]}”)
index++
} while (index < items.size)

Более подробно циклы while и do-while рассматриваются


в главе 5 – «Управляющие операторы».

Выражение when

В языке Kotlin нет оператора switch. Вместо него разработчики


языка ввели выражение when. Выражение when действует подобно
оператору switch, но имеет ряд существенных отличий, которые под-
робно рассматриваются в главе 5 – «Управляющие операторы».

fun describe(obj: Any): String =


when (obj) {
1 -> “One”
“Hello” -> “Greeting”
is Long -> “Long”
!is String -> “Not a string”
else -> “Unknown”
}

Интервалы

Интервалы в языке Kotlin имеют оператор в виде ..., который допол-


няется in и !in. Они применимы ко всем сравниваемым (comparable)

32
Глава 2. Краткий обзор Kotlin

типам, но для целочисленных примитивов есть оптимизированная ре-


ализация. Подробно интервалы рассматриваются в главе 12 – «Другие
особенности языка».
Проверка на вхождение числа в интервал с помощью оператора in:

if (x in 1..y-1)
print(“OK”)

Проверка значения на выход за пределы интервала:

if (x !in 0..array.lastIndex)
print(“Out”)

Итерация по интервалу:

for (x in 1..5)
print(x)

Или по арифметической прогрессии:

for (x in 1..10 step 2) {


print(x)
}
for (x in 9 downTo 0 step 3) {
print(x)
}

Коллекции

Коллекция — это возможность собрать объекты в некоторую груп-


пу/множество и работать с этой группой. Kotlin не имеет специальных
синтаксических конструкций для создания списков или множеств. Для
создания коллекций вы можете использовать функции из стандарт-
ной библиотеки Kotlin, такие как: listOf(), mutableListOf(),
setOf(), mutableSetOf(). Более подробно коллекции рассматри-
ваются в главе 11.
Итерация по коллекции:

for (name in names)


println(name)

33
Язык программирования Kotlin

Проверка, содержит ли коллекция данный объект, с помощью опе-


ратора in:

val items = setOf(“apple”, “banana”, “kiwi”)


when {
“orange” in items -> println(“juicy”)
“apple” in items -> println(“apple is fine
too”)
}

Использование лямбда-выражения для фильтрации и модификации


коллекции:

names
.filter { it.startsWith(«A») }
.sortedBy { it }
.map { it.toUpperCase() }
.forEach { print(it) }

Интерфейсы и классы

Объявление класса состоит из имени класса, заголовка класса (с ука-


занием его параметров типа, основного конструктора и т. д.) и тела
класса, окруженного фигурными скобками. И заголовок, и тело являют-
ся необязательными; если класс не имеет тела, фигурные скобки мож-
но опустить.

class Invoice {
//..
}

Вы можете объявлять свойства в интерфейсах. Свойство, объяв-


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

interface MyInterface {
val prop: Int // abstract
val propertyWithImplementation: String

34
Глава 2. Краткий обзор Kotlin

get() = “foo”
fun foo() {
print(prop)
}
}
class Child : MyInterface {
override val prop: Int = 29
}

Подробно классы и интерфейсы рассматриваются в главе 7 – «Клас-


сы и объекты».

КЛЮЧЕВЫЕ СЛОВА
В отличие от других языков программирования язык Kotlin поддержи-
вает концепцию «жестких» и «мягких» ключевых слов. Их различие со-
стоит в том, что «жесткие» ключевые слова не могут быть использова-
ны в качестве идентификаторов, в отличие «мягких».
В настоящее время в языке Kotlin определено 74 ключевых слова, ко-
торые вместе с синтаксисом операторов и разделителей образуют осно-
ву языка Kotlin.

Hard keywords (28)


package as typealias class

this super val var

fun for null true

false is in throw

return break continue object

if try else while

do when interface typeof

35
Язык программирования Kotlin

Soft keywords (46)


file field property receiver

param setparam delegate import

where by get set

constructor init abstract enum

open inner override private

public internal protected catch

out vararg reified dynamic

companion sealed finally final

lateinit data inline noinline

tailrec external annotation crossinline

operator infix const suspend

header impl

Заметка о жестких и мягких ключевых словах: некоторые слова


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

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

36
Глава 2. Краткий обзор Kotlin

более высокого уровня (подробнее модификаторы доступа рассматри-


ваются в главе 7).
Создать пакет очень просто. Достаточно включить оператор
package в первую строку кода исходного файла:

package foo.bar
// ...

Любые классы, функции, интерфейсы и свойства, объявлен-


ные в этом файле, будут принадлежать указанному пакету. Оператор
package определяет пространство имен, в котором хранятся клас-
сы, функции и т.д. Если оператор package не указан, то имена клас-
сов, функций и других элементов размещаются в пакете по умолчанию,
не имеющего имени. Если пакет по умолчанию вполне подходит для ко-
ротких примеров программ, то он совершенно не годится для реальных
больших приложений. Зачастую для прикладного кода придется опре-
делять не один пакет.
В файле может быть только один оператор package. Но при этом
количество спецификаций пакетов на приложение не ограничивается.
Этот оператор просто определяет пространство имен.
В Kotlin можно создавать иерархию пакетов. Для этой цели служит
оператор (.) точка. Объявление многоуровневого пакета имеет следую-
щую общую форму:

package пакет1[.пакет2[.пакет3]]

Иерархия пакета должна быть отражена в файловой системе той сре-


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

Импорт пакетов

Для того чтобы воспользоваться классами, интерфейсами и т.д.,


определенными в именованных пакетах, эти данные необходимо как-то
получить. Для того чтобы эти данные стали доступны, необходимо вы-
полнить импорт этих данных из пакета. В Kotlin для импорта данных из
пакета служит ключевое слово import.

37
Язык программирования Kotlin

Оператор import должен идти сразу за объявлением пакета, опера-


тором package (если таковой имеется) и перед любыми определения-
ми кода и данных. Оператор import имеет следующую общую форму:

import пакет1[.пакет2].(имя_объекта | *)

где пакет1 обозначает имя пакета верхнего уровня, пакет2 — имя


подчиненного пакета из внешнего пакета, отделяемое знаком (.) точ-
ка. Глубина вложенности ограничивается только возможностями фай-
ловой системы. И наконец, имя_объекта может быть задано явно или
с помощью знака (*) звездочка, который указывает компилятору на не-
обходимость импорта всего содержимого пакета. Например:
Мы можем импортировать одно имя

import foo.Bar
или доступное содержимое пространства имён
(пакет, класс, объект и т.д.)
import foo.* // всё в ‘foo’ становится
доступно без указания пакета

При совпадении имен мы можем разрешить коллизию, исполь-


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

import foo.Bar // Bar доступен


import bar.Bar as bBar // bBar заменяет имя
‘bar.Bar’

Ключевое слово import можно использовать не только с классами,


но и с другими объявлениями:
• функции и свойства верхнего уровня;
• функции и свойства, объявленные в объявлениях объектов;
• перечисления (enum constants)
После того как импорт выполнен, к объекту можно обращаться без
необходимости указания имени пакета, в котором объект объявлен.

Импорт по умолчанию

Для каждого файла Kotlin происходит неявное импортирование (им-


порт по умолчанию) следующих пакетов:

38
Глава 2. Краткий обзор Kotlin

kotlin.*
kotlin.annotation.*
kotlin.collections.*
kotlin.comparisons.* (since 1.1)
kotlin.io.*
kotlin.ranges.*
kotlin.sequences.*
kotlin.text.*

Дополнительные пакеты импортируются в зависимости от целевой


платформы:

JVM:
java.lang.*
kotlin.jvm.*
JS:
kotlin.js.*

Классы и функции стандартной библиотеки Kotlin хранятся в выше-


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

import java.util.*
class MyDate: Date() {
}

Его можно переписать следующим образом:

class MyDate: java.util.Date() {


}

39
Язык программирования Kotlin

При импортировании пакетов или объектов нужно учитывать, что


их доступность определяется соответствующими модификаторами
видимости.

40
Глава 3. Типы данных и переменные

ГЛАВА 3.
ТИПЫ ДАННЫХ
И ПЕРЕМЕННЫЕ

ТИПЫ ДАННЫХ
Kotlin — строго типизированный язык. Это значит, что каждая пере-
менная и каждое выражение имеет конкретный тип, и каждый тип
строго определен. Также все операции присваивания, как явные, так
и через параметры, передаваемые при вызове методов, проверяются
на соответствие типов. Компилятор проверяет все выражения и пара-
метры на соответствие типов. Любые несоответствия типов считаются
ошибками, которые должны быть исправлены до завершения процес-
са компиляции.
Kotlin реализует следующий набор типов:

Числа (Double, Float, Long, Int, Short,


Byte)
Символы (Char)
Строки (String)
Логический тип (Boolean)
Массивы (Array)

Эти типы можно использовать непосредственно или для создания


собственных типов классов.

41
Язык программирования Kotlin

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


Также в Kotlin тип может быть выведен автоматически, исходя из зна-
чения правой части.

ЧИСЛА
Kotlin обрабатывает численные типы примерно так же, как и Java, хотя
некоторые различия все же имеются. Например, отсутствует неявное
расширяющее преобразование для чисел, а литералы в некоторых слу-
чаях немного отличаются.
Для представления чисел в Kotlin используются следующие встроен-
ные типы:

Размер
Тип Диапазон значений
(кол-во бит)

Double 64 От 4.9e-324 до 1.8е+308


Float 32 От 1.4е-045 до 3.4е+038
Long 64 От -9223372036854775808
до 9223372036854775807
Int 32 От -2147483648 до 2147483647
Short 16 От -32768 до 32767
Byte 8 От -128 до 128

На заметку В языке Kotlin, в отличие от JavaЫ, символы


(characters) не являются числами.

Целые числа

Для целых чисел в Kotlin определены четыре типа: Byte, Short,


Int и Long. Все эти типы данных представляют целочисленные зна-
чения со знаком: как положительные, так и отрицательные.

42
Глава 3. Типы данных и переменные

Тип Byte

Наименьшим по длине является тип Byte. Это 8-разрядный тип


данных со знаком и диапазоном допустимых значений от -128 до 127.

val a: Byte
val b: Byte = 10

Тип Short

Тип Short представляет 16-разрядные целочисленные значения


со знаком в пределах от -32768 до 32767.

val a: Short
val b: Short = 255

Тип Int

Наиболее часто используемым целочисленным типом является Int.


Это тип 32-разрядных целочисленных значений со знаком в пределах
от -2147483648 до 2147483647.

val a: Int
val b: Int = 10000

Тип Long

Тип Long представляет целочисленные 64-разрядные значения


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

val a: Long
val b: Long = 16070400000000
/**
* Пример программы вычисления расстояния,
проходимого светом
*/

43
Язык программирования Kotlin

fun main(args: Array<String>) {


val lightSpeed: Long = 186000
val days: Int = 1000
val seconds: Int = days * 24 * 60 * 60
val distance: Long = lightSpeed * sec-
onds
println(“За $days дней свет пройдет
около $distance миль.”)
}

Эта программа выводит следующий результат:


За 1000 дней свет пройдет около 16070400000000
миль.

Числа с плавающей точкой

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


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

Тип Float

Этот тип определяет числовое значение с плавающей точкой оди-


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

val a: Float
val a: Float = 3.14

44
Глава 3. Типы данных и переменные

Тип Double

Для определения числовых значений с плавающей точкой двойной


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

val a: Double
/**
* Пример программы вычисления площади круга
*/
fun main(args: Array<String>) {
val pi: Double = 3.1416
val r: Double = 10.8
val a: Double = pi * r * r
println(“Площадь круга равна $a”)
}

Эта программа выводит следующий результат:


Площадь круга равна 366.436224

Числовые литералы

В языке Kotlin присутствуют следующие виды числовых литералов:


• Десятичные числа: 123
• Тип Long обозначается заглавной L: 123L
• Шестнадцатиричные числа: 0x0F
• Двоичные числа: 0b00001011

На заметку Восьмеричные литералы в языке Kotlin


не поддерживаются.

Подчеркивание в числовых литералах

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


числовые константы более читаемыми:

45
Язык программирования Kotlin

val oneMillion = 1_000_000


val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_100
10010

Представление (обертки)

Обычно платформа Java хранит числа в виде примитивных типов


JVM; если же нам необходима ссылка, которая может принимать значе-
ние null (например, Int?), то используются обертки. В приведенном
ниже примере показано использование оберток.
Обратите внимание, что использование оберток для одного и того же
числа не гарантирует равенства ссылок на них:

val a: Int = 1000


println(a === a) //Выведет true
val boxedA: Int? = a
val boxedB: Int? = a
println(boxedA === boxedB) //Выведет false

Однако равенство на значение сохраняется

println(boxedA == boxedB) //Выведет true

Явные преобразования

В Kotlin, из-за разницы в представлениях, меньшие типы не являют-


ся подтипами больших типов. Как следствие, неявное преобразование
меньших типов в большие НЕ происходит. Это значит, что мы не мо-
жем присвоить значение типа Byte переменной типа Int без явного
преобразования:

val b: Byte = 1 // порядок, литералы


проверяются статически
val i: Int = b // ОШИБКА

Но можно использовать явное преобразование для «сужения» чисел:

46
Глава 3. Типы данных и переменные

val i: Int = b.toInt() // Вот теперь


порядок, и все работает.

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

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

Отсутствие неявных преобразований редко заметно, потому что тип


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

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

В данном случае тип явно не указан, но он выводится из контекста


правой части.

Операции

Kotlin поддерживает обычный набор арифметических действий над


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

val x = (1 shl 2) and 0x000FF000

Ниже приведен полный список битовых операций (доступны только


для типов Int и Long):

shl(bits) – сдвиг влево с учетом знака (<<в


Java)
shr(bits) – сдвиг вправо с учетом знака (>>
в Java)
ushr(bits) – сдвиг вправо без учета знака

47
Язык программирования Kotlin

(>>> в Java)
and(bits) – побитовое И
or(bits) – побитовое ИЛИ
xor(bits) – побитовое исключающее ИЛИ
inv() – побитовое отрицание

Операции подробно будут рассмотрены в главе 4.

СИМВОЛЫ
Для хранения символов Kotlin использует тип Char. Обратите внима-
ние, что символы в Kotlin напрямую не могут рассматриваться в каче-
стве чисел.

fun check(c: Char) {


if (c == 1) { // ОШИБКА: несовместимый
тип
// ...
}
}

Символьные литералы записываются в одинарных кавычках: ‘1’.


Специальные символы экранируются обратным слэшем. В Kotlin под-
держиваются следующие последовательности: ‘\t’, ‘\b’, ‘\n’,
‘\r’, ‘\’’, ‘\»’, ‘\\’ and ‘\$’. Для декодирования других
различных символов используйте Unicode синтаксис: ‘\uFF00’.
В Kotlin мы можем явно преобразовать символ в число Int:

fun decimalDigitValue(c: Char): Int {


if (c !in ‘0’..’9’)
throw IllegalArgumentException(“Вне
диапазона»)
return c.toInt() - ‘0’.toInt() // Явные
преобразования в число
}

Подобно числам, символы оборачиваются при необходимости ис-


пользования nullable ссылки. При использовании оберток тожде-
ственность (равенство по ссылке) не сохраняется.

48
Глава 3. Типы данных и переменные

/**
* Программа демонстрирует применение типа
Char
*/
fun main(args: Array<String>) {
val char1: Int = 88
val char2: Char = ‘Y’
println(“Convert int 88 to char
${char1.toChar()}”)
println(“Convert char Y to int ${char2.
toInt()}”)
}

Эта программа выводит следующий результат:


Convert int 88 to char X
Convert char Y to int 89

СТРОКИ
Строки в Kotlin представлены типом String. Они являются неизме-
няемыми. Строки состоят из символов, которые могут быть получены
по порядковому номеру: s[i]. Проход по строке выполняется циклом
for:

for (c in str) {
println(c)
}

Строковые литералы

В Kotlin есть два типа строковых литералов: экранированные стро-


ки, которые могут содержать экранированные символы, в них и raw
строки, которые могут содержать символы новой строки и произволь-
ный текст. Экранированная строка очень похожа на строку Java:

val s = «Hello, world!\n»

49
Язык программирования Kotlin

Экранирование выполняется общепринятым способом, а именно


с помощью обратной косой черты.
Raw строка выделяется тройной кавычкой («»»), не содержит экра-
нированных символов, может содержать символы новой строки и лю-
бые другие символы:

val text = “””


for (c in “foo”)
print(c)
“””

При необходимости вы можете удалить ведущие пробелы при помо-


щи функции trimMargin():

val text = “””


|Tell me and I forget.
|Teach me and I remember.
|Involve me and I learn.
|(Benjamin Franklin)
“””.trimMargin()

По умолчанию | используется как margin prefix, но вы може-


те выбрать другой символ и передать его как параметр, например
trimMargin («>»).

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

Строки могут содержать шаблонные выражения, то есть фрагменты


кода, которые оцениваются и результаты которых объединены в стро-
ку. Выражение шаблона начинается со знака доллара ($) и состоит из
простого имени:

val i = 10
val s = “i = $i” // вычисляется как «i =
10»

Или произвольное выражение в фигурных скобках:

50
Глава 3. Типы данных и переменные

val s = “abc”
val str = “$s.length is ${s.length}” //
вычисляется как «abc.length is 3»

Шаблоны поддерживаются как в обычных, так и в экранированных


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

val price = “””


${‘$’}9.99
“””
val price = “${‘$’}9.99”

Конкатенация строк

Конкатенация строк в Kotlin выполняется при помощи оператора +.

/**
* Пример программы работы со строками.
Программа выводит символ и код символа
*/
fun main(args: Array<String>){
val text = “This is a string”
for(c in text) {
print(“$c[${c.toInt()}] “)
}
println()
Println(”The ”+ “end”)
}

МАССИВЫ
Массивы — это группа однотипных переменных, для обращения к ко-
торым используется общее имя. Доступ к конкретному элементу масси-
ва осуществляется по его индексу. Массивы представляют собой удоб-
ный способ группирования связанной вместе информации.

51
Язык программирования Kotlin

Массивы в Kotlin реализуются классом Array, имеющим функции


get и set, которые обозначаются [ ] согласно соглашению о перегруз-
ке операторов, и свойством size, а также несколькими полезными
встроенными функциями:

class Array<T> private constructor() {


val size: Int
operator fun get(index: Int): T
operator fun set(index: Int, value: T):
Unit
operator fun iterator(): Iterator<T>
// ...
}

Для создания массива мы можем использовать библиотечную функцию


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

// создает массив типа Array<String>


// со значениями [«0», «1», «4», «9», «16»]
val asc = Array(5, { i -> (i *
i).toString() })

Как отмечено выше, оператор [ ] используется вместо вызовов


встроенных функций get() и set().
Обратите внимание: в отличие от Java массивы в Kotlin являются не-
изменяемыми. Это значит, что Kotlin запрещает нам присваивать зна-
чение Array<String> массиву типа Array<Any>, предотвращая та-
ким образом возможный отказ во время исполнения (хотя вы можете
использовать Array<out Any> (модификаторы in и out будут рас-
смотрены в главе 7, в разделе «Обобщения»).
Также в Kotlin есть особые классы для представления масси-
вов примитивных типов без дополнительных затрат на оборачива-
ние: ByteArray, ShortArray, IntArray и т.д. Данные классы

52
Глава 3. Типы данных и переменные

не наследуют класс Array, хотя и обладают тем же набором методов


и свойств. У каждого из них есть соответствующая фабричная функция:

val x: IntArray = intArrayOf(1, 2, 3)


x[0] = x[1] + x[2]
/**
* Пример программы, которая создает массивы
и выводит их значения
*/
fun main(args: Array<String>){
val a = arrayOf(1, 2, 3, 4, 5)
val b = Array(5, { i -> (i + 65).
toChar()})

for (k in 0..a.size-1) {
print(“${a[k]} “)
}
println()
for (k in b) {
print(“$k “)
}
println()
}
/**
* Пример программы, которая создает 2D
и 3D массивы
*/
fun main(args: Array<String>){
val one: IntArray = intArrayOf(1, 2, 3)
val two: IntArray = intArrayOf(4, 5, 6)
val three: IntArray = intArrayOf(7, 8,
9)
val a2d: Array<IntArray> = arrayOf(one,
two)
val a3d: Array<Array<IntArray>> =
arrayOf(arrayOf(one, two, three))
println(“Print 2d array”)
for (i in a2d) {
for (j in i) {

53
Язык программирования Kotlin

print(j)
}
println()
}
println(“Print 3d array”)
for (i in a3d) {
for (j in i) {
for (k in j) {
print(k)
}
println()
}
println()
}
}

ЛОГИЧЕСКИЙ ТИП
Тип Boolean представляет логический тип данных и принимает два зна-
чения: true и false. При необходимости использования nullable
ссылок логические переменные оборачиваются.
Встроенные действия над логическими переменными включают:

||, or – ленивое логическое ИЛИ


&&, and – ленивое логическое И
! - отрицание
xor - исключающее ИЛИ

Например:

var status: Boolean


var check: Boolean
if (status && check) {
//..
}

54
Глава 3. Типы данных и переменные

ПРИВЕДЕНИЕ ТИПОВ
В языке Kotlin, в отличие от Java, нет автоматического преобразования
и продвижения типов. Для того что бы один числовой тип присвоить
другому, необходимо осуществить явное приведение типа.

fun main(args: Array<String>){


val a: Byte = 5
val b: Int
b = a.toInt()
println(“Переменная a приведена к типу
переменной b”)
println(“Переменная b теперь имеет
значение переменной а: $b”)
}

Операторы is и !is

Мы можем проверить, принадлежит ли объект к какому-либо типу,


во время исполнения с помощью оператора is или его отрицания !is:

if (obj is String) {
print(obj.length)
}
if (obj !is String) { // тоже самое что
и !(obj is String)
print(«Not a String»)
}
else {
print(obj.length)
}

Умные приведения

Во многих случаях в Kotlin вам не нужно использовать явные при-


ведения, потому что компилятор следит за is-проверками для неизме-
няемых значений и вставляет приведения автоматически там где они
нужны.

55
Язык программирования Kotlin

fun demo(x: Any) {


if (x is String) {
print(x.length) // x автоматически
преобразовывается в String
}
}

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


приведения в случаях, когда проверка на несоответствие типу (!is)
приводит к выходу из функции:

if (x !is String) return


print(x.length) // x автоматически
преобразовывается в String
или в случаях, когда приводимая переменная
находится справа от оператора && или ||:
// x автоматически преобразовывается
в String справа от `||`
if (x !is String || x.length == 0) return
// x is автоматически преобразовывается
в String справа от `&&`
if (x is String && x.length > 0) {
print(x.length) // x автоматически
преобразовывается в String
}

Такие умные приведения работают вместе с when-выражениями


и циклами while:

when (x) {
is Int -> print(x + 1)
is String -> print(x.length + 1)
is IntArray -> print(x.sum())
}

Стоит иметь в виду, что умные приведения не работают, когда ком-


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

Умные приведения будут работать:


• с локальными val переменными — всегда

56
Глава 3. Типы данных и переменные

• с val свойствами — если поле имеет модификатор доступа


private или internal либо проверка происходит в том же
модуле, в котором объявлено это свойство. Умные приведения
не применимы к публичным свойствам, или свойствам, которые
имеют переопределенные getter’ы
• с локальными var переменными — если переменная не изме-
няется между проверкой и использованием и не захватывается
лямбдой, которая ее модифицирует

Умные приведения НЕ будут работать:


• с var свойствами — потому что переменная может быть изме-
нена в любое время другим кодом

Оператор «небезопасного» приведения

Обычно оператор приведения выбрасывает исключение, если при-


ведение невозможно. Таким образом, мы называем его небезопасным.
Небезопасное приведение в Kotlin выполняется с помощью инфиксно-
го оператора as:

val x: String = y as String

Заметьте, что null не может быть приведен к String, так как


String не является nullable, т.е. если y — null, код выше выбро-
сит исключение. Чтобы соответствовать семантике приведений в Java,
нам нужно указать nullable тип в правой части приведения:

val x: String? = y as String?

Оператор “безопасного” (nullable) приведения

Чтобы избежать исключения, вы можете использовать оператор без-


опасного приведения as?, который возвращает null в случае неудачи:

val x: String? = y as? String

Заметьте, что несмотря на то что справа от as? стоит non-null


тип String, результат приведения является nullable.

57
Язык программирования Kotlin

ПСЕВДОНИМЫ ТИПОВ
Начиная с версии 1.1 в Kotlin появился такой полезный функционал,
как псевдонимы типов. Псевдонимы типов предоставляют альтернатив-
ные имена для существующих типов. Если имя типа слишком длинное,
вы можете ввести другое, более короткое имя, и использовать его вме-
сто первоначального.
Псевдонимы типов полезны, когда вы хотите сократить длинные
имена типов, содержащих обобщения. К примеру, можно упрощать на-
звания типов коллекций:

typealias NodeSet = Set<Network.Node>


typealias FileTable<K> = MutableMap<K,
MutableList<File>>
val nodeSet: NodeSet

Также вы можете создавать различные псевдонимы (алиасы) для


функциональных типов.

typealias MyHandler = (Int, String, Any) ->


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

Вы можете вводить новые имена (псевдонимы) для внутренних


и вложенных классов:

class A {
inner class Inner
}
class B {
inner class Inner
}
typealias AInner = A.Inner
typealias BInner = B.Inner

Псевдонимы типов не вводят новых типов. Они эквивалентны соот-


ветствующим базовым типам.

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


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

58
Глава 3. Типы данных и переменные

fun main(args: Array<String>) {


val f: (Int) -> Boolean = { it > 0 }
println(foo(f)) // выведет «true»
val p: Predicate<Int> = { it > 0 }
println(listOf(1, -2).filter(p)) //
выведет «[1]»
}

Когда вы добавляете typealias Predicate<T> и используете


Predicate<Int> в своем коде, компилятор Kotlin всегда преобразо-
вывает это в (Int) -> Boolean. Таким образом, вы можете пере-
дать переменную своего типа в любое место, где требуется базовый тип
(тот, которому был задан псевдоним), и наоборот.

59
Язык программирования Kotlin

ГЛАВА 4.
ОПЕРАЦИИ

ОПЕРАЦИИ В KOTLIN
В языке Kotlin поддерживается обширный ряд операций. Большинство
из них можно отнести к одной их следующих категорий: арифметиче-
ские, логические, отношения, нахождения и вхождения.

АРИФМЕТИЧЕСКИЕ ОПЕРАЦИИ
Арифметические операции применяются в математических выражени-
ях таким же образом, как и в алгебре. Ниже перечислены арифметиче-
ские операции, доступные в Kotlin:

Операция Описание

+ Сложение (а также унарный плюс)

- Вычитание (а также унарный минус)

* Умножение

60
Глава 4. Операции

Операция Описание

/ Деление

% Деление по модулю

++ Инкремент

+= Сложение с присваиванием

-= Вычитание с присваиванием

*= Умножение с присваиванием

/= Деление с присваиванием

%= Деление по модулю с присваиванием

-- Декремент

Операнды арифметических операций должны иметь числовой тип.


Арифметические операции нельзя выполнять над логическими типами
данных.

Основные арифметические операции

Все основные арифметические операции (сложения, вычитания,


умножения и деления) воздействуют на числовые типы так, как это-
го и следовало ожидать. Операция унарного вычитания изменяет знак
своего единственного операнда. Операция унарного сложения просто
возвращает значение своего операнда.
Следующий пример простой программы демонстрирует применение
арифметических операций в языке Kotlin.

/**
* Программа демонстрирует основные
арифметические операции
*/
fun main(args: Array<String>) {

61
Язык программирования Kotlin

val a = 1 +1
val b = a * 2
val c = b / 4
val d = c - a
val e = -d
val da: Double = 1.0 + 1.0
val db = da * 3
val dc = db / 4
val dd = dc - a
val de = -dd
println(“Целочисленная арифметика”)
println(«a = $a»)
println(«b = $b»)
println(«c = $c»)
println(«d = $d»)
println(“e = $e”)
println(“\nАрифметика с плавающей
точкой”)
println(«da = $da»)
println(«db = $db»)
println(«dc = $dc»)
println(«dd = $dd»)
println(“de = $de”)
}

Обратите внимание, что в программе не объявлен явно тип перемен-


ных. Это показывает краткость языка. Компилятор в Kotlin очень ум-
ный и умеет выводить тип из выражения.
Программа выдаст следующий результат:
Целочисленная арифметика
a = 2
b = 4
c = 1
d = -1
e = 1
Арифметика с плавающей точкой
da = 2.0
db = 6.0
dc = 1.5
dd = -0.5
de = 0.5

62
Глава 4. Операции

Операция деления по модулю

Операция деления по модулю % возвращает остаток от деления. Эту


операцию можно выполнить над всеми числовыми типами. Следующий
пример программы демонстрирует применение операции %:

/**
* Программа демонстрирует деление по модулю
*/
fun main(args: Array<String>){
val x = 42
val y = 42.25
println(“x mod 10 = ${x % 10}”)
println(“y mod 10 = ${y % 10}”)
}

Программа выведет следующий результат:


x mod 10 = 2
y mod 10 = 2.25

Составные арифметические операции

В Kotlin есть ряд специальных операций, объединяющих арифмети-


ческие операции с операцией присваивания.
Довольно часто нужно выполнять операции типа:

a = a + 5
b = b - 5
c = c / 5
d = d * 5
e = e % 5

Эти операции в Kotlin называются составные, и их можно записать


короче таким образом:

a += 5
b -= 5
c /= 5
d *= 5
e %= 5

63
Язык программирования Kotlin

Следующий пример простой программы демонстрирует работу со-


ставных операторов

/**
* Программа демонстрирует работу составных
операций
*/
fun main(args: Array<String>){
var a = 5
var b = 5
var c = 5
var d = 5
var e = 5
a += 5
b -= 5
c /= 5
d *= 5
e %= 5
println(“a += 5 будет $a»)
println(«b -= 5 будет $b»)
println(«c /= 5 будет $c»)
println(«d *= 5 будет $d»)
println(«e %= 5 будет $e»)
}

Программа выведет следующий результат:


a += 5 будет 10
b -= 5 будет 0
c /= 5 будет 1
d *= 5 будет 25
e %= 5 будет 0

Операции инкремента и декремента

Операции ++ и — выполняют инкремент и декремент. Эти операции


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

64
Глава 4. Операции

x = x + 1 можно записать x++


x = x - 1 можно записать x--

Операции ++ и -- могут быть записаны как в постфиксной форме,


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

• Для префиксной формы значение операнда увеличивает-


ся или уменьшается до извлечения значения для применения
в выражении
• В постфиксной форме предыдущее значение извлекается для
применения в выражении, а затем изменяется значение операнда.

В приведенной ниже программе демонстрируется применение опе-


раций инкремента и декремента в различных формах.

/**
* Программа демонстрирует применение
операций инкремента и декремента
*/
fun main(args: Array<String>){
var a = 1
var b: Int
println(“Значение а равно $a»)
a++
println(«Выполняем a++ теперь а равно
$a»)
a--
println(«Выполняем a-- теперь а равно
$a»)
b = a++
println(«Выполняем b = a++ теперь b
равно $b а равно $a»)
b = ++a
println(«Выполняем b = ++a теперь b
равно $b а равно $a»)
b = a--
println(«Выполняем b = a-- теперь b

65
Язык программирования Kotlin

равно $b а равно $a»)


b = --a
println(«Выполняем b = --a теперь b
равно $b а равно $a»)
}

Программа выведет следующий результат:


Значение а равно 1
Выполняем a++ теперь а равно 2
Выполняем a-- теперь а равно 1
Выполняем b = a++ теперь b равно 1 а равно 2
Выполняем b = ++a теперь b равно 3 а равно 3
Выполняем b = a-- теперь b равно 3 а равно 2
Выполняем b = --a теперь b равно 1 а равно 1

ОПЕРАЦИИ ОТНОШЕНИЯ
Операции отношения, или другое название — операции сравнения,
определяют отношение одного операнда к другому. В частности, они
определяют равенство и упорядочивание. Ниже в таблице перечислены
операции отношения, доступные в Kotlin.

Операция Описание

== Равно

!= Не равно

> Больше

< Меньше

>= Больше или равно

66
Глава 4. Операции

Операция Описание

<= Меньше или равно

=== Ссылки равны

!== Ссылки не равны

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


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

val a: Int = 1
val e: Long = 1
if (a == e) { // Error: Operator ‘==’ can-
not be applied to ‘Int’ and ‘Long’
//..
}

Если в подобной ситуации вам все-таки необходимо произвести


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

val a: Int = 1
val e: Long = 1
// Теперь оба операнда имеют одинаковый тип
// и их можно сравнивать.
if (a == е.toInt()) {
//..
}

Тем, у кого имеется опыт программирования на С/С++, следует об-


ратить внимание на то, что в программах на С/С++ часто можно встре-
тить следующую форму записи оператора if:

int done;
//...

67
Язык программирования Kotlin

if (done) ...
if (!done) ...

Такая форма записи в Kotlin запрещена. Вы должны написать условие


таким образом, чтобы оно явно возвращало тип boolean. Например

if (done == 0) ...
if (done != 0) ...

Следующий пример простой программы демонстрирует работу опе-


раций отношения

/**
* Программа демонстрирует использование
операций отношения
*/
fun main(args: Array<String>){
val a = 1
val b = 3
val c = 1L
val char1 = ‘1’
val char2 = ‘1’
val char3 = ‘2’
val char_equals_result: String
val number1 = Integer(10)
val number2 = Integer(10)
val number3 = number1
println(“Выражение a == b вернет ${a ==
b}»)
println(«Выражение a == c.toInt()
вернет ${a == c.toInt()}»)
println(«Выражение number1 == number2
вернет « + (number1 == number2))
println(«Выражение number1 === number2
вернет « + (number1 === number2))
println(«Выражение number1 === number3
вернет « + (number1 === number3))
println(«Выражение char1 == char2
вернет « + (char1 == char2))
if (char1 < char3) {
println(“char1 меньше char3»)

68
Глава 4. Операции

} else {
println(“char1 больше char3»)
}
char_equals_result = if (char1 ==
char3) “символы равны» else “символы
не равны»
println(“Результат сравнения
на равенство char1 и char3: $char_equals_re-
sult”)
}

Программа выдаст следующий результат:


Выражение a == b вернет false
Выражение a == c.toInt() вернет true
Выражение number1 == number2 вернет true
Выражение number1 === number2 вернет false
Выражение number1 === number3 вернет true
Выражение char1 == char2 вернет true
char1 меньше char3
Результат сравнения на равенство char1 и char3:
символы не равны

РАВЕНСТВО
В языке Kotlin есть два типа равенства:

• Равенство ссылок (две ссылки указывают на один и тот же


объект)
• Равенство структур (проверка через equals())

Равенство ссылок

Равенство ссылок проверяется с помощью оператора === (и его от-


рицания !==). Выражение a === b является истиной тогда и только
тогда, когда a и b указывают на один и тот же объект.

69
Язык программирования Kotlin

Равенство структур

Структурное равенство проверяется оператором == (и его отрица-


нием !=). Условно выражение a == b транслируется в:

a?.equals(b) ?: (b === null)

Т.е. если a не null, вызывается функция equals(Any?), иначе


(т.е. если a указывает на null) b ссылочно сравнивается с null. За-
метьте, что в явном сравнении с null для оптимизации нет смысла: a
== null будет автоматически транслироваться в a === null.

Сравнение на равенство массивов

Для массивов начиная с Kotlin 1.1 мы можем проверить структур-


ное равенство, используя функции infix функции contentEquals()
и contentDeepEquals():

• contentEquals()— возвращает true, если два указанных


массива структурно равны друг другу, т.е. содержат одинаковое
количество одинаковых элементов в одном порядке
• contentDeepEquals() — возвращает true, если два указанных
массива равны друг другу, включая вложенные массивы, т.е. со-
держат одинаковое количество одинаковых элементов в одном
порядке

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


вышесказанное:

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


Int)
fun main(args: Array<String>){
val a = Integer(10)
val b = Integer(10)
val user1 = User(“Вася», 22)
val user2 = User(“Вася», 22)
val array1 = arrayOf(“Hiking, Chess”)
val array2 = arrayOf(“Hiking, Chess”)
println (“a == b is ${a == b}”)
println (“a === b is ${a === b}”)

70
Глава 4. Операции

println (“user1 == user2 is ${user1 ==


user2}”)
println (“user1 === user2 is ${user1
=== user2}”)
println (“array1 contentEquals array2
${array1 contentEquals array2}”)
}

Программа выводит следующий результат:


a == b is true
a === b is false
user1 == user2 is true
user1 === user2 is false
array1 contentEquals array2 true
array1 contentDeepEquals array2 true

ЛОГИЧЕСКИЕ ОПЕРАЦИИ
Описываемые в этом разделе логические операции могут использовать-
ся только с операндами типа boolean. Ниже в таблице перечислены
все доступные в языке Kotlin логические операции.
Операция Описание

&&, and Логическая операция И

||, or Логическая операция ИЛИ

! Логическая операция НЕ

xor Логическая исключающая операция ИЛИ

Поразрядные логические операции в языке Kotlin реализованы


в виде именованных функций и описаны ниже.
Результат операции ИЛИ равен true, когда один из операндов ра-
вен true. Результат операции И равен true, когда все операнды рав-
ны true. Логическая операция НЕ инвертирует логическое состояние:
!true == false и !false == true.
Программа ниже демонстрирует применение логических операций.

71
Язык программирования Kotlin

/**
* Программа демонстрирует использование
логических операций
*/
fun main(args: Array<String>){
val a = 1
val b = 2
val c = 0
val d = false
val d = true
val e = true
val f = false
println(“Логические операции”)
println(“Выражение a > 0 && b > 0
вернет ${a > 0 && b > 0}”)
println(“Выражение a > 0 && c > 0
вернет ${a > 0 && c > 0}”)
println(“Выражение a > 0 || c == 0
вернет ${a > 0 && c == 0}”)
println(“Выражение !d вернет ${!d}”)
println(“Поразрядные логические
операции”)
println(“Выражение e and f вернет ${e
and f}”)
println(«Выражение d or e вернет ${d or
f}»)
println(«Выражение d xor e вернет ${d
xor e}»)
}

Программа выдаст следующий результат:


Логические операции
Выражение a > 0 && b > 0 вернет true
Выражение a > 0 && c > 0 вернет false
Выражение a > 0 || c == 0 вернет true
Выражение !d вернет false
Поразрядные логические операции
Выражение d and e вернет false
Выражение d or e вернет true
Выражение d xor e вернет false

72
Глава 4. Операции

ПОРАЗРЯДНЫЕ ОПЕРАЦИИ
В языке Kotlin определены несколько поразрядных операций. Эти опе-
рации воздействуют на отдельные двоичные разряды операндов. По-
разрядные операции выполнимы для типов Boolean, Long и Int.
В языке Kotlin, в отличие от других языков, для поразрядных бито-
вых операций вместо особых обозначений используются именованные
функции, которые могут быть вызваны в инфиксной форме. Например:

val x = (1 shl 2) and 0x000FF000

Ниже приведен полный список битовых операций.

shl(bits) сдвиг влево с учетом знака (<<)

shr(bits) сдвиг вправо с учетом знака (>>)

ushr(bits) сдвиг вправо без учета знака (>>>)

and(bits) побитовое И

or(bits) побитовое ИЛИ

xor(bits) побитовое исключающее ИЛИ

inv() побитовое отрицание

На заметку Поразрядные операции доступны только для


типов Int и Long.

Поразрядные операции манипулируют двоичными разрядами (би-


тами) в целочисленном значении, поэтому очень важно понимать, ка-
кое влияние подобные манипуляции могут оказывать на целочислен-
ные значения.
Поразрядная унарная операция НЕ в Kotlin является функцией ти-
пов Int и Long inv(). Эта функция инвертирует все двоичные раз-
ряды своего операнда.

y = x.inv()

73
Язык программирования Kotlin

При выполнении поразрядной логической операции И, обозначае-


мой функцией and, в двоичном разряде результата устанавливается 1
лишь в том случае, если соответствующие двоичные разряды в операн-
дах также равны 1. Во всех остальных случаях в двоичном разряде ре-
зультата устанавливается 0.

z = x and y

При выполнении поразрядной логической операции ИЛИ, обознача-


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

z = x or y

При выполнении поразрядной логической операции исключающее


ИЛИ, обозначаемой функцией xor, в двоичном разряде результата уста-
навливается 1, если двоичный разряд только в одном из операндов ра-
вен 1, иначе устанавливается 0.

z = x xor y

Операция сдвига влево, обозначаемая функцией shl, смещает все


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

z = x shl 2

Операция сдвига вправо, обозначаемая функцией shr, смещает все


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

z = x shr 2

Операция сдвига вправо без учета знака, обозначаемая функцией


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

74
Глава 4. Операции

z = x ushr 2

Простая программа ниже демонстрирует применение поразрядных


операций.

/**
* Программа демонстрирует поразрядные
операции
*/
fun main(args: Array<String>){
val x: Int = 42
val y: Int = 15
val r: Int = 64
val z: Int = -1
println(“Выполнение операции x.inv()
равно ${x.inv()}”)
println(“Выполнение операции x and y
равно ${x and y}”)
println(“Выполнение операции x or y
равно ${x or y}”)
println(“Выполнение операции x xor y
равно ${x xor y}”)
println(“Выполнение операции r shl 2
равно ${r shl 2}”)
println(“Выполнение операции z ushr 24
равно ${z ushr 24}”)
}

Программа выдаст следующий результат:


Выполнение операции x.inv() равно -43
Выполнение операции x and y равно 10
Выполнение операции x or y равно 47
Выполнение операции x xor y равно 37
Выполнение операции r shl 2 равно 256
Выполнение операции z ushr 24 равно 255

75
Язык программирования Kotlin

ОПЕРАЦИЯ ПРИСВАИВАНИЯ
Операция присваивания обозначается одиночным знаком равенства =.
В Kotlin операция присваивания действует так же, как и во многих дру-
гих языках программирования. Она имеет следующую форму:

Переменная = выражение

В этой форме переменная и выражение должны иметь совместимый


тип.
Обратите внимание, что в отличие от Java в Kotlin нельзя делать при-
сваивания в виде цепочки. Например:

В Java
int x, y, x;
X = y = z = 100;

В Kotlin так делать нельзя. В Kotlin придется записать так:

val x: Int; val y: Int; val z: Int


X = 100; y = 100; z = 100

ТЕРНАРНАЯ ОПЕРАЦИЯ
В языке Kotlin отсутствует тернарная операция как отдельный вид опе-
рации. Так как в Kotlin управляющая конструкция if является выра-
жением (возвращает значение), то она вполне справляется с функцио-
нальностью тернарный функции.
Если в Java мы пишем тернарную операцию:

ratio = denom == 0 ? 0 : num /denom;

То в Kotlin мы пишем:

ratio = if (denom == 0) 0 else num / denom

Подробнее выражение if рассматривается в главе 5 – «Управляю-


щие операторы».

76
Глава 4. Операции

ПРИОРИТЕТ ОПЕРАЦИЙ
Старшинство Название Символ

Высший Postfix ++, —, ., ?., ?


приоритет

Prefix -, +, ++, —, !,
label

Type RHS :, as, as?

Mutiplicative *,/,%

Additive +, -

Range ..

Infix function shl, shr, ushr,


and or, xor, inv,

Elvis ?:

Named checks in, !in, is, !is

Comparison <, >, <=, >=

Equality ==, !=

Conjunction &&

Disjunction ||

Низший Assignment =, +=, -=, *=, /=,


приоритет %=

Приоритет операций в Kotlin приведен в таблице выше. Операции,


находящиеся в одном ряду, имеют одинаковый приоритет. Операции
с двумя операндами имеют порог исчисления слева направо (исключе-
ние составляет операция присваивания, которая выполняется справа
налево).

77
Язык программирования Kotlin

Круглые скобки

Круглые скобки повышают приоритет заключенных в них операций.


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

y = a shr b + 3

Сначала в этом выражении к значению переменной b добавляется


значение 3, а затем двоичные разряды значения переменной a сдви-
гаются вправо на полученное количество позиций. Но если требует-
ся сначала сдвинуть двоичные разряды значения переменной а впра-
во на b позиций, а затем добавить 3 к полученному результату, то
следует использовать круглые скобки для повышения приоритета
операции:

y = (a shr b) + 3

Кроме изменения приоритета операций, круглые скобки можно ис-


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

a or 4 + c shr b and 7
(a or (((4 + c) shr b) and 7))

Следует понимать, что применение круглых скобок никоим образом


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

ПЕРЕГРУЗКА ОПЕРАТОРОВ
В языке Kotlin реализован предопределенный набор операторов для су-
ществующих типов. Эти операторы имеют фиксированное символиче-
ское представление (вроде + или -) и фиксированные приоритеты. Для
реализации оператора Kotlin предоставляет функцию-член или функ-
цию-расширение с фиксированным именем и соответствующим типом,

78
Глава 4. Операции

т.е. левосторонним типом для бинарных операций или типом аргумен-


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

Унарные префиксные операторы


+a a.unaryPlus()

-a a.unaryMinus()

!a a.not()

Когда компилятор обрабатывает, к примеру, выражение +a, он


оcуществляет следующие действия:
• Определяет тип выражения а
• Смотрится функция unaryPlus() с модификатором
operator без параметров для соответствующего типа
• Если функция отсутствует или она неоднозначна, компилятор
выдает ошибку
• Если функция присутствует и ее возвращаемый тип R, выраже-
ние +a имеет тип R

Инкремент и декремент
a++ a.inc()

a-- a.dec()

Функции inc() и dec() должны возвращать значение, которое


будет присвоено переменной, к которой была применена операция ++
или --. Они не должны пытаться изменять сам объект, для которого
inc или dec были вызваны.
Компилятор осуществляет следующие шаги для разрешения опера-
торов в постфиксной форме, например для a++:

79
Язык программирования Kotlin

• Определяется тип переменной a, пусть это будет T


• Смотрится функция inc() с модификатором operator без
параметров, применимая для приемника типа Т
• Проверяется, что возвращаемый тип такой функции является
подтипом T

Эффектом вычисления будет:


• Загружается инициализирующее значение a во временную пере-
менную a0
• Результат выполнения a.inc() to a
• Возвращается a0 как результат вычисления выражения (т.е. зна-
чение до инкремента)

Для a-- шаги выполнения полностью аналогичные.

Для префиксной формы ++a или--a разрешение работает подобно,


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

Арифметические операторы

Для перечисленных в таблице операций компилятор всего лишь раз-


решает выражение в вызов функции из 2-й колонки

a + b a.plus(b)

a - b a.minus(b)

a * b a.times(b)

a / b a.div(b)

a % b a.rem(b), a.mod(b)
(deprecated)

a..b a.rangeTo(b)

80
Глава 4. Операции

Следует отметить, что операция rem поддерживается только начи-


ная с Kotlin 1.1. Kotlin 1.0 использует только операцию mod, которая от-
мечена как устаревшая в Kotlin 1.1.

Оператор in
a in b b.contains(a)
a !in b !b.contains(a)

Оператор доступа по индексу


a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ..., i_n] a.get(i_1, ..., i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ..., i_n] = b a.set(i_1, ..., i_n, b)

Оператор вызова
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ..., i_n) a.invoke(i_1, ..., i_n)

Составные операторы
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.modAssign(b)

81
Язык программирования Kotlin

Для присваивающих операций, таких как a += b, компилятор осу-


ществляет следующие шаги:
• Проверяет если функция из правой колонки таблицы доступна
• Если соответствующая бинарная функция (т.е. plus() для
plusAssign()) также доступна, то фиксируется ошибка
(неоднозначность)
• Проверяется, что возвращаемое значение функции Unit, в про-
тивном случае фиксируется ошибка
• Генерируется код для a.plusAssign(b)
• В противном случае делается попытка сгенерировать код для
a = a + b (при этом включается проверка типов: тип выра-
жения a + b должен быть подтипом a)

Следует отметить, что присвоение НЕ ЯВЛЯЕТСЯ выражением


в Kotlin.

Операторы равенства
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

Следует помнить, что операции === и !== (проверка идентичности)


являются неперегружаемыми, поэтому не приводятся никакие соглаше-
ния для них.
Операция == имеет специальный смысл: она транслируется в со-
ставное выражение, в котором экранируются значения null.
null == null — это всегда истина, а x == null для ненулевых
x - всегда ложь и не должно расширяться в x.equals().

Операторы сравнений
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

Все сравнения транслируются в вызовы compareTo(), от которых


требуется, чтобы они возвращали значение Int.

82
Глава 5. Управляющие операторы

ГЛАВА 5.
УПРАВЛЯЮЩИЕ
ОПЕРАТОРЫ

УПРАВЛЯЮЩИЕ ОПЕРАТОРЫ
В языках программирования управляющие операторы применяют-
ся для реализации переходов, ветвлений в потоке исполнения команд
программы, исходя из ее состояния. Управляющие операторы в языке
Kotlin можно разделить на три категории: операторы выбора, операто-
ры цикла и операторы перехода. Операторы выбора позволяют выби-
рать разные ветви выполнения команд в соответствии с результатом
вычисления заданного выражения или состояния переменной. Опера-
торы цикла позволяют повторять выполнение одного или нескольких
операторов. Операторы перехода обеспечивают возможность нелиней-
ного выполнения программы.

ОПЕРАТОРЫ ВЫБОРА
В языке Kotlin поддерживаются два оператора выбора: выражения
if и when. Эти операторы позволяют управлять порядком выполне-
ния команд программы в соответствии с условиями, которые известны
только во время выполнения.

83
Язык программирования Kotlin

ВЫРАЖЕНИЕ IF
В отличие от языка Java, где if является оператором ветвления, в языке
Kotlin if можно использовать и как оператор, и как выражение. А это,
в свою очередь, значит, что if может возвращать значение. Вследствие
этого поведение if в языке Kotlin несколько отличается от традицион-
ного поведения оператора if в других языках программирования. Об-
щая форма выражения if выглядит следующим образом:

if (условие)
оператор1
else
оператор2

Где условие — любое выражение, возвращающее логическое значе-


ние типа Boolean, а оператор обозначает одиночный или составной
оператор языка Kotlin.
Выражение if действует следующим образом: если условие истин-
но, то выполняется оператор1, в противном случае, если указан блок
else, выполняется оператор2.
Рассмотрим несколько примеров использования выражения if:

// Простое использование в качестве


оператора
var max = a
if (a < b) max = b
// Простое использование в качестве
оператора с else
var max: Int
if (a > b) {
max = a
} else {
max = b
}

// Использование в качестве выражения,


замещающего к тому же тернарный оператор.
val max = if (a > b) a else b

84
Глава 5. Управляющие операторы

“Ветви” выражения if могут содержать несколько строк кода.


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

val max = if (a > b) {


print(“возвращаем a»)
a
} else {
print(“возвращаем b”)
b
}

Если вы используете конструкцию if в качестве выражения (напри-


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

if (условие)
оператор
else if (условие)
оператор
else if (условие)
Оператор
...
...
...

При использовании конструкции if-else-if условные выраже-


ния if выполняются последовательно, сверху вниз. Как только одно
из условий оказывается равным true, выполняется оператор, связан-
ный с этим условием, а остальные уловия пропускаются. Если ни одно
из условий не выполняется, то выполняется заключительный оператор
else. Этот последний else оператор служит условием по умолчанию.
Если же заключительный else оператор не указан и результат всех ус-
ловий равен false, то не выполняется никаких действий.

85
Язык программирования Kotlin

Ниже приведен пример программы, в которой конструкция if-


else-if служит для определения времени года, к которому относит-
ся конкретный месяц.

/**
* Программа демонстрирует if-else-if
*/
fun main(args: Array<String>){
val month = 4
val season: String
if (month == 12 || month == 1 || month
== 2)
season = “зиме»
else if (month == 3 || month == 4 ||
month == 5)
season = “весне»
else if (month == 6 || month == 7 ||
month == 8)
season = “лету»
else if (month == 9 || month == 10 ||
month == 11)
season = “осени”
else
season = «вымышленным месяцам»
Println(«Указанный месяц относится
к $season»)
}

Эта программа выводит следующий результат:


Указанный месяц относится к весне

ОПЕРАТОР ?:
Если у нас есть nullable ссылка r, мы можем либо провести провер-
ку этой ссылки и использовать ее, либо использовать non-null зна-
чение x:

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


-1

86
Глава 5. Управляющие операторы

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

val l = b?.length ?: -1

Если выражение, стоящее слева от элвис-оператора, не является


null, то элвис-оператор его вернет. В противном случае в качестве воз-
вращаемого значения послужит то, что стоит справа. Обращаем ваше
внимание на то, что часть кода, расположенная справа, выполняется
ТОЛЬКО в случае, если слева получается null.
Так как throw и return тоже являются выражениями в Kotlin, их
также можно использовать справа от элвис-оператора. Это может быть
крайне полезным для проверки аргументов функции:

fun foo(node: Node): String? {


val parent = node.getParent() ?: re-
turn null
val name = node.getName() ?: throw
IllegalArgumentException(“name expected”)
// ...
}

ВЫРАЖЕНИЕ WHEN
В языке Kotlin нет оператора switch. Его призвано заменить выраже-
ние when. По сути, выражение when можно назвать оператором вет-
вления. Оно представляет собой простой способ направить поток ис-
полнения команд по разным ветвям кода, в зависимости от значения
управляющего выражения. Зачастую выражение when оказывается эф-
фективнее длинных последовательностей операторов конструкции if-
else-if. Общая форма выражения when имеет следующий вид:

when (аргумент) {
Значение -> оператор
Значение -> оператор
else -> оператор
}

В простейшем виде использование when выглядит так:

87
Язык программирования Kotlin

when (x) {
1 -> print(“x == 1”)
2 -> print(“x == 2”)
else -> print(“x is neither 1 nor 2”)
}

Выражение when действует следующим образом: when последова-


тельно сравнивает аргумент со всеми указанными значениями до удов-
летворения одного из условий. Если ни одно из значений не удовлетво-
рило потребности выражения, то выполняется код ветви else.
Выражение when можно использовать и как выражение, и как опе-
ратор. При использовании в виде выражения значение ветки, удов-
летворяющей условию, становится значением всего выражения. При
использовании в виде оператора значения отдельных веток отбрасыва-
ются. (В точности как if: каждая ветвь может быть блоком, и ее значе-
нием является значение последнего выражения блока.)
Если when используется как выражение, то ветка else является
обязательной, за исключением случаев, когда компилятор может убе-
диться, что ветки покрывают все возможные значения.
Если для нескольких значений выполняется одно и то же действие,
то условия можно перечислять в одной ветке через запятую:

when (x) {
0, 1 -> print(“x == 0 or x == 1”)
else -> print(«otherwise»)
}

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


выражения:

when (x) {
parseInt(s) -> print(“s encodes x”)
else -> print(“s does not encode x”)
}

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


!in или его наличие в коллекции:

when (x) {
in 1..10 -> print(“x is in the range”)
in validNumbers -> print(“x is valid”)

88
Глава 5. Управляющие операторы

!in 10..20 -> print(“x is outside the


range”)
else -> print(“none of the above”)
}

Помимо этого Kotlin позволяет с помощью is и !is проверить тип


аргумента. Обратите внимание, что благодаря smart casts (умные
приведения, см. главу 3 – «Типы данных», раздел «Приведение типов»),
вы можете получить доступ к методам и свойствам типа без дополни-
тельной проверки:

val hasPrefix = when(x) {


is String -> x.startsWith(“prefix”)
else -> false
}

Выражение when удобно использовать вместо цепочки условий


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

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

Ниже приведен пример программы, в которой выражение when слу-


жит для определения времени года, к которому относится конкретный
месяц.

/**
* Программа демонстрирует работу выражения
when
*/
fun main(args: Array<String>){
val month = 4
val season: String
season = when (month) {
12, 1, 2 -> “зиме”
3, 4, 5 -> “весне”

89
Язык программирования Kotlin

6, 7, 8 -> “лету”
9, 10, 11 -> “осени”
else -> «непонятно куда»
}
println(«Указанный месяц относится
к $season»)
}

ОПЕРАТОРЫ ЦИКЛА
Для управления конструкциями, которые называются циклами, в языке
Kotlin предоставляются операторы for, while и do-while. Циклы —
это конструкции, которые многократно выполняют один и тот же на-
бор инструкций до тех пор, пока не будет удовлетворено условие завер-
шения цикла.

ЦИКЛ FOR
Цикл for обеспечивает перебор всех значений, поставляемых итерато-
ром. Язык Kotlin не поддерживает традиционную форму записи цикла
for, такую, как, например, в Java:

for (int i=0; i<100; i++) {


println(i)
}

Вместо этого используется следующий синтаксис:

for (переменная in выражение) оператор

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

for (item: Int in ints) {


// ...
}

90
Глава 5. Управляющие операторы

Как отмечено выше, цикл for позволяет проходить по всем элемен-


там объекта, имеющего итератор. Например, объект должен обладать
внутренней или внешней функцией iterator(), возвращаемый тип
которой обладает внутренней или внешней функцией next()и вну-
тренней или внешней функцией hasNext(), возвращающей Boolean.
Все три указанные функции должны быть объявлены как operator.
Если при проходе по массиву или списку необходим порядковый но-
мер элемента, используйте следующий подход:

for (i in array.indices)
Print(array[i])

Ниже представлены несколько примеров использования цикла for.

Простой цикл

for (i in 0..99) {
println(i)
}

Цикл с понижением значения

for (i in 99 downTo 0) {
println(i)
}

Цикл с использованием шага

for (i in 99 downTo 0 step 10) {


println(i)
}

Создание inline функции

public inline fun repeat(times: Int, action:


(Int) -> Unit) {
for (index in 0..times - 1) {

91
Язык программирования Kotlin

action(index)
}
}
repeat(times = 5) {
// implicit “it” is 0..4
println(it)
}

Перебор массива

val names = arrayOf(“Jake”, “Jill”, “Ash-


ley”, “Bill”)
for (name in names) {
println(name)
}

Проход по коллекции

for ((index,value) in (‘a’..’z’).withIn-


dex()) {
// index is 0..25 value is ‘a..z’
println(“$index $value”)
}

ЦИКЛ WHILE И DO-WHILE

Цикл while

Оператор цикла while повторяет оператор или блок операторов


до тех пор, пока значение его управляющего выражения истинно. Опе-
ратор while имеет следующую форму:

while (условие) {
//..
}

92
Глава 5. Управляющие операторы

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


простым, и демонстрирующей работу цикла while:

/**
* Программа проверяет, является ли число
простым
* Программа демонстрирует цикл while
*/
fun main(args: Array<String>){
val num: Int = 13
var isPrime: Boolean
var i: Int = 2
isPrime = num >= 2
while(i < num / i) {
if ((num % i) == 0) {
isPrime = false
break
}
i++
}
println(“Число $num « + (if (isPrime)
«простое» else «не простое»))
}

При выполнении оператора цикла while сначала происходит про-


верка условия и только потом, если это условие истинно, будет выпол-
нены операторы в теле цикла. Но иногда тело цикла желательно вы-
полнить хотя бы один раз, даже если в начальный момент условное
выражение ложно. Для этой цели в языке Kotlin предоставляется цикл,
который называется do-while.

Цикл do-while

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

do {
//..
} while (условие)

93
Язык программирования Kotlin

При каждом повторении цикла сначала выполняется тело цикла,


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

/**
* Программа демонстрирует работу цикла do-
while
*/
fun main(args: Array<String>){
var n = 10
do {
println(“Такт №$n»)
} while (--n > 0)
}

ВЛОЖЕННЫЕ ЦИКЛЫ
Как и в других языках программирования, в Kotlin допускается при-
менение вложенных циклов. Это означает, что один цикл выполняется
в другом цикле. Следующий пример программы демонстрирует работу
вложенных циклов:

/**
* Программа демонстрирует работу цикла for
*/
fun main(args: Array<String>){
val i: Int = 0
val j: Int
for (i in 0..10) {
for (j in i..10) {
print(“.”)
}
println()
}
}

Программа выведет следующий результат:

94
Глава 5. Управляющие операторы

...........
..........
.........
........
.......
......
.....
....
...
..
.

ОПЕРАТОРЫ ПЕРЕХОДА
В языке Kotlin определены три оператора перехода: break, continue
и return. Они служат для непосредственной передачи управления дру-
гой части программы.

break завершает выполнение цикла

continue продолжает выполнение цикла


со следующего его шага, без обработки
оставшегося кода текущей итерации

return по умолчанию производит возврат


из ближайшей окружающей его функции или
анонимной функции

Любое выражение в Kotlin может быть помечено меткой label.


Метки имеют идентификатор в виде знака @. Например: метки abc@,
fooBar@ являются корректными (см. грамматика). Для того чтобы по-
метить выражение, мы просто ставим метку перед ним:

loop@ for (i in 1..100) {


// ...
}

Теперь мы можем уточнить значения операторов break или


continue с помощью меток:

95
Язык программирования Kotlin

loop@ for (i in 1..100) {


for (j in 1..100) {
if (...)
break@loop
}
}

Оператор break, отмеченный @loop, переводит выполнение кода


к той его части, которая находится сразу после соответствующей мет-
ки loop@. Оператор continue продолжает цикл со следующей его
итерации.
В Kotlin функции могут быть вложены друг в друга с помощью ано-
нимных объектов, локальных функций и функциональных литералов.
Подходящий return позволит вернуться из внешней функции. Одним
из самых удачных применений этой синтаксической конструкции слу-
жит возврат из лямбда-выражения. Подумайте над этим утверждением,
читая данный пример:

fun foo() {
ints.forEach {
if (it == 0) return
print(it)
}
}

Оператор return возвращается из ближайшей функции,


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

fun foo() {
ints.forEach lit@ {
if (it == 0) return@lit
print(it)
}
}

96
Глава 5. Управляющие операторы

Теперь он возвращает только из лямда-выражения. Зачастую намно-


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

fun foo() {
ints.forEach {
if (it == 0) return@forEach
print(it)
}
}

Возможно также использование анонимной функции в качестве аль-


тернативы лямбда-выражениям. Оператор return возвращается из са-
мой анонимной функции.

fun foo() {
ints.forEach(fun(value: Int) {
if (value == 0) return
print(value)
})
}

Применение оператора break

В языке Kotlin оператор break находит несколько применений. Ис-


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

/**
* Пример программы, демонстрирующей работу
оператора break
*/
fun main(args: Array<String>){
for (i in 0..100) {
if (i == 10) break
println(“i: $i”)
}
println(“Цикл завершен оператором

97
Язык программирования Kotlin

break”)
}

Программа выведет следующий результат:


i: 0
i: 1
i: 2
i: 3
i: 4
i: 5
i: 6
i: 7
i: 8
i: 9
Цикл завершен оператором break

Как видно из примера выше, оператор break приводит к более ран-


нему выходу из цикла for, когда значение переменной i становится
равным 10, хотя цикл должен бы выполняться при значениях перемен-
ной от 0 до 100.
Оператор break можно использовать в любых циклах, доступных
в Kotlin.
Если в программе применяется ряд вложенных циклов, то оператор
break осуществляет выход только из самого внутреннего цикла:

/**
* Пример программы, демонстрирующей
оператор break
*/
fun main(args: Array<String>){
for (i in 0..3) {
print(“Проход $i: «)
for (j in 0..99) {
if (j == 10) break
print(“$j “)
}
println()
}
println(“Циклы завершены»)
}

98
Глава 5. Управляющие операторы

Программа выведет следующий результат:


Проход 0: 0 1 2 3 4 5 6 7 8 9
Проход 1: 0 1 2 3 4 5 6 7 8 9
Проход 2: 0 1 2 3 4 5 6 7 8 9
Проход 3: 0 1 2 3 4 5 6 7 8 9
Циклы завершены

Как видно из примера, оператор break во внутреннем цикле может


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

На заметку Оператор break не предназначен в качестве


обычного средства выхода из цикла. Для этого служит
условное выражение в цикле. Этот оператор следует
использовать для выхода из цикла только в особых случаях.

Применение оператора continue

Иногда требуется, чтобы повторение цикла осуществлялось с более


раннего оператора в его теле. Это означает, что на данном конкретном
шаге может возникнуть потребность продолжить выполнение цикла
без выполнения остального кода тела цикла. По существу, это означа-
ет переход в теле цикла к его окончанию. Для выполнения этого служит
оператор перехода continue. В циклах while и do-while оператор
continue вызывает передачу управления непосредственно условию,
управляющему циклом. В цикле for управление передает-
ся вначале итерационный части. Во всех трех видах циклов любой про-
межуточный код пропускается.
Ниже приведен пример программы, демонстрирующий применение
оператора continue:

/**
* Программа демонстрирует применение
оператора continue
*/
fun main(args: Array<String>){
for (i in 0..9) {

99
Язык программирования Kotlin

print(“$i “)
if ( i % 2 == 0) continue
println()
}
}

Программа выведет следующий результат:


0 1
2 3
4 5
6 7
8 9

Как и оператор break, оператор continue может содержать мет-


ку перехода. Ниже приведен пример программы, в которой оператор
continue применяется для вывода треугольной таблицы умножения
чисел от 0 до 9:

/**
* Программа демонстрирует применение
оператора continue с меткой
*/
fun main(args: Array<String>){
loop@ for (i in 0..9) {
for ( j in 0..9 ) {
if (j > i) {
println()
continue@loop
}
print(“ “ + (i * j))
}
}
println()
}

Программа выведет следующий результат:


0
0 1
0 2 4
0 3 6 9
0 4 8 12 16

100
Глава 5. Управляющие операторы

0 5 10 15 20 25
0 6 12 18 24 30 36
0 7 14 21 28 35 42 49
0 8 16 24 32 40 48 56 64
0 9 18 27 36 45 54 63 72 81

Оператор return

Оператор return служит для выполнения явного выхода из функ-


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

101
Язык программирования Kotlin

ГЛАВА 6.
ФУНКЦИИ И ЛЯМБДЫ

ФУНКЦИИ В KOTLIN
Основным способом оперирования данными в языке Kotlin являют-
ся функции или, как еще их называют, методы. Функция — это эле-
мент языка, который позволяет сгруппировать программный код в от-
дельную программную единицу и обращаться к ней из другого места
программы. В объектно-ориентированном программировании функ-
ции, объявления которых являются неотъемлемой частью определения
класса, называют также методами. Общая форма определения функции
имеет следующий вид:

fun имя_функции(параметры): тип {


тело_функции
}

Чем же так примечательны функции? Давайте рассмотрим пример,


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

fun main(args: Array<String>){


val valid_pass = “qwerty”
val user_pass: String?
print(“Введите пароль: «)

102
Глава 6. Функции и лямбды

user_pass = readLine()
if (user_pass == valid_pass) {
println(“Пароль верный”)
} else {
println(«Пароль неверный»)
}
/*
* ...Некий длинный код и повторная
проверка
*/
if (user_pass == valid_pass) {
println(“Пароль верный”)
} else {
println(«Пароль неверный»)
}
}

А вот аналогичный код с функцией:

fun checkPass(user_pass: String?): Unit {


val valid_pass = “qwerty”
if (user_pass == valid_pass) {
println(“Пароль верный”)
} else {
println(«Пароль неверный»)
}
}
fun main(args: Array<String>){
val user_pass: String?
print(“Введите пароль: «)
user_pass = readLine()
checkPass(user_pass)
/*
* ...Некий длинный код и повторная
проверка
*/
checkPass(user_pass)
}

С точки зрения компилятора этот код идентичен, но с точки зре-


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

103
Язык программирования Kotlin

мы получили ряд преимуществ. Во-первых, мы избавились от необхо-


димости повторять один и тот же обширный код, в нашем случае это
оператор if, что позволяет нам избежать возможных ошибок в повто-
ряющемся коде. Во-вторых, мы скрыли часть переменных в область
видимости функции, выведя их из глобальной видимости программы
(константа valid_pass). В-третьих, если нам стало вдруг необходимо
изменить повторяющийся код, то нам достаточно изменить его внутри
функции, а не выискивать его по всему коду программы.
В Kotlin функции объявляются с помощью ключевого слова fun, за-
тем идет имя функции, это может быть любой разрешенный идентифи-
катор, затем в скобках параметры функции, затем тип возвращаемого
значения и потом в фигурных скобках идет тело функции. Определение
типа может быть опущено, если функция ничего не возвращает явно.
В этом случае возвращается тип Unit.
Ярким примером функции является функция main(), которая уже
неоднократно была здесь представлена в примерах. Давайте рассмо-
трим ее еще раз:

fun main(args: Array<String>) {


//..
}

В данном примере первым идет ключевое слово fun, затем имя


функции main, затем в круглых скобках определяются параметры
функции, в данном случае это один параметр args, имеющий тип мас-
сив строк Array<String>. Так как функция у нас явно не возвраща-
ет тип, а неявно этим типом является Unit, то тип у нас не указан, опу-
щен. И в фигурных скобках располагается собственно тело функции,
в котором находится программный код.

ПРИМЕНЕНИЕ ФУНКЦИЙ
Функции в Kotlin имеют много разных форм и различные тонкости.
Ниже приведен список особенностей для функций:
• Функции одиночного выражения
• Необязательные параметры
• Позиционные и именованные аргументы
• Аргумент переменной длины
• Функциональный тип

104
Глава 6. Функции и лямбды

• Функциональный литерал
• Вызываемые ссылки (Callable references)
• Функции расширения
• Индексный вызов функций
• Локальные функции
• Замыкания
• Обобщенные функции (Generic)
• Перегрузка операторов

Функция может быть вызвана как отдельный оператор и как


выражение.

// Как выражение
val result = myFunction()
// Как отдельный оператор
myFinction()

В случае если функция вызывается как выражение, то она долж-


на возвращать значение. Следующий пример наглядно демонстрирует
применение функций:

fun fact(n: Long): Long {


if (n <= 0) {
return 0
} else if (n.toInt() == 1) {
return 1
} else {
return n * fact(n - 1)
}
}
fun printResult(n: Long, r: Long) {
println(“Факториал числа $n равен $r”)
}
fun main(args: Array<String>){
var result: Long
for (i in 0..5) {
// Вызываем функцию fact как
выражение
result = fact(i.toLong())
// Вызываем функцию как отдельный
оператор

105
Язык программирования Kotlin

printResult(i.toLong(), result)
}
}

В случае если вызываемая функция является методом класса, то ее


вызов может предварять оператор (.) точка, если вызов осуществляет-
ся за пределами класса (подробно классы рассматриваются в главе 7):

MathUtils.fact()

ИНФИКСНОЕ ОБОЗНАЧЕНИЕ
Язык Kotlin предоставляет возможность вызывать функцию при помо-
щи инфиксного обозначения. Это может быть сделано, если:
• Функция является членом другой функции или расширения
• В ней используется один параметр
• Функция помечена ключевым словом infix

// Определить выражение как Int


infix fun Int.shl(x: Int): Int {
...
}
// вызвать выражение функции, используя infix
1 shl 2
// то же самое, что
1.shl(2)

ПАРАМЕТРЫ ФУНКЦИИ
Как говорилось ранее, параметры функции определяются в заголовке
функции после ее имени в круглых скобках. Параметры записываются
в виде пар значений имя:тип. Параметры разделяются запятыми. Каж-
дый параметр должен быть явно указан.

fun powerOf(number: Int, exponent: Int) {


...
}

106
Глава 6. Функции и лямбды

В данном примере мы объявили два параметра: number с типом


Int и exponent с типом Int.

Значения по умолчанию

Параметры функции могут иметь значения по умолчанию, которые


используются в случае, если аргумент функции не указан при ее вы-
зове. Это позволяет снизить уровень перегруженности кода по сравне-
нию с другими языками. Значения по умолчанию указываются после
типа за знаком =.

fun read(b: Array<Byte>, off: Int = 0, len:


Int = b.size()) {
...
}

Такая функция может быть вызвана без явного указания второ-


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

fun read(b: Array<Byte>, off: Int = 0) {


for (i in off..b.size-1) {
println(“b[$i] = ${b[i]}”)
}
}
fun main(args: Array<String>){
val bytes: Array<Byte> = arrayOf(1, 2,
3, 4, 5)
println(“Вызов без параметра
по умолчанию”)
read(bytes)
println(“Указаны оба параметра”)
read(bytes, 3)
}

Программа выведет следующий результат:


Вызов без параметра по умолчанию
b[0] = 1
b[1] = 2
b[2] = 3

107
Язык программирования Kotlin

b[3] = 4
b[4] = 5
Указаны оба параметра
b[3] = 4
b[4] = 5

ИМЕНА В НАЗВАНИЯХ
ПАРАМЕТРОВ
Параметры функции могут быть названы в момент вызова функций.
Это очень удобно, когда у функции большой список параметров, в том
числе со значениями по умолчанию.
Рассмотрим такую функцию:

fun reformat(str: String,


normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ‘ ‘) {
...
}

Мы можем вызвать ее, используя аргументы по умолчанию

val newStr = reformat(str)

Однако при вызове этой функции без аргументов по умолчанию по-


лучится что-то вроде:

val newStr = reformat(str, true, true,


false, ‘_’)

С названными аргументами мы можем сделать код намного более


читабельным:

val newStr = reformat(str,


normalizeCase = true,
upperCaseFirstLetter = true,

108
Глава 6. Функции и лямбды

divideByCamelHumps = false,
wordSeparator = ‘_’
)

А если нам не нужны все аргументы, имеющие значения по умолча-


нию, то:

val newStr = reformat(str, wordSeparator =


‘_’)

На заметку Обратите внимание, что синтаксис


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

ФУНКЦИИ,
ВОЗВРАЩАЮЩИЕ UNIT
Если функция не возвращает никакого полезного значения, ее возвра-
щаемый тип — Unit. Это возвращаемое значение не нуждается в яв-
ном указании:

fun printHello(name: String?): Unit {


if (name != null)
println(“Hello ${name}”)
else
println(“Hi there!”)
// `return Unit` или `return`
необязательны
}

Идентично

fun printHello(name: String?) {


...
}

109
Язык программирования Kotlin

ФУНКЦИИ С ОДНИМ
ВЫРАЖЕНИЕМ
Когда функция возвращает одно-единственное выражение, круглые
скобки { } могут быть опущены и тело функции может быть описано
после знака =

fun double(x: Int): Int = x * 2

Компилятор языка Kotlin достаточно умный и способен сам опреде-


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

fun double(x: Int) = x * 2

ЯВНЫЕ ТИПЫ ВОЗВРАЩАЕМЫХ


ЗНАЧЕНИЙ
Функции, в которых есть тело, всегда должны указывать возвращае-
мый ими тип данных (если в этом качестве не указан тип Unit). Kotlin
не вычисляет самостоятельно тип возвращаемого значения для функ-
ций с заключенным в них блоком кода потому, что подобные функции
могут иметь сложную структуру и возвращаемый тип не очевиден для
читающего этот код человека (иногда даже для компилятора).

ПЕРЕМЕННОЕ ЧИСЛО
АРГУМЕНТОВ
Параметр функции (обычно для этого используется последний) может
быть помечен модификатором vararg. Это позволит указать множе-
ство значений в качестве аргументов функции:

110
Глава 6. Функции и лямбды

fun <T> asList(vararg ts: T): List<T> {


val result = ArrayList<T>()
for (t in ts) // ts - это массив (Ar-
ray)
result.add(t)
return result
}
val list = asList(1, 2, 3)

Внутри функции параметр с меткой vararg и типом T виден как


массив элементов T, таким образом, переменная ts в вышеуказанном
примере имеет тип Array<out T>.
Только один параметр может быть помечен меткой vararg. Если
параметр с именем vararg не стоит на последнем месте в списке аргу-
ментов, значения для соответствующих параметров могут быть переда-
ны с использованием named argument синтаксиса (см. «Имена в на-
званиях параметров»). В случае если параметр является функцией, для
этих целей можно вынести лямбду за фигурные скобки (лямбды будут
рассмотрены позднее в этой главе).
При вызове vararg функции мы можем передать аргументы один
за одним, например asList(1, 2, 3) или, если у нас уже есть не-
обходимый массив элементов и мы хотим передать его содержимое
в нашу функцию, использовать оператор spread (*) (необходимо
пометить массив знаком *):

val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)

ОБЛАСТЬ ДЕЙСТВИЯ ФУНКЦИЙ


В Kotlin функции могут быть объявлены в самом начале файла. Подраз-
умевается, что вам не обязательно создавать объект какого-либо класса,
чтобы воспользоваться его функцией (как в Java, C# или Scala). В допол-
нение к этому функции в языке Kotlin могут быть объявлены локально,
как функции-члены и функции-расширения.

111
Язык программирования Kotlin

Локальные функции

В языке Koltin поддерживается определение и создание локальных


функций. Например, функции, вложенные в другие функции:

fun dfs(graph: Graph) {


fun dfs(current: Vertex, visited:
Set<Vertex>) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v, visited)
}
dfs(graph.vertices[0], HashSet())
}

Такие локальные функции могут иметь доступ к локальным пере-


менным внешних по отношению к ним функций (типа closure). Та-
ким образом, в примере, приведенном выше, visited может быть ло-
кальной переменной:

fun dfs(graph: Graph) {


val visited = HashSet<Vertex>()
fun dfs(current: Vertex) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v)
}
dfs(graph.vertices[0])
}

Функции-члены или методы классов

Функции-члены — это функции, объявленные внутри классов или


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

class Sample() {
fun foo() { print(«Foo») }
}

112
Глава 6. Функции и лямбды

val s = Sample()
S.foo()

Более подробно методы классов рассматриваются в главе 7.

Функции-расширения

Аналогично таким языкам программирования, как C# и Gosu,


Kotlin позволяет расширять класс путем добавления нового функ-
ционала, не наследуясь от такого класса и не используя паттерн «Де-
коратор». Это реализовано с помощью специальных выражений, на-
зываемых «расширения». Kotlin поддерживает функции-расширения
и свойства-расширения.
Для того чтобы объявить функцию-расширение, нам нужно ука-
зать в качестве приставки возвращаемый тип, то есть тип, кото-
рый мы расширяем. Следующий пример добавляет функцию swap
к MutableList<Int>:

fun MutableList<Int>.swap(index1: Int, in-


dex2: Int) {
val tmp = this[index1] // ‘this’ даёт
ссылку на Int
this[index1] = this[index2]
this[index2] = tmp
}

Ключевое слово this внутри функции-расширения соотносится


с получаемым объектом (его тип ставится перед точкой). Теперь мы мо-
жем вызывать такую функцию в любом MutableList<Int>:

val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // ‘this’ внутри ‘swap()’
не будет содержать значение ‘l’

Подробнее о расширениях рассказывается в главе 7.

113
Язык программирования Kotlin

Функции обобщения

Функции могут иметь обобщенные параметры, которые задаются


треугольными скобками и помещаются перед именем функции.

fun <T> singletonList(item: T): List<T> {


// ...
}

Для более подробной информации об обобщениях смотрите главу 7.

ФУНКЦИИ С ХВОСТОВОЙ
РЕКУРСИЕЙ
Хвостовая рекурсия — частный случай рекурсии, при котором любой
рекурсивный вызов является последней операцией перед возвратом из
функции. Подобный вид рекурсии примечателен тем, что может быть
легко заменен на итерацию путем формальной и гарантированно кор-
ректной перестройки кода функции. Оптимизация хвостовой рекурсии
путем преобразования ее в плоскую итерацию реализована во многих
оптимизирующих компиляторах. В некоторых функциональных языках
программирования спецификация гарантирует обязательную оптими-
зацию хвостовой рекурсии.
Kotlin поддерживает такой стиль функционального программирова-
ния. Это позволяет использовать циклические алгоритмы вместо ре-
курсивных функций, но без риска переполнения стека. Когда функция
помечена модификатором tailrec и ее форма отвечает требованиям
компилятора, он оптимизирует рекурсию, оставляя вместо нее быстрое
и эффективное решение этой задачи, основанное на циклах.

tailrec fun findFixPoint(x: Double = 1.0):


Double
= if (x == Math.cos(x)) x else
findFixPoint(Math.cos(x))

Этот код высчитывает fixpoint косинуса, который является ма-


тематической константой. Он просто-напросто постоянно вызыва-
ет Math.cos начиная с 1.0 до тех пор, пока результат не изменится,

114
Глава 6. Функции и лямбды

приняв значение 0.7390851332151607. Получившийся код эквивалентен


вот этому более традиционному стилю:

private fun findFixPoint(): Double {


var x = 1.0
while (true) {
val y = Math.cos(x)
if (x == y) return y
x = y
}
}

Для соответствия требованиям модификатора tailrec функ-


ция должна вызывать сама себя в качестве последней операции, ко-
торую она предпринимает. Вы не можете использовать хвостовую ре-
курсию, когда существует еще какой-то код после вызова этой самой
рекурсии. Также нельзя использовать ее внутри блоков try/catch/
finally. На данный момент хвостовая рекурсия поддерживается толь-
ко в backend виртуальной машины Java (JVM).

ЛЯМБДА-ВЫРАЖЕНИЯ
И АНОНИМНЫЕ ФУНКЦИИ
Лямбда-выражение, по сути, является анонимным методом. Но этот
метод не выполняется самостоятельно, а служит для реализации мето-
да, определяемого в функциональном интерфейсе. Нередко лямбда-вы-
ражения называют также замыканиями.
Язык Kotlin предоставляет широкие возможности по использова-
нию лямбда-выражений. Лямбда-выражения всегда заключены в фи-
гурные скобки. Лямбда-выражение определяется лямбда-оператором
или операцией (->) «стрелка».
Оператор стрелка разделяет лямбда-выражение на две части.
В левой части указываются параметры, требующиеся в лямбда- выра-
жении. Если параметры не требуются, они опускаются. В правой части
находится тело лямбда-выражения, где указываются действия, выпол-
няемые лямбда-выражением. Еще одно полезное соглашение состоит
в том, что если литерал функции имеет только один параметр, его объ-
явление может быть опущено (вместе с ->) и его имя будет it.

115
Язык программирования Kotlin

Рассмотрим пример простой программы, которая демонстрирует не-


которые примеры лямбда-выражений:

fun main(args: Array<String>){


val m = { x : String -> println(x) }
val n : (String) -> Unit = { x ->
println(x) }
val o : (String) -> Unit = { x :
String -> println(x) }
m(“good morning”)
n(“good morning”)
o(“good morning”)
val pi = { -> 3.1416 }
val isEven = { n: Int -> (n % 2) == 0
}
val fact = { n: Int -> Int
var result = 1
for (i in 1..n)
result *= i
result
}
println(“Значение числа Pi равно
${pi()}»)
for (i in 1..10) {
println(“Число $i ${if (isEven(i))
«четное» else «нечетное»} его факториал
равен ${fact(i)}»)
}
}

Программа выведет следующий результат:


good morning
good morning
good morning
Значение числа Pi равно 3.1416
Число 1 нечетное его факториал равен 1
Число 2 четное его факториал равен 2
Число 3 нечетное его факториал равен 6
Число 4 четное его факториал равен 24
Число 5 нечетное его факториал равен 120
Число 6 четное его факториал равен 720

116
Глава 6. Функции и лямбды

Число 7 нечетное его факториал равен 5040


Число 8 четное его факториал равен 40320
Число 9 нечетное его факториал равен 362880
Число 10 четное его факториал равен 3628800

Синтаксис лямбда-выражений

Лямбда-выражение или анонимная функция являются «функцио-


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

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

или же с использованием функционального типа

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


y }

Лямбда-выражение всегда заключено в фигурные скобки {...},


объявление параметров при таком синтаксисе происходит внутри этих
скобок и может включать в себя аннотации типов (опционально), тело
функции начинается после знака ->. Если тип возвращаемого значе-
ния не Unit, то в качестве возвращаемого типа принимается последнее
(а возможно, и единственное) выражение внутри тела лямбды.
Обычное дело, когда лямбда-выражение имеет только один пара-
метр. Если Kotlin может определить сигнатуру метода сам, он позволит
нам не объявлять этот единственный параметр и объявит его сам под
именем it:

ints.filter { it > 0 } //Эта константа имеет


тип ‘(it: Int) -> Boolean’

Анонимные функции

Особенностью синтаксиса лямбда-выражений является способ-


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

117
Язык программирования Kotlin

в определении возвращаемого типа, вы можете воспользоваться альтер-


нативным синтаксисом:

fun(x: Int, y: Int): Int = x + y

Объявление анонимной функции выглядит очень похоже на обыч-


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

fun(x: Int, y: Int): Int {


return x + y
}

Параметры функции и возвращаемый тип обозначаются таким же


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

ints.filter(fun(item) = item > 0)

Аналогично и с типом возвращаемого значения: он вычисляется ав-


томатически для функций-выражений или же должен быть определен
вручную (если не является типом Unit) для анонимных функций, ко-
торые имеют в себе блок.
Обратите внимание, что параметры анонимных функций всегда за-
ключены в скобки. Прием, позволяющий оставлять параметры вне ско-
бок, работает только с лямбда-выражениями.
Одним из отличий лямбда-выражений от анонимных функций явля-
ется поведение оператора return (non-local returns). Ключевое
слово return , не имеющее метки (@), всегда возвращается из функции,
объявленной ключевым словом fun. Это означает, что return вну-
три лямбда-выражения возвратит выполнение к функции, включающей
в себя это лямбда-выражение. Внутри анонимных функций оператор
return, в свою очередь, выйдет, собственно, из анонимной функции.

Замыкания

Лямбда-выражение или анонимная функция (так же, как и локаль-


ная функция или объектное выражение) имеет доступ к своему замы-
канию, то есть к переменным, объявленным вне этого выражения или

118
Глава 6. Функции и лямбды

функции. В отличие от Java переменные, захваченные в замыкании, мо-


гут быть изменены:

var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)

Литералы функций с объектом-приемником

Kotlin предоставляет возможность вызывать литерал функции с ука-


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

sum : Int.(other: Int) -> Int

По аналогии с расширениями литерал функции может быть вызван


так, будто он является методом объекта-приемника:

1.sum(2)

Синтаксис анонимной функции позволяет вам явно указать тип при-


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

val sum = fun Int.(other: Int): Int = this


+ other

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


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

class HTML {
fun body() { ... }
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // создание

119
Язык программирования Kotlin

объекта-приемника
html.init() // передача
приемника в лямбду
return html
}
html { // лямбда с приемником
начинается тут
body() // вызов метода объекта-
приемника
}

ВЫСОКОУРОВНЕВЫЕ ФУНКЦИИ
Высокоуровневая функция — это функция, которая принимает другую
функцию в качестве входного аргумента либо имеет функцию в каче-
стве возвращаемого результата. Хорошим примером такой функции яв-
ляется lock(), которая берет залоченный объект и функцию, приме-
няет лок, выполняет функцию и отпускает lock:

fun <T> lock(lock: Lock, body: () -> T): T{


lock.lock()
try{
return body()
}
finally {
lock.unlock()
}
}

Давайте проанализируем этот блок. Параметр body имеет функци-


ональный тип: () -> T, то есть предполагается, что это функция,
которая не имеет никаких входных аргументов и возвращает значение
типа T. Она вызывается внутри блока try, защищена lock, и ее ре-
зультат возвращается функцией lock().
Если мы хотим вызвать метод lock(), мы можем подать другую
функцию в качестве входящего аргумента:

120
Глава 6. Функции и лямбды

fun toBeSynchronized() = sharedResource.op-


eration()
val result = lock (lock, ::toBeSynchronized)

Другой, наиболее удобный способ применения лямбда-выражения:

val result = lock(lock, { sharedResource.op-


eration() })

В Kotlin существует конвенция, по которой, если последний пара-


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

lock (lock) {
sharedResource.operation()
}

Другим примером функции высшего порядка служит функция


map():

fun <T, R> List<T>.map(transform: (T) -> R):


List<R> {
val result = arrayListOf<R>()
for (item in this)
result.add(transform(item))
return result
}

Эта функция может быть вызвана следующим образом:

val doubled = ints.map { it -> it * 2 }

Обратите внимание, что параметры могут быть проигнорированы


при вызове функции в том случае, если лямбда является единственным
аргументом для ее вызова.

it: неявное имя одного параметра

Еще одной полезной особенностью синтаксиса является возмож-


ность опустить объявление параметра функции в случае, если он

121
Язык программирования Kotlin

единственный (вместе с ->). Слово it будет принято в качестве имени


для такой функции:

ints.map { it * 2 }

Это соглашение позволяет писать код в LINQ-стиле:

strings.filter { it.lenght == 5 }.sortBy {


it }.map { it.toUpperCase() }

Подчеркивание для неиспользуемых


переменных

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


подчеркивания вместо его имени:

map.forEach { _, value -> println(«$value!»)


}

Деструктуризация в лямбда-выражениях

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


ния (подробнее деструктурирование рассматривается в главе 7) для
лямбда-параметров. Если лямбда имеет параметр типа Pair (или Map.
Entry или любой другой тип, который имеет соответствующие функ-
ции componentN), вы можете ввести несколько новых параметров вме-
сто одного, поставив их в круглые скобки:

map.mapValues { entry -> «${entry.value}!» }


map.mapValues { (key, value) -> «$value!» }

Обратите внимание на разницу между объявлением двух параме-


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

{ a -> ... } // один параметр


{ a, b -> ... } // два параметра
{ (a, b) -> ... } // деструктуризованная
пара

122
Глава 6. Функции и лямбды

{ (a, b), c -> ... } // деструктуризованная


пара и параметр

Если компонент деструктурированного параметра не используется,


вы можете заменить его знаком подчеркивания, чтобы не изобретать
его имя:

map.mapValues { (_, value) -> «$value!» }

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


или для отдельного компонента:

map.mapValues { (_, value): Map.Entry<Int,


String> -> «$value!» }
map.mapValues { (_, value: String) -> «$val-
ue!» }

ВСТРОЕННЫЕ (INLINE)
ФУНКЦИИ

inline

Использование функций высшего порядка влечет за собой сниже-


ние производительности: во-первых, функция является объектом, а во-
вторых, происходит захват контекста замыканием, то есть функции ста-
новятся доступны переменные, объявленные вне ее тела. А выделения
памяти (как для объекта функции, так и для ее класса) и виртуальные
вызовы занимают системные ресурсы.
Но во многих случаях эти «накладные расходы» можно устра-
нить с помощью встраивания лямбда-выражений. Например, функ-
ция lock() может быть легко встроена в то место, из которого она
вызывается:

lock(l) { foo() }

Вместо создания объекта функции для параметра и генерации вызо-


ва компилятор мог бы выполнить что-то подобное этому коду:

123
Язык программирования Kotlin

l.lock()
try {
foo()
}
finally {
l.unlock()
}

Чтобы заставить компилятор поступить именно так, нам необходи-


мо отметить функцию lock модификатором inline:

inline fun lock<T>(lock: Lock, body: () ->


T): T {
// ...
}

Модификатор inline влияет и на функцию, и на лямбду, передан-


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

noinline

В случае если вы хотите, чтобы только некоторые лямбды, пере-


данные inline-функции, были встроены, вам необходимо отметить мо-
дификатором noinline те функции-параметры, которые встроены
не будут:

inline fun foo(inlined: () -> Unit, noinline


notInlined: () -> Unit) {
// ...
}

Тогда как встраиваемые лямбды могут быть вызваны только вну-


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

124
Глава 6. Функции и лямбды

Заметьте, что если inline-функция не имеет ни inline параметров,


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

Нелокальный return

В Kotlin мы можем использовать обыкновенный, безусловный


return только для выхода из именованной функции или анонимной
функции. Это значит, что для выхода из лямбды нам нужно использо-
вать label. Обычный return запрещен внутри лямбды, потому что
она не может заставить внешнюю функцию завершиться:

fun foo() {
ordinaryFunction {
return // Ошибка: return не может
быть здесь использован
}
}

Но если функция, в которую передана лямбда, встроена, то return


также будет встроен, поэтому так делать можно:

fun foo() {
inlineFunction {
return // OK: the lambda is in-
lined
}
}

Такие return (находящиеся внутри лямбд, но завершающие внеш-


нюю функцию) называются нелокальными (non-local). Как прави-
ло, подобные конструкции используются в циклах, которые являются
inline-функциями:

fun hasZeros(ints: List<Int>): Boolean {


ints.forEach {
if (it == 0) return true // re-
turns from hasZeros

125
Язык программирования Kotlin

}
return false
}

Заметьте, что некоторые inline-функции могут вызывать переданные


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

inline fun f(crossinline body: () -> Unit)


{
val f = object: Runnable {
override fun run() = body()
}
// ...
}

Параметры вещественного типа

Иногда нам необходимо получить доступ к типу, переданному в ка-


честве параметра:

fun <T> TreeNode.findParentOfType(clazz:


Class<T>): T? {
var p = parent
while (p != null && !clazz.
isInstance(p)) {
p = p?.parent
}
@Suppress(“UNCHECKED_CAST”)
return p as T
}

В этом примере мы осуществляем проход по дереву и используем


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

126
Глава 6. Функции и лямбды

myTree.findParentOfType(MyTreeNodeType::class.
java)

На самом деле хотелось бы передать этой функции тип и вызвать ее


так:

myTree.findParentOfType<MyTreeNodeType>()

Чтобы получить такую возможность, inline-функции должны уметь


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

inline fun <reified T> TreeNode.findParen-


tOfType(): T? {
var p = parent
while (p != null && p !is T) {
p = p?.parent
}
return p as T
}

Мы определили тип параметра с помощью модификатора reified,


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

myTree.findParentOfType<MyTreeNodeType>()

Хотя рефлексия может быть не нужна во многих случаях, мы все еще


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

inline fun <reified T> membersOf() =


T::class.members
fun main(s: Array<String>) {
println(membersOf<StringBuilder>().
joinToString(“\n”))
}

127
Язык программирования Kotlin

Обычная функция (не отмеченная как встроенная) не может иметь


параметры вещественного типа. Тип, который не имеет представле-
ние во времени исполнения (например, параметр невещественного или
фиктивного типа вроде Nothing), не может использоваться в качестве
аргумента для параметра вещественного типа.

128
Глава 7. Классы и объекты

ГЛАВА 7.
КЛАССЫ И ОБЪЕКТЫ

ВВЕДЕНИЕ В КЛАССЫ
В языке Kotlin класс является одной из составляющих основ элементов
языка. Класс определяет форму и сущность объекта и образует основу
объектно-ориентированного программирования на Kotlin.
Наиболее важная особенность класса состоит в том, что он опре-
деляет новый тип данных. Как только этот новый тип данных будет
определен, им можно воспользоваться для создания объектов данно-
го типа. Таким образом, класс — это шаблон для создания объекта,
а объект — это экземпляр класса. А поскольку объект является эк-
земпляром класса, то понятия «объект» и «экземпляр» употребляют-
ся как синонимы.

ОБЩАЯ ФОРМА КЛАССА


При определении класса объявляется его конкретная форма и сущ-
ность. Для этого указываются данные, которые он содержит (свойства),
и код (методы), который воздействует на эти данные. Простые классы
могут содержать только код или только данные. Большинство классов,
применяемых в реальных программах, содержат и код, и данные. Код
класса определяет интерфейс с его данными.
Для объявления класса служит ключевое слово class.
Упрощенная форма определения класса имеет следующий вид:

129
Язык программирования Kotlin

сlass имя_класса {
Свойство1
Свойство2
...
СвойствоN
Метод1
Метод2
...
Метод N
}

Данные, определенные в классе, называются свойствами экземпляра.


Код содержится в теле методов. Вместе со свойствами методы, опреде-
ленные в классе, называются членами класса. В большинстве
классов действия над свойствами и доступ к ним осуществляют мето-
ды, определенные в этом классе. Таким образом, именно методы, как
правило, определяют порог использования данных класса.
Свойства, определенные в классе, называются свойствами экземпля-
ра, поскольку каждый экземпляр класса содержит собственные копии
этих свойств. Таким образом, данные одного экземпляра отделены и от-
личаются от данных другого объекта.
Все методы класса имеют ту же самую общую форму, что и метод
main(), упоминавшийся ранее и используемый в примерах.

ОБЪЯВЛЕНИЕ КЛАССА
Классы в Kotlin объявляются с помощью использования ключевого сло-
ва class:

class Invoice {
}

Объявление класса состоит из имени класса, заголовка (указания


типов его параметров, первичного конструктора и т.п) и тела класса,
заключенного в фигурные скобки. И заголовок, и тело класса являют-
ся необязательными составляющими: если у класса нет тела, фигурные
скобки могут быть опущены.

class Empty

130
Глава 7. Классы и объекты

Классы могу т содержать в себе:

• Конструкторы и инициализирующий блок


• Функции (методы)
• Свойства
• Вложенные классы
• Объявления объектов

Начнем рассмотрение классов с простого примера. Ниже приведен


код класса Box, который определяет три свойства экземпляра: width,
height и depth. Пока класс не содержит методов, мы добавим их
позже.

class Box (val width: Int, val height: Int,


val depth: Int)

Давайте разберем, что же мы здесь написали. Ключевым словом


class мы сказали, что будем создавать класс с именем Box. Далее
в круглых скобках определяется тело первичного конструктора (в Kotlin
у класса может быть несколько конструкторов), который объявляет
внутри класса три свойства width, height и depth.
Как говорилось выше, класс определяет новый тип данных. В дан-
ном случае новый тип данных называется Box. Это имя будет исполь-
зоваться для объявления объектов типа Box. Не следует забывать, что
объявление класса создает только шаблон, но не конкретный объект.
Таким образом, приведенный выше код не приводит к созданию каких-
либо объектов типа Box.
Чтобы действительно создать объект класса Box, нужно воспользо-
ваться оператором присваивания. Для создания экземпляра класса кон-
структор вызывается так, как если бы он был обычной функцией:

val myBox = Box(100, 100, 100)

На заметку Обратите внимание, что в отличие от Java


и других языков программирования в языке Kotlin нет
оператора new перед именем класса при объявлении
объекта.

131
Язык программирования Kotlin

После выполнения этого оператора объект myBox станет экземпля-


ром класса Box. Таким образом класс Box обретет свою «физическую»
форму.
Всякий раз, когда создается объект класса, он содержит собственную
копию всех свойств класса. Таким образом, каждый объект класса Box
будет содержать собственные копии свойств класса width, height,
depth. Для доступа к свойствам служит операция (.) точка. Эта опе-
рация связывает имя объекта с именем свойства. Так, чтобы получить
значение свойства width, нужно выполнить следующий оператор:

boxWidth = myBox.width

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


ства width объекта myBox и присвоить его переменной boxWidth.
Ниже приведен пример программы, использующей класс Box. Ниче-
го полезного она не делает, но демонстрирует сам принцип.

class Box (val width: Int, val height: Int,


val depth: Int)
fun main(args: Array<String>){
val vol: Int
val box = Box(10, 20, 15)
vol = box.width * box.height * box.
depth
println(“Объем равен $vol”)
}

Программа выведет следующий результат:


Объем равен 3000

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


свойств экземпляра. Это значит, что при наличии двух объектов клас-
са Box каждый из них будет содержать собственные копии переменных
width, height и depth. Изменения в переменных экземпляра одно-
го объекта не влияют на переменные другого объекта. Следующий при-
мер демонстрирует это:

class Box (val width: Int, val height: Int,


val depth: Int)
fun main(args: Array<String>){
var vol: Int

132
Глава 7. Классы и объекты

val box1 = Box(10, 20, 15)


val box2 = Box(20, 40, 20)
vol = box1.width * box1.height * box1.
depth
println(“Объем box1 равен $vol»)
vol = box2.width * box2.height * box2.
depth
println(«Объем box2 равен $vol»)
}

Программа выведет следующий результат:


Объем box1 равен 3000
Объем box2 равен 16000

Как видим, данные из объекта box1 полностью изолированы от дан-


ных, содержащихся в объекте box2.

Введение в методы

Обычно классы состоят из двух компонентов: свойств экземпляра


и методов или функций. Общая форма объявления метода (функции)
выглядит следующим образом:

fun имя_функции(список_параметров): тип {


//..
}

где тип обозначает конкретный тип данных, возвращаемый функ-


цией. Он может быть любым допустимым типом данных, в том чис-
ле и типом созданного класса. Если функция не возвращает значение,
то его возвращаемый тип должен быть объявлен как Unit. Для указа-
ния имени служит идентификатор имя_функции. Это может быть лю-
бой допустимый идентификатор, кроме тех, которые уже используются
в текущей области действия. Список_параметров обозначает пары имя
: тип, разделенных запятыми. Если у функции отсутствуют параметры,
то список_параметров оказывается пустым.
Методы, возвращаемый тип которых отличен от Unit, возвращают
значение в соответствии со следующей формой оператора return:

return значение

133
Язык программирования Kotlin

Если посмотреть на пример с классом Box, то нетрудно прийти


к выводу, что класс мог бы улучшить свой функционал, если бы имел,
в дополнение к свойствам, еще и метод расчета объема. В конце концов,
было бы логично, если бы расчет объема выполнялся внутри класса,
без необходимости получать наружу значения свойств width, height
и depth. Добавим такой метод в наш класс Box:

class Box (val width: Int, val height: Int,


val depth: Int) {
fun volume() : Int {
return width * height * depth
}
}

Теперь мы можем переписать нашу программу следующим образом:

fun main(args: Array<String>){


val vol: Int
val box = Box3(10, 20, 15)
vol = box.volume()
println(«Объем равен $vol»)
}

Посмотрим на строку

vol = box.volume()

В этом операторе мы говорим компилятору, что необходимо вызвать


метод класса volume() и результат, который возвращает этот метод,
присвоить переменной vol.
Мы избавили себя от необходимости прямого обращения к свой-
ствам класса width, height и depth. Мы даже можем ничего
не знать о существовании этих свойств и о том, что написано в теле
метода volume(). Нам достаточно знать, что, чтобы получить объем,
нужно вызвать метод volume(). Таким образом мы защитили данные
класса, вынеся работу с ними в соответствующий метод. Это и есть ин-
капсуляция в действии.

134
Глава 7. Классы и объекты

КОНСТРУКТОРЫ
Инициализация всех свойств класса при каждом создании его экзем-
пляра может оказаться утомительным процессом, поэтому объектам
разрешается выполнять собственную инициализацию при их созда-
нии. Такая автоматическая инициализация осуществляется с помощью
конструкторов.
Класс в Kotlin может иметь первичный конструктор (primary
constructor) и один или более вторичный конструктор (secondary
constructors).

Первичный конструктор

Первичный конструктор является частью заголовка класса, его объ-


явление идет сразу после имени класса (и необязательных параметров).

class Person constructor(firstName: String)

Если у первичного конструктора нет аннотаций и модификаторов


видимости, ключевое слово constructor может быть опущено:

class Person(firstName: String)

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


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

class Customer(name: String) {


init {
logger.info(“Customer initialized
with value ${name}”)
}
}

Обратите внимание, что параметры первичного конструктора могут


быть использованы в инициализирующем блоке. Они также могут быть
использованы при инициализации свойств в теле класса:

135
Язык программирования Kotlin

class Customer(name: String) {


val customerKey = name.toUpperCase()
}

В действительности для объявления и инициализации свойств пер-


вичного конструктора в Kotlin есть лаконичное синтаксическое ре-
шение (его мы использовали в примере с классом Box в предыдущем
разделе):

class Person(val firstName: String, val last-


Name: String, var age: Int) {
// ...
}

Свойства, объявленные в первичном конструкторе, могут быть изме-


няемые (определяются ключевым словом var) и неизменяемые (опре-
деляются ключевым словом val).
Если у конструктора есть аннотации или модификаторы видимости,
ключевое слово constructor обязательно, и модификаторы исполь-
зуются перед ним.

class Customer public @Inject


constructor(name: String) { ... }

Подробно модификаторы доступа и аннотации рассматриваются да-


лее в этой главе.

Вторичные конструкторы

Язык Kotlin разрешает объявлять более одного конструктора для


класса. Такие дополнительные конструкторы называются вторичны-
ми. Они объявляются в теле класса с использованием ключевого сло-
ва constructor:

class Person {
constructor(parent: Person) {
parent.children.add(this)
}
}

136
Глава 7. Классы и объекты

Если у класса есть главный (первичный) конструктор, каждый по-


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

class Person(val name: String) {


constructor(name: String, parent: Per-
son) : this(name) {
parent.children.add(this)
}
}

Если в абстрактном классе (абстрактные классы рассматриваются


далее в этой главе) не объявлено никаких конструкторов (первичных
или второстепенных), у этого класса автоматически сгенерируется пу-
стой конструктор без параметров. Видимость этого конструктора будет
public. Если вы не хотите иметь класс с открытым public конструк-
тором, вам необходимо объявить пустой конструктор с соответствую-
щим модификатором видимости:

class DontCreateMe private constructor () {


}

СВОЙСТВА И ПОЛЯ
Как уже говорилось ранее, в языке Kotlin роль данных выполняют свой-
ства класса. Свойства в Kotlin могут быть изменяемые (mutable) и неиз-
меняемые (read-only). Для определения свойств служат ключевые сло-
ва var и val. Чтобы определить неизменяемое свойство, используется
ключевое слово val, для определения изменяемого свойства — var.
Например:

public class Address {


public var name: String = ...
public var street: String = ...
public var city: String = ...
public var state: String? = ...
public var zip: String = ...
}

137
Язык программирования Kotlin

Чтобы воспользоваться свойством, мы просто обращаемся к нему


по имени. В зависимости от текущего контекста может потребовать-
ся уточнить имя свойства префиксом с именем объекта и операцией
точка:

class ClassTest1 {
val title = “Str”
var str: String = this.title
get() = this.toString()
}
fun copyAddress(address: Address): Address {
val result = Address()
result.name = address.name
result.street = address.street
// ...
return result
}

Полная форма определения свойства выглядит следующим образом:

var|val <propertyName>: <PropertyType> [=


<property_initializer>]
[<getter>]
[<setter>]

Сначала идет ключевое слово var или val, далее имя свойства, за-
тем указывается его тип, затем инициализирующий значение и затем
методы getter и setter, которые служат для получения и установки зна-
чения свойства.
Если тип можно вывести из контекста или базового класса, то его
можно не указывать. Ели вы не собираетесь переопределять getter
и setter, их также можно не указывать. Для классов начальная иници-
ализация свойства обязательна. Например, определим изменяемые
свойства:

var allByDefault: Int? = null


set(value) {
field = value
}
var initialized = 1

138
Глава 7. Классы и объекты

Синтаксис объявления неизменяемых свойств отличается от изме-


няемых двумя вещами:
• Свойство начинается с ключевого слова val
• Свойство не должно иметь setter

val allByDefault: Int? = null


val initialized = 1

Вы можете написать пользовательские аксессоры (методы доступа


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

val isEmpty: Boolean


get() = this.size == 0

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


изменяемого свойства:

var stringRepresentation: String = “”


get() = this.toString()
set(value) {
field = value
}

Начиная с Kotlin версии 1.1 вы можете опустить тип свойства, если


он может быть выведен из получателя (getter):

val isEmpty get() = this.size == 0 // has


type Boolean

Если вам нужно изменить область видимости метода доступа или


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

var setterVisibility: String = “abc”


private set // Установить для сеттер
модификатор доступа private
var setterWithAnnotation: Any? = null

139
Язык программирования Kotlin

@Inject set // Добавление аннотации In-


ject к сеттеру

Backing Fields (поля)

Когда мы определяем свойства класса, то на первый взгляд можно


подумать, что они похожи на поля в Java. Однако это не так. Классы
в Kotlin не могут иметь полей. Т.е. переменные, которые вы объявляе-
те внутри класса, только выглядят и ведут себя как поля из Java, хотя
на самом деле являются свойствами, так как для них неявно реализуют-
ся методы get и set. А сама переменная, в которой находится значение
свойства, называется backing field. Однако иногда, при использовании
пользовательских методов доступа, необходимо иметь доступ к backing
field. Для этих целей Kotlin предоставляет автоматическое backing field,
к которому можно обратиться с помощью идентификатора field:

var counter = 0
set(value) {
if (value >= 0) field = value
}

Идентификатор field может быть использован только в методах до-


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

val isEmpty: Boolean


get() = this.size == 0

Если вы хотите предпринять что-то такое, что выходит за рамки вы-


шеуказанной схемы “неявного backing field”, вы всегда можете исполь-
зовать механизм backing property и написать что-то подобное:

private var _table: Map<String, Int>? = null


public val table: Map<String, Int>
get() {
if (_table == null) {

140
Глава 7. Классы и объекты

_table = HashMap()
}
return _table ?: throw
AssertionError(“Set to null by another
thread”)
}

Во всех отношениях такой подход идентичен подходу в Java, по-


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

Константы времени компиляции

Свойства, значение которых известно во время компиляции, могут


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

• Находиться на самом высоком уровне или быть членом объек-


та object
• Быть проинициализированы значением типа String или значе-
нием числового типа
• Не иметь предопределенного геттера

Такие свойства могут быть использованы в аннотациях:

const val SUBSYSTEM_DEPRECATED: String =


“This subsystem is deprecated”
@Deprecated(SUBSYSTEM_DEPRECATED) fun foo()
{ ... }

Свойства с поздней инициализацией

Обычно свойства, объявленные non-null типом, должны быть прои-


нициализированы в конструкторе. Однако довольно часто это неосуще-
ствимо. К примеру, свойства могут быть инициализированы через вне-
дрение зависимостей, в установочном методе юнит-теста или в методе
onCreate в Android. В таком случае вы не можете обеспечить non-null

141
Язык программирования Kotlin

инициализацию в конструкторе, но все равно хотите избежать прове-


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

public class MyTest {


lateinit var subject: TestSubject
@SetUp fun setup() {
subject = TestSubject()
}
@Test fun test() {
subject.method() // объект
инициализирован, проверять на null не нужно
}
}

Такой модификатор может быть использован только с var свой-


ствами, объявленными внутри тела класса (не в главном конструкто-
ре). И только тогда, когда свойство не имеет пользовательских getter
и setter. Тип такого свойства должен быть non-null и не должен быть
примитивным.
Доступ к lateinit свойству до того, как оно проинициализиро-
вано, выбрасывает специальное исключение, которое четко обозначает,
что свойство не было определено.

МЕТОДЫ И ПЕРЕГРУЗКА
МЕТОДОВ
Как уже говорилось ранее, в Kotlin для работы с данными в классах
предназначены методы. Методы — это те же самые функции, объявлен-
ные как члены класса:

class MyClass {
fun bar() {
//..
}
}

142
Глава 7. Классы и объекты

К методам можно применять модификаторы доступа. Если модифи-


катор доступа не указан, то используется модификатор по умолчанию
public:

class MyClass {
protected fun bar() {
//..
}
}

Так как методы — это по сути обычные функции, то к ним примени-


мы все правила, определенные для функций.

Перегрузка методов

В языке Kotlin разрешается в одном классе определять два или бо-


лее метода с одним именем, но с разными объявлениями параметров.
Такой механизм называется перегрузка методов, а сами методы — пе-
регружаемыми. Перегрузка методов является одним из способов под-
держки полиморфизма в Kotlin.

class OverloadinbgClass {
fun method(x: Int){
}
fun method(y: Long){

}
}

При вызове перегружаемого метода для определения нужного его


варианта используется тип и/или количество аргументов метода. По-
этому перегружаемые методы должны отличаться по типу и/или коли-
честву параметров. Возвращаемые типы перегружаемых методов могут
отличаться.

class OverloadingClass {
fun method(x: Int) {
println(«Вызван метод для типа Int
[$x]»)
}

143
Язык программирования Kotlin

fun method(x: Long) {


println(«Вызван метод для типа
Long [$x]»)
}
fun method() {
println(«Вызван метод без
паратров»)
}
fun method(x: Boolean): Boolean {
println(“Вызван метод с return
$x”)
return true
}
}
fun main(args: Array<String>){
val int = 1
val long = 1L
val c = OverloadingClass()
c.method(int)
c.method(long)
c.method()
c.method(true)
}

Программа выведет следующий результат:


Вызван метод для тип Int [1]
Вызван метод для тип Long [1]
Вызван метод без паратров
Вызван метод с return true

Как видно из примера выше, метод method() перегружается четы-


ре раза. В первом случае он принимает параметр с типом Int, во вто-
ром — с типом Long, в третьем вызывается без параметров, в четвер-
том — с типом Boolean и возвращаемым значением типа Boolean.
Перегрузка методов поддерживает полиморфизм, поскольку это
один из способов реализации в Kotlin принципа «один интерфейс, не-
сколько методов».
Перегрузка методов ценна тем, что позволяет обращаться к похо-
жим методам по общему имени. Выбор подходящего метода для кон-
кретной ситуации входит в обязанности компилятора, а программисту
нужно лишь запомнить общее выполняемое действие.

144
Глава 7. Классы и объекты

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


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

КЛАСС STACK
Ранее в этой главе мы рассматривали класс Box, когда разбирали объ-
явление класса. Несмотря на то что класс Box удобен для демонстра-
ции основных элементов класса, его практическая польза невелика. По-
этому возьмем более сложный пример, демонстрирующий потенциал
классов. Для практического применения, изложенного выше рассмо-
трим реализацию стека. Данные хранятся в стеке по принципу: первым
пришел, последним получил пряники. В этом отношении стек можно
сравнить со стопкой тарелок: тарелка, поставленная на стол первой, бу-
дет использована последней. Для управления стеком служат две опе-
рации: размещение в стеке и извлечение из стека. Для того чтобы по-
местить элемент в стек, выполняется операция размещения, а для того
чтобы извелась элемент, нужно выполнить операцию извлечения.

class Stack(val size: Int = 10) {


private val stck = IntArray(size)
private var tos: Int = -1
// Разместить элемент в стеке
fun push(item: Int) : Unit {
if (tos == size - 1)
println(“Стек заполнен»)
else
stck[++tos] = item
}
// Извлечь элемент из стека
fun pop(): Int {

145
Язык программирования Kotlin

if (tos < 0) {
println(“Стек пуст»)
return 0
}
return stck[tos--]
}
}

Как видно из примера, в классе Stack определены три элемента


данных: size, stck, tos и два метода: push() и pop(). Стек це-
лочисленных значений хранится в массиве stck, размер которого за-
дается в первичном конструкторе. Этот массив индексируется по пере-
менной tos, которая инициализируется значением -1, обозначающим
пустой стек. Функция push() размещает элемент в стеке, а функция
pop() извлекает элемент из стека.
Применение класса Stack демонстрируется в приведенном ниже
примере:

fun main(args: Array<String>){


val size = 5
val stack = Stack(size)
for (i in 0..size - 1)
stack.push(i)
println(“Содержимое стека:»)
for (i in 0..size - 1)
println(stack.pop())
}

Эта программа выводит следующий результат:


Содержимое стека:
4
3
2
1
0

Обратите внимание, что свойства класса stck и tos объявлены


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

146
Глава 7. Классы и объекты

к данным в этих свойствах осуществляется через методы push()


и pop(). Это прямая демонстрация принципа инкапсуляции объек-
тно-ориентированного программирования.

МОДИФИКАТОРЫ ДОСТУПА
Инкапсуляция связывает данные с манипулирующим ими кодом.
Но она также предоставляет еще одно важное средство- управление до-
ступом. Инкапсуляция позволяет управлять доступом к членам класса
из отдельных частей программы, а следовательно, предотвращать злоу-
потребления со стороны управляющего кода. Например, предоставляя
доступ к данным только с помощью ряда методов, можно предотвра-
тить злоупотребление этими данными и обеспечить защиту данных.
Способ доступа к члену класса определяется модификатором досту-
па, присутствующим в его объявлении.
Классы, объекты, интерфейсы, конструкторы, функции, свойства
и их сеттеры могут иметь модификаторы доступа (у геттеров всегда та-
кая же видимость, как у свойств, к которым они относятся). В Kotlin
предусмотрено четыре модификатора доступа: private, protected,
internal и public. Если явно не используется никакого модифика-
тора доступа, то по умолчанию применяется public. Модификаторы
доступа позволяют задать допустимую область видимости для соответ-
ствующих объектов, то есть контекст, в котором можно употреблять
данный объект.
В Kotlin используются следующие модификаторы доступа:

public: публичный, общедоступный класс или член класса. Поля


и методы, объявленные с модификатором public, видны другим клас-
сам из текущего пакета и из внешних пакетов

private: закрытый класс или член класса, противоположность мо-


дификатору public. Закрытый класс или член класса доступен только из
кода в том же классе

protected: такой класс или член класса доступен из любого места


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

internal: видимость будет распространяться на весь модуль

147
Язык программирования Kotlin

Пакеты

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


объектов, определенных в этих пакетах. Для этого объект должен быть
помечен соответствующим модификатором доступа.
Функции, свойства, классы, объекты и интерфейсы могут быть объ-
явлены на самом «высоком уровне» прямо внутри пакета:

package foo
fun baz() {}
class Bar {}

• Если вы не укажете никакого модификатора доступа, будет ис-


пользован public. Это значит, что весь код данного объявления
будет виден из космоса
• Если вы пометите объявление словом private, оно будет иметь
видимость только внутри файла, где было объявлено
• Если вы используете internal, видимость будет распростра-
няться на весь модуль
• protected запрещено использовать в объявлениях «высокого
уровня»

package foo
private fun foo() {} // имеет видимость
внутри файла
public var bar: Int = 5 // свойство видно
с обратной стороны луны
private set // сеттер видно только
внутри файла
internal val baz = 6 // имеет видимость
внутри модуля

Классы и интерфейсы

Классы, интерфейсы, объекты и свойства в языке Kotlin могут иметь


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

148
Глава 7. Классы и объекты

protected — то же самое, что и private + видимость в подклассах

internal — любой клиент внутри модуля, который видит объяв-


ленный класс, видит и его internal члены

public — любой клиент, который видит объявленный класс, видит


его public члены

На заметку К сведению программистов на Java: в Kotlin


внешний класс не видит private члены своих вложенных
классов.

Если вы переопределите protected член и явно не укажете его ви-


димость, переопределенный элемент также будет иметь модификатор
доступа protected.
Ниже приведен код с примером видимости членов класса:

open class Outer {


private val a = 1
protected open val b = 2
internal val c = 3
val d = 4 // public по умолчанию

protected class Nested {


public val e: Int = 5
}
}
class Subclass : Outer() {
// a не видно
// b, c и d видно
// класс Nested и e видно
override val b = 5 // ‘b’ - protected
}
class Unrelated(o: Outer) {
// o.a, o.b не видно
// o.c и o.d видно (тот же модуль)
// Outer.Nested не видно, и Nested::e
также не видно
}

149
Язык программирования Kotlin

Первичные конструкторы

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


ется следующий синтаксис:

class C private constructor(a: Int) { ... }

Обратите внимание, что если вы указываете модификатор видимо-


сти первичного конструктора, то вы также должны добавить ключевое
слово constructor.
В этом примере конструктор является private. По умолчанию все
конструкторы имеют модификатор доступа public, то есть видны вез-
де, где виден сам класс. Исключение составляет конструктор internal
класса, который видно только в том же модуле.

private — означает видимость только внутри этого класса

protected — то же самое, что и private + видимость в субклассах

internal — любой клиент внутри модуля, который видит объяв-


ленный класс, видит и его internal конструктор

public — любой клиент, который видит объявленный класс, видит


его public конструктор

Локальные объявления

Локальные переменные, функции и классы не могут иметь модифи-


каторов доступа.

Модуль

Модификатор доступа internal означает, что этот член видно


в рамках его модуля. Модуль — это набор скомпилированных вместе
Kotlin файлов:
• модуль в IntelliJ IDEA
• Maven или Gradle проект
• набор скомпилированных вместе файлов с одним способом вы-
зова <kotlinc> задачи в Ant

150
Глава 7. Классы и объекты

ИНТЕРФЕЙСЫ
Kotlin предоставляет возможность полностью абстрагировать интер-
фейс класса от его реализации, называемую интерфейсы. С помо-
щью этого механизма можно указать, что именно должен понять класс,
но не как это делать. Интерфейсы в Kotlin очень похожи на интерфей-
сы в Java 8. Они могут содержать абстрактные методы, методы с реали-
зацией. Главное отличие интерфейсов от абстрактных классов заключа-
ется в невозможности хранения переменных экземпляров. Они могут
иметь свойства, но те должны быть либо абстрактными, либо предо-
ставлять реализацию методов доступа. Для создания интерфейса слу-
жит ключевое слово interface:

interface MyInterface {
fun bar()
fun foo() {
// необязательное тело
}
}

Синтаксически интерфейсы аналогичны классам, но, как правило,


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

class Child : MyInterface1, MyInterface2 {


override fun bar() {
// тело
}
}

Чтобы реализовать интерфейс, в классе должен быть создан пол-


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

151
Язык программирования Kotlin

Интерфейсы, так же, как и классы, могут иметь иерархическую


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

модификатор_доступа interface имя_интерфейса


{
свойство_интерфейса
...

имя_метода
}

Если определение интерфейса не содержит никакого модификатора


доступа, то используется доступ по умолчанию public.

Методы в интерфейсах

В языке Kotlin в интерфейсе можно определить не только то, что


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

Свойства в интерфейсах

Вы можете объявлять свойства в интерфейсах. Свойство, объявлен-


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

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

152
Глава 7. Классы и объекты

val propertyWithImplementation: String


get() = “foo”
fun foo() {
print(prop)
}
}
class Child : MyInterface {
override val prop: Int = 29
}

Реализация интерфейсов

Как только интерфейс определен, он может быть реализован в од-


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

interface I1 {
fun bar()
}
interface I2 {
fun foo(): Int
}
interface II: I1, I2 {
override fun bar()
override fun foo(): Int
}
class C1: II {
override fun bar() {}
override fun foo(): Int {
//..
}
}

Обратите внимание, что сигнатура типа реализующего метода долж-


на в точности совпадать с сигнатурой типа, указанного в определении
интерфейса.

153
Язык программирования Kotlin

Вполне допустима и достаточно распространена ситуация, когда


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

interface Callback {
fun callback(param: Int)
}
class Client: Callback {
override fun callback(param: Int){
println(“Метод callback() вызван
со значением $param”)
}
fun nonIFaceMethod() {
println(«В классах, реализующих
интерфейсы, могут определяться и другие
методы»)
}
}
fun main(args: Array<String>){
val cli = Client()
cli.callback(123)
cli.nonIFaceMethod()
}

Программа выведет:

Метод callback() вызван со значением 123


В классах, реализующих интерфейсы, могут
определяться и другие методы

Доступ к реализациям через


ссылки на интерфейсы

Переменные можно объявлять как ссылки на объекты, в которых ис-


пользуется тип интерфейса, а не тип класса. Например:

val cli: Callback = Client()

С помощью такой переменной можно ссылаться на любой экземпляр


какого угодно класса, реализующего объявленный интерфейс. При

154
Глава 7. Классы и объекты

вызове метода по одной из таких ссылок нужный вариант будет выби-


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

interface Callback {
fun callback(param: Int)
}
class Client: Callback {
override fun callback(param: Int){
println(“Метод callback() вызван
со значением $param”)
}
fun nonIFaceMethod() {
println(«В классах, реализующих
интерфейсы, могут определяться и другие
методы»)
}
}
fun main(args: Array<String>){
val cli: Callback = Client()
cli.callback(123)
// cli.nonIFaceMethod() Нельзя вызвать
метод, так как в интерфейсе подобного нет
}

Частичная реализация

Если класс включает в себя интерфейс, но не полностью реализует


определенные в интерфейсе методы, он должен быть объявлен как аб-
страктный при помощи модификатора abstract:

abstract class AbstractClient: Callback {


fun show() {
println(“Этот клас должен быть
объявлен как абстрактный”)

155
Язык программирования Kotlin

}
}

В данном примере кода класс AbstractClient не реализует ме-


тод callback(), поэтому он должен быть объявлен как абстрактный,
иначе вы получите ошибку на этапе компиляции. Любой класс, насле-
дующий от класса AbstractClient, должен либо реализовать ме-
тод callback(), либо быть также объявленным с модификатором
abstract.

Вложенные интерфейсы

Интерфейс может быть объявлен членом класса или другого ин-


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

class A {
public interface NestedIF {
fun bar()
}
}
class B: A.NestedIF {
override fun bar(){
println(“Пример вложенного
интерфейса»)
}
}
fun main(args: Array<String>){
val b = B()
b.bar()
}

Переменные в интерфейсах

Интерфейсы можно применять для импорта совместно исполь-


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

156
Глава 7. Классы и объекты

интерфейса, который содержит свойства, инициализированные нуж-


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

import java.util.Random
interface SharedConsts {
val NO get() = 1
val YES get() = 2
val MAYBE get() = 3
}
class Question: SharedConsts {
val rand = Random()
fun ask(): Int {
val prob: Int = rand.nextInt(100)
when (prob) {
in 0..33 -> return NO
in 34..67 -> return YES
else -> return MAYBE
}
}
}
class AskMe: SharedConsts {
fun answer(res: Int) {
when(res) {
NO -> println(“НЕТ»)
YES -> println(«ДА»)
else -> println(“ВОЗМОЖНО»)
}
}
}
fun main(args: Array<String>){
val q = Question()
val a = AskMe()
a.answer(q.ask())
a.answer(q.ask())
a.answer(q.ask())
a.answer(q.ask())

157
Язык программирования Kotlin

a.answer(q.ask())
}

Методы по умолчанию

Как уже говорилось раньше, в языке Kotlin в определении интер-


фейса можно указывать методы с реализацией. Их называют методами
по умолчанию. Используя эту особенность языка, можно реализовы-
вать «необязательные методы». Например, в интерфейсе можно опре-
делить группу методов, работающих с последовательностью элементов.
Один из методов можно назвать remove() и предназначить его для
удаления элемента. Но если интерфейс предназначен для поддержки
как изменяемых последовательностей, так и неизменяемых, то во вто-
ром случае метод remove() теряет свою актуальность и оказывается,
по существу, необязательным, поскольку его нельзя применить к неиз-
меняемой последовательности. Реализацию такого метода, не выпол-
няющего никаких действий или выбрасывающего исключение, можно
указать в интерфейсе. Благодаря этому отпадает необходимость реали-
зовывать в классе замещающий метод.

interface InterfaceWithDefaultMethods {
fun bar(){
println(«Это выполнился метод
по умолчанию»)
}
}
class ClassIDM: InterfaceWithDefaultMethods
{
fun foo(){
println(“Это выполнился метод
класса”)
}
}
fun main(args: Array<String>){
val c = ClassIDM()
c.bar()
c.foo()
}

158
Глава 7. Классы и объекты

Устранение противоречий
при переопределении

Когда мы объявляем большое количество типов в списке нашего су-


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

interface A {
fun foo() { print(“A”) }
fun bar()
}
interface B {
fun foo() { print(“B”) }
fun bar() { print(“bar”) }
}
class C : A {
override fun bar() { print(“bar”) }
}
class D : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}
}

Оба интерфейса, A и B, объявляют функции foo() и bar(). Оба


реализуют foo(), но только B содержит реализацию bar() (bar()
не отмечен как абстрактный метод в интерфейсе A потому, что в интер-
фейсах это подразумевается по умолчанию, если у функции нет тела).
Теперь, если мы унаследуем какой-нибудь класс C от A, нам, очевид-
но, придется переопределять bar(), обеспечивать его реализацию.
А если мы унаследуем D от A и B, нам не надо будет переопределять
bar(), потому что мы унаследовали только одну его имплементацию.
Но мы получили в наследство две имплементации foo(), поэтому
компилятору не известно, какую выбрать. Он заставит нас переопреде-
лить функцию foo() и явно указать, что мы имели в виду.

159
Язык программирования Kotlin

НАСЛЕДОВАНИЕ
Одним из основополагающих принципов объектно-ориентирован-
ного программирования является наследование, поскольку оно по-
зволяет создавать иерархические классификации. Используя насле-
дование, можно создать класс, который определяет характеристики,
общие для набора связанных элементов. Затем этот общий класс мо-
жет наследоваться другими, более специализированными класса-
ми, каждый из которых будет добавлять свои особые характеристики.
В терминологии Kotlin наследуемый класс называется суперклассом,
а наследующий — подклассом.
Чтобы наследовать класс, достаточно ввести определение суперклас-
са в определяемый класс. Рассмотрим небольшой пример:

open class A (val i: Int, val j: Int) {


open fun show(){
println(“i и j: $i, $j»)
}
}
class B(val k: Int): A(5, 6) {
override fun show(){
super.show()
println(“k: $k”)
println(“Сумма всех значений i + j
+ k: ${i + j + k}»)
}
}
fun main(args: Array<String>){
val c = B(7)
c.show()
}

Программа выведет следующий результат:


i и j: 5, 6
k: 7
Сумма всех значений i + j + k: 18

В приведенном выше примере создается суперкласс A и подкласс B.


Как видим из результата, класс B включает в себя все члены своего

160
Глава 7. Классы и объекты

суперкласса. Об этом нам говорит сумма всех значений. И мы можем


ссылаться непосредственно на свойства класса A из класса B.
Несмотря на то что класс А является суперклассом для класса В, он
в то же время остается полностью независимым и самостоятельным
классом. То, что один класс является суперклассом для другого клас-
са, совсем не исключает возможность его самостоятельного использо-
вания. Более того, один подкласс может быть суперклассом для другого
класса. Таким образом мы можем получить иерархию классов.
Для каждого создаваемого подкласса можно указать только один су-
перкласс. В Kotlin не поддерживается наследование нескольких супер-
классов в одном подклассе. Также ни один из классов не может стать су-
перклассом для самого себя.

Класс Any

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


ется класс Any. Он также является родительским классом для любого
класса, в котором не указан какой-либо другой родительский класс:

class Example

У класса Any нет никаких членов кроме методов: equals(),


hashCode()и toString().

Объявление суперкласса и подкласса

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


двоеточия в оглавлении класса.

open class Base(p: Int)


class Derived(p: Int) : Base(p)

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

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


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

161
Язык программирования Kotlin

Если у класса нет первичного конструктора, тогда каждый последу-


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

class MyView : View {


constructor(ctx: Context) : super(ctx)
{
}
constructor(ctx: Context, attrs: Attri-
buteSet) : super(ctx, attrs) {
}
}

Доступ к членам класса и наследование

Несмотря на то что подкласс включает в себя все члены своего су-


перкласса, он не может иметь доступ к тем членам суперкласса, кото-
рые объявлены как private.

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

В отличие от Java Kotlin требует четкой аннотации и для членов, ко-


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

open class Base {


open fun v() {}
fun nv() {}
}
class Derived() : Base() {
override fun v() {}
}

Ключевое слово open является противоположностью слову final


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

162
Глава 7. Классы и объекты

Для Derived.v() необходима аннотация override. В случае ее


отсутствия компилятор выдаст ошибку. Если у функции типа Base.
nv() нет аннотации open, объявление метода с такой же сигнатурой
в производном классе невозможно, с override или без. В final
классе (классе без аннотации open) запрещено использование аннота-
ции open для его членов.
Член класса, помеченный override, является сам по себе open,
таким образом, он может быть переопределен в производных классах.
Если вы хотите запретить возможность переопределения такого члена,
используйте final:

open class AnotherDerived() : Base() {


final override fun v() {}
}

Устранение неоднозначности

В Kotlin правила наследования имплементации определены следую-


щим образом: если класс перенимает большое количество имплемен-
таций одного и того же члена от ближайших родительских классов, он
должен переопределить этот член и обеспечить свою собственную им-
плементацию (возможно, используя одну из унаследованных). Для того
чтобы отметить супертип (родительский класс), от которого мы унасле-
довали данную имплементацию, мы используем ключевое слово super.
Для уточнения имени родительского супертипа используются треуголь-
ные скобки, например, super<Base>:

open class A {
open fun f() { print(“A”) }
fun a() { print(“a”) }
}
interface B {
fun f() { print(“B”) } // interface mem-
bers are ‘open’ by default
fun b() { print(“b”) }
}
class C() : A(), B {
// The compiler requires f() to be over-
ridden:
override fun f() {

163
Язык программирования Kotlin

super<A>.f() // call to A.f()


super<B>.f() // call to B.f()
}
}

У нас не возникнет никаких проблем с a() и b() в том случае, если C


унаследует только одну имплементацию этих функций. Но для f() у нас
есть две имплементации, унаследованные классом C, поэтому необхо-
димо переопределить f() в C и обеспечить нашу собственную реализа-
цию этого метода для устранения получившейся неоднозначности.

Переменная суперкласса может


ссылаться на объект подкласса

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


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

open class ClassA {


override fun toString(): String {
return “I’am ClassA”
}
}
class ClassB: ClassA() {
override fun toString(): String {
return “I’am ClassB”
}
}
fun main(args: Array<String>){
var c = ClassA()
var b = ClassB()
println(c)
c = b
println(c)
}

Программа выведет следующий результат:


I’am ClassA
I’am ClassB

164
Глава 7. Классы и объекты

В данном примере переменная с содержит ссылку на объект клас-


са ClassA, а переменная b — на объект класса ClassB. А поскольку
ClassB — это подкласс от ClassA, то переменной с можно присвоить
ссылку на ClassB.

Ключевое слово super

Иногда нам необходимо обращаться к свойствам и методам су-


перкласса из подкласса. Для этого служит ключевое слово super.
У ключевого слова super есть две общие формы. Первая форма слу-
жит для вызова конструктора суперкласса, а вторая — для обращения
к членам суперкласса.
О вызов конструктора суперкласса уже говорилось выше. Теперь
рассмотрим второе применение super — для доступа к членам класса.
Вторая форма ключевого слова super действует подобно клю-
чевому слову this, но в отличие от него ссылается на суперкласс,
а не на объект этого класса. В общем виде эта форма выглядит следую-
щим образом:

super.член_класса

Член_класса в данном случае — это свойство или метод класса.


Вторая форма применения ключевого слова super наиболее пригод-
на в тех случаях, когда имена членов подкласса скрывают члены супер-
класса с такими же именами. Рассмотрим следующий простой пример:

open class A {
open val i: Int = 1
open fun show(){
println(“Функция show()
суперкласса»)
}
}
class B: A() {
override val i: Int = 2
override fun show(){
super.show()
println(“Функция show() подкласса»)
println(«Член i в суперклассе:
${super.i}»)

165
Язык программирования Kotlin

println(«Член i в подклассе:
${this.i}»)
}
}
fun main(args: Array<String>){
val c = B()
c.show()
}

Эта программа выведет следующий результат:


Функция show() суперкласса
Функция show() подкласса
Член i в суперклассе: 1
Член i в подклассе: 2

Хотя переменная i из класса В скрывает переменную экземпляра i


из класса А, ключевое слово super позволяет получить доступ к пере-
менной i, определенной в суперклассе. Таким же образом мы получили
доступ к методу show().

АБСТРАКТНЫЕ КЛАССЫ
Иногда суперкласс требуется определить таким образом, чтобы за-
декларировать в нем структуру заданной абстракции, не предостав-
ляя полную реализацию каждого метода. Для этого в языке Kotlin слу-
жит специальный модификатор, который позволяет определять классы
и методы как абстрактные.
Класс и некоторые его члены могут быть объявлены как abstract.
Абстрактный член не имеет реализации в его классе. Обратите внима-
ние, что нам не надо аннотировать абстрактный класс или функцию
словом open — это подразумевается и так.
В языке Kotlin можно переопределить не абстрактный open член
абстрактным.

abstract class A {}
open class Base: A() {
open fun f() {}
}
abstract class Derived : Base() {

166
Глава 7. Классы и объекты

override abstract fun f()


}

Любой класс, содержащий один или больше абстрактных методов,


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

abstract class absA {}


open class Base: absA() {
open fun f() {}
}
fun main(args: Array<String>){
val a: absA
val b = Base()
a = b
a.f()
}

КЛАССЫ ДАННЫХ
Нередко нам приходится создавать классы, единственным назначением
которых является хранение данных. Функционал таких данных зависит
от самих данных, которые в них хранятся. Как правило, при создании
таких классов нам необходимо предусмотреть хотя бы самый мини-
мальный набор методов для работы с данными. И мы создаем такие
методы, как: equals, hashCode, toString, copy, setters
и getters. Обычно все методы во всех создаваемых data-классах де-
лают одно и то же. Как было бы круто, если бы такие рутинные зада-
чи, как создание подобных методов, взял на себя кто-то другой. Язык
Kotlin берет на себя эту обязанность, а нам взамен дает ключевое слово

167
Язык программирования Kotlin

data, которым мы должны отметить класс, чтобы компилятор Kotlin


сделал за нас всю работу по созданию вышеозначенных методов.

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


Int)

Такой класс называется классом данных. Компилятор автоматически


создает следующие методы, исходя из свойств, объявленных в основ-
ном конструкторе:
• пара функций equals()/hashCode()

toString() в форме «User(name=Jhon, age=42)»

• функции componentN(), которые соответствуют свойствам,


в зависимости от их порядка объявления
• функция copy()
Ниже представлен пример, демонстрирующий данную возможность
языка Kotlin:

data class Customer(val name: String, val


age: Int)
fun main(args: Array<String>){
val a = Customer(“Петя», 25)
val c = Customer(“Вася», 25)
val b: Customer
val d: Customer
println(“hasCode: “ + c.hashCode())
println(“toString: “ + c.toString())
b = c
println(“B эквивалентен С: “ + (b ==
c))
println(“А эквивалентен В: “ + (a ==
b))
d = a.copy(age = 30)
println(«А имеет значения: « +
a.toString())
println(«D имеет значения: « +
d.toString())
}

Программа выведет следующий результат:

168
Глава 7. Классы и объекты

hasCode: 995325581
toString: Customer(name=Вася, age=25)
B эквивалентен С: true
А эквивалентен В: false
А имеет значения: Customer(name=Петя, age=25)
D имеет значения: Customer(name=Петя, age=30)
Как видим из приведенного примера, нам не нужно заботиться о соз-
дании методов equals(), toString(), hashCode() и copy(). Эту
работу выполнил за нас компилятор Kotlin.
Начиная с версии 1.1 классы данных могут расширять другие классы.

Копирование

Выше в примере мы использовали метод copy(). Скажем несколь-


ко слов об этом методе класса данных.
Довольно часто нам приходится копировать объект с изменением
только некоторых его свойств. Для этой задачи генерируется функция
copy(). Для написанного выше класса Customer такая реализация
будет выглядеть следующим образом:

fun copy(name: String = this.name, age: Int


= this.age) = User(name, age)

Это позволит нам писать следующий код:

val jack = User(name = “Jack”, age = 1)


val olderJack = jack.copy(age = 2)

Функции componentN(), мультидекларации


и деструктуризация

Выше мы говорили о том, что компилятор Kotlin генерирует для


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

169
Язык программирования Kotlin

val jane = Customer(“Jane”, 35)


val (name, age) = jane
println(“$name, $age years of age”) //
выводит «Jane, 35 years of age»

Синтаксис вида val (name, age) = person называется де-


структуризирующее присвоение. Он позволяет присвоить объект сразу
нескольким переменным, разбив его на части. Мы объявили две пере-
менные: name и age — и теперь можем использовать их по отдельности.
Декларация вида:

val (name, age) = person

транслируется в код:

val name = person.component1()


val age = person.component2()

Вы можете сами создавать функции componentN(). Они могут


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

class DestructuringClass(val name: String,


val age: Int) {
operator fun component1(): String{
return name
}
operator fun component2(): Int{
return age
}
}
fun main(args: Array<String>){
val c = DestructuringClass(“Петя», 25)
val (name, age) = c
println(“Деструктуризация объекта”)
println(“Значение name: $name”)
println(“Значение age: $age”)
}

170
Глава 7. Классы и объекты

Программа выводит следующий результат:


Деструктуризация объекта
Значение name: Петя
Значение age: 25

Заметьте, что функции componentN() нужно отмечать ключевым


словом operator, чтобы позволить их использование в деструктури-
зирующем присваивании.
Деструктуризирующие присваивания также работают в циклах for:

for ((a, b) in collection) { ... }

В данном примере значения переменных a и b возращены метода-


ми component1() и component2(), вызванными неявно у элемен-
тов коллекции.
Рассмотрим еще один пример. Допустим, у нас есть функция, кото-
рая проводит ряд вычислений и потом должна вернуть два значения,
например, результат и какой-то статус. В языке Kotlin это решается
не просто, а очень просто, с использованием дата-классов и деструкту-
ризации. Решение в данном случае будет выглядеть так:

data class Result(val result: Int, val sta-


tus: Status)
fun function(...): Result {
// вычисления

return Result(result, status)


}
// Теперь мы можем использовать
деструктуризирующее присваивание:
val (result, status) = function(...)

Деструктуризация и цикл for – это, пожалуй, самый оптимальный


способ итерации по ассоциативному списку:

for ((key, value) in map) {


// do something with the key and the
value
}

Чтобы это работало, необходимо:

171
Язык программирования Kotlin

• представить ассоциативный список как последовательность зна-


чений, предоставив функцию iterator()
• представить каждый элемент как пару с помощью функций
component1() и component2()

Стандартная библиотека Kotlin предоставляет такие расширения


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

Мультидекларации

Выше был приведен пример такого вот кода:

val (name, age) = person

Как видно из определения, одним оператором val объявлено сразу


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

ИЗОЛИРОВАННЫЕ КЛАССЫ
Изолированные классы используются для отражения ограниченных ие-
рархий классов, когда значение может иметь тип только из ограничен-
ного набора, и никакой другой. Они являются, по сути, расширением
enum-классов: набор значений enum-типа также ограничен, но каждая
enum-константа существует только в единственном экземпляре, в то
время как наследник изолированного класса может иметь множество
экземпляров, которые могут нести в себе какое-то состояние.
Чтобы описать изолированный класс, укажите модификатор sealed
перед именем класса. Изолированный класс может иметь наследников,
но все они должны быть объявлены в том же файле, что и сам изоли-
рованный класс. Рассмотрим два примера, один для версии Kotlin 1.0
и один для версии Kotlin 1.1

// Kotlin 1.0
sealed class Car {

172
Глава 7. Классы и объекты

class Maruti(val speed: Int) : Car()


class Bugatti(val speed: Int, val
boost: Int) : Car()
object NotACar : Car()
}
fun speed(car: Car): Int = when (car) {
is Car.Maruti -> car.speed
is Car.Bugatti -> car.speed + car.boost
Car.NotACar -> 0
}
fun main(args: Array<String>){
val m = Car.Maruti(200)
val b = Car.Bugatti(250, 100)
val n = Car.NotACar
println(“Maruti speed: “ + speed(m))
println(“Bugatti speed: “ + speed(b))
println(“UFO speed: “ + speed(n))
}
// Kotlin 1.1
sealed class Car2
data class Maruti(val speed: Int) : Car2()
data class Bugatti(val speed: Int, val
boost: Int) : Car2()
object NotACar : Car2()
fun speed(car: Car2): Int = when (car) {
is Maruti -> car.speed
is Bugatti -> car.speed + car.boost
NotACar -> 0
}
fun main(args: Array<String>){
val m = Maruti(200)
val b = Bugatti(250, 100)
val n = NotACar
println(“Maruti speed: “ + speed(m))
println(“Bugatti speed: “ + speed(b))
println(“UFO speed: “ + speed(n))
}

Обе программы выводят следующий результат:


Maruti speed: 200
Bugatti speed: 350

173
Язык программирования Kotlin

UFO speed: 0

Как видим, Kotlin 1.1 привнес нам возможность объявлять подклас-


сы изолированных классов вне класса.
Ключевое преимущество от использования изолированных классов
проявляется тогда, когда вы используете их в выражении when. Если
возможно проверить, что выражение покрывает все случаи, то вам
не нужно добавлять else.
Начиная с версии 1.1 изолированные класса могут быть расширены
классами данных.
Стоит обратить внимание, что классы, которые расширяют наслед-
ников изолированного класса (непрямые наследники), могут быть по-
мещены где угодно, не обязательно в том же файле.
Обратите внимание, что для объявления типа NotACar мы исполь-
зовали ключевое слово object. Этим ключевым словом создаются
объекты. О них мы поговорим позднее в этой главе.

ПЕРЕЧИСЛЕНИЯ
В простейшем виде перечисления в Kotlin похожи на перечисления
в других языках программирования, но это поверхностное сходство.
В большинстве языков вроде С++ перечисления являются списками це-
лочисленных констант. В Kotlin же перечисления определяют тип клас-
са. Благодаря тому что в Kotlin перечисления реализованы в виде клас-
сов, они могут иметь конструкторы, методы и свойства.
Перечисления создаются с помощью ключевого слова enum:

enum class Direction {


NORTH, SOUTH, WEST, EAST
}

Идентификаторы NORTH, SOUTH и так далее называются кон-


стантами перечисляемого типа. Каждая из них объявлена как откры-
тый статический конечный член класса Direction. Они относятся к типу
того перечисления, в котором объявлены. Такие константы называются
самотипизированными. При этом префикс «само» относится к охваты-
вающему их перечислению.

174
Глава 7. Классы и объекты

Каждая enum-константа является объектом. При объявлении кон-


станты разделяются запятыми. Так как константы являются экземпля-
рами enum-класса, они могут быть инициализированы:

enum class Color(val rgb: Int) {


RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}

Enum-константы также могут объявлять свои собственные аноним-


ные классы как с их собственными методами, так и с перегруженны-
ми методами базового класса. Следует заметить, что при объявлении
в enum-классе каких-либо членов необходимо отделять их от списка
констант точкой с запятой, так же как и в Java.

enum class ProtocolState {


WAITING {
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}

Enum-классы в Kotlin имеют стандартные методы для вывода списка


объявленных констант и для получения enum-константы по ее имени.
Ниже приведены сигнатуры этих методов:

EnumClass.valueOf(value: String): EnumClass


EnumClass.values(): Array<EnumClass>

Метод values() возвращает массив, содержащий список констант


перечислимого типа. А метод valueOf() возвращает константу пере-
числимого типа, значение которой соответствует символьной строке,
переданной в качестве аргумента.
Метод valueOf() выбрасывает исключение
IllegalArgumentException, если указанное имя не соответствует
ни одной константе, объявленной в классе.

175
Язык программирования Kotlin

Каждая enum-константа имеет поля, в которых содержатся ее имя


и порядковый номер в enum-классе:

val name: String


val ordinal: Int

Также enum-константы реализуют интерфейс Comparable. Поря-


док сортировки соответствует порядку объявления.

Применение

Объявив перечисление, можно создавать переменные этого типа.


Несмотря на то что перечисления определяют тип класса, получать эк-
земпляры класса типа enum нельзя. Вместо этого переменные перечис-
ления объявляются и применяются практически так же, как и перемен-
ные примитивных типов. Рассмотрим небольшой пример:

interface IAppleColor {
fun appleColor(): String
}
enum class Apple(val color: String): IApple-
Color {
Jonathan(“Красный»),
GoldenDel(«Желтый»),
RedDel(«Темно-красный»),
Winesap(“Красно-зеленый”),
Cortland(“Красно-желтый”)
override fun appleColor(): String {
return color
}
}
fun main(args: Array<String>){
val myApple: Apple
val goldenApple = Apple.GoldenDel
val apples: Array<Apple> = arrayOf(
Apple.Jonathan,
Apple.GoldenDel,
Apple.RedDel,
Apple.Winesap,

176
Глава 7. Классы и объекты

Apple.Cortland
)

myApple = goldenApple
val name = when (myApple) {
Apple.Jonathan -> “Джонатан»
Apple.GoldenDel -> «Гольден»
Apple.RedDel -> «Ред Делишес»
Apple.Winesap -> «Сорт Винный»
Apple.Cortland -> «Кортлэнд»
else -> «Это вообще не яблоко»
}
println(«Мое любимое яблоко $name имеет
более сладкий вкус и ${myApple.color} цвет»)
for (apple in apples) {
println(“Яблоко $apple имеет цвет
${apple.color}»)
if (apple == Apple.Winesap) {
println(“Яблоко $apple имеет
кисло-сладкий вкус»)
}
}
}

Эта программа выводит следующий результат:


Мое любимое яблоко Гольден имеет более сладкий
вкус и Желтый цвет
Яблоко Jonathan имеет цвет Красный
Яблоко GoldenDel имеет цвет Желтый
Яблоко RedDel имеет цвет Темно-красный
Яблоко Winesap имеет цвет Красно-зеленый
Яблоко Winesap имеет кисло-сладкий вкус
Яблоко Cortland имеет цвет Красно-желтый

В данном примере, во-первых, мы объявили перечисляемый тип


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

177
Язык программирования Kotlin

определять как с типом непосредственно, так и автоматически выводи-


мым типом:

val myApple: Apple


val goldenApple = Apple.GoldenDel

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


ли предопределенный метод values() перечисляемого типа, который
добавляется автоматически компилятором Kotlin.
Также стоит обратить внимание на то, что константы перечисляемо-
го типа можно проверять на равенство с помощью оператора ==.
Значения перечислимого типа можно использовать в выражении
when. При выводе константы перечисляемого типа, например, методом
print, отображается ее имя.

Перечисления в Kotlin относятся


к типам классов

Как говорилось ранее, перечисление в Kotlin относится к типу клас-


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

Ограничения

Хотя перечисления и ведут себя как классы, на них накладывается


ряд ограничений:

• Перечисление не может наследоваться от другого класса


• Перечисление не может быть суперклассом

178
Глава 7. Классы и объекты

ВЛОЖЕННЫЕ КЛАССЫ
В языке Kotlin классы могут быть определены внутри других классов.
Такие классы называются вложенными. Рассмотрим небольшой пример:

class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
inner class Inner {
fun foo() = bar
}
}
fun main(args: Array<String>){
println(“Значение из вложенного класса:
${Outer.Nested().foo()}”)
println(“Значение внутреннего класса:
${Outer().Inner().foo()}”)
}

Внутри класса Outer мы объявили два вложенных класса: Nested


и Inner. При этом класс Inner помечен ключевым словом inner.
Класс Nested называется вложенным классом. Он живет своей жиз-
нью и не имеет доступа к членам внешнего класса. Вызов такого клас-
са осуществляется с уточнением в виде имени внешнего класса и опера-
тора (.), при этом мы по факту не создаем экземпляр внешнего класса
(мы не указываем круглые скобки после имени внешнего класса):

Outer.Nested().foo()

Класс Inner называется внутренним классом и отличается от вло-


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

Outer().Nested().foo()

179
Язык программирования Kotlin

Внутренние классы содержат ссылку на объект внешнего класса.


Подробнее про ссылки на объекты с использованием ключевого слова
this говорится в главе 12.

Анонимные вну тренние классы

Экземпляры анонимных классов создаются с помощью объектных


выражений:

window.addMouseListener(object: MouseAdapt-
er() {
override fun mouseClicked(e: Mou-
seEvent) {
// ...
}

override fun mouseEntered(e: Mou-


seEvent) {
// ...
}
})

Если объект является экземпляром функционального Java-


интерфейса (т.е. Java-интерфейса с единственным абстрактным мето-
дом), вы можете создать его с помощью лямбда-выражения с префик-
сом — типом интерфейса:

val listener = ActionListener {


println(«clicked») }

ОБЪЕКТЫ
Объекты в языке Kotlin — это вполне себе определенные сущности, ко-
торые являются языковой конструкцией и призваны решить ряд опре-
деленных задач. Объекты в Kotlin создаются с помощью ключевого
слова object. В Kotlin существует три типа объектов: именованные,
анонимные и вспомогательные.
Рассмотрим общую форму создания объекта:

180
Глава 7. Классы и объекты

object [имя_объекта] [: Супертип] {


[свойства_объекта]
[методы_объекта]
}

Как видим, определение объекта предваряет ключевое слово object.


За ним идет имя объекта, супертип объекта и далее в фигурных скобках
тело объекта, в котором определяются его свойства и методы.

Именованные объекты
(объектные декларации)

Именованные объекты позволяют реализовывать очень полез-


ный паттерн программирования, называемый Одиночка (Singleton).
Singleton — это шаблон, гарантирующий, что в приложении будет един-
ственный экземпляр некоторого класса, и предоставляющий глобаль-
ную точку доступа к этому экземпляру.

object DataProviderManager {
fun registerDataProvider(provider: Data-
Provider) {
// ...
}
val allDataProviders:
Collection<DataProvider>
get() = // ...
}

Аналогично объявлению переменной объявление объекта не явля-


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

DataProviderManager.registerDataProvider(...)

Объявление объекта не может иметь локальный характер (т.е. быть


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

181
Язык программирования Kotlin

class MouseEvent
interface MouseAdapter {
fun onMouseClicked(e: MouseEvent) {
}
fun onMouseEntered(e: MouseEvent) {
}
}
object DefaultListener : MouseAdapter {
override fun onMouseClicked(e: MouseEvent)
{
// do nothing
}
override fun onMouseEntered(e: MouseEvent)
{
// do nothing
}
}

Анонимные объекты (объектные выражения)

Иногда нам необходимо получить экземпляр некоторого класса с не-


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

window.addMouseListener(object : MouseAdapt-
er() {
override fun mouseClicked(e: Mou-
seEvent) {
// ...
}
override fun mouseEntered(e: Mou-
seEvent) {
// ...
}
})

В данном примере функция addMouseListener принимает пе-


ременную типа MouseAdapter. Чтобы не создавать дополнительный

182
Глава 7. Классы и объекты

класс, производный от MouseAdapter, мы можем воспользоваться


возможностями языка Kotlin и использовать анонимный объект с су-
пертипом MouseAdapter.
Если у супертипа есть конструктор, то в него должны быть переданы
соответствующие параметры. Множество супертипов может быть ука-
зано через запятую.

open class A(x: Int) {


public open val y: Int = x
}
interface B {...}
val ab: A = object : A(1), B {
override val y = 15
}

Если нам надо создать простой объект без супертипа, то мы можем


написать следующий код:

fun foo() {
val adHoc = object {
var x: Int = 0
var y: Int = 0
}
print(adHoc.x + adHoc.y)
}

Важным является тот факт, что анонимные объекты могут исполь-


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

class C {
// Приватная функция, возвращаемый тип
- анонимный объект
private fun foo() = object {
val x: String = “x”
}
// Public function, so the return type

183
Язык программирования Kotlin

is Any
fun publicFoo() = object {
val x: String = “x”
}
fun bar() {
val x1 = foo().x // Здесь все
работает
val x2 = publicFoo().x // Ошибка:
Unresolved reference ‘x’
}
}

Код внутри объявленного объекта может обращаться к переменным


за скобками:

fun countClicks(window: JComponent) {


var clickCount = 0
var enterCount = 0
window.addMouseListener(object : Mouse-
Adapter() {
override fun mouseClicked(e: Mou-
seEvent) {
clickCount++
}
override fun mouseEntered(e: Mou-
seEvent) {
enterCount++
}
})
// ...
}

Вспомогательные объекты

В отличие от других языков программирования в Kotlin отсутству-


ет возможность создавать статические методы в классах. Но тем не ме-
нее существует способ, который позволяет решить эту проблему с по-
мощью объектов.
Объявление объекта внутри класса может быть отмечено ключевым
словом companion:

184
Глава 7. Классы и объекты

class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}

Для вызова членов такого companion объекта используется имя


класса:

val instance = MyClass.create()

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


для вызова которого не нужно создавать экземпляр класса.
Имя вспомогательного объекта указывать не обязательно. В таком
случае он будет назван Companion:

class MyClass {
companion object {
}
}
val x = MyClass.Companion

Несмотря на то что такие члены вспомогательных объектов выгля-


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

interface Factory<T> {
fun create(): T
}
class MyClass {
companion object : Factory<MyClass> {
override fun create(): MyClass =
MyClass()
}
}

185
Язык программирования Kotlin

Семантическое различие
между видами объектов

Существует одна важная смысловая разница между видами объектов:

• анонимный объект инициализируется сразу после того, как был


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

ДЕЛЕГИРОВАНИЕ
Еще одной привлекательной особенностью языка Kotlin является натив-
ная поддержка такого шаблона программирования, как делегирование.
Делегирование (англ. Delegation) — основной шаблон проектирова-
ния, в котором объект внешне выражает некоторое поведение, но в ре-
альности передает ответственность за выполнение этого поведения
связанному объекту. Шаблон делегирования является фундаменталь-
ной абстракцией, на основе которой реализованы другие шаблоны —
композиция (также называемая агрегацией), примеси (mixins) и аспек-
ты (aspects).
Шаблон делегирования является хорошей альтернативой наследова-
нию, и Kotlin поддерживает его изначально, освобождая вас от необ-
ходимости написания шаблонного кода. Делегирование позволяет из-
менить поведение конкретного экземпляра объекта вместо создания
нового класса путем наследования.

interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b
fun main(args: Array<String>) {
val b = BaseImpl(10)

186
Глава 7. Классы и объекты

Derived(b).print() // prints 10
}

Ключевое слово by в оглавлении Derived, находящееся после


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

Делегированные свойства

Существует несколько основных видов свойств, которые мы реа-


лизуем каждый раз вручную в случае их надобности. Однако намного
удобнее было бы реализовать их раз и навсегда и положить в какую-ни-
будь библиотеку. Примеры таких свойств:
• ленивые свойства (lazy properties): значение вычисляется один
раз, при первом обращении
• свойства, на события об изменении которых можно подписаться
(observable properties)
• свойства, хранимые в ассоциативном списке, а не в отдельных
полях
Для таких случаев Kotlin поддерживает делегированные свойства:

class Example {
var p: String by Delegate()
}

Их синтаксис выглядит следующим образом:

val/var <имя свойства>: <Тип> by <выражение>

Выражение после by — делегат: обращения (get(), set())


к свойству будут обрабатываться этим выражением. Делегат не обязан
реализовывать какой-то интерфейс, достаточно, чтобы у него были ме-
тоды get() и set() с определенной сигнатурой:

187
Язык программирования Kotlin

class Delegate {
operator fun getValue(thisRef: Any?,
property: KProperty<*>): String {
return “$thisRef, спасибо
за делегирование мне ‘${property.name}’!»
}

operator fun setValue(thisRef: Any?,


property: KProperty<*>, value: String) {
println(“$value было присвоено
значению ‘${property.name} в $thisRef.’»)
}
}

Когда мы читаем значение свойства p, вызывается метод


getValue() класса Delegate, причем первым параметром ей пере-
дается тот объект, у которого запрашивается свойство p, а вторым —
объект-описание самого свойства p (у него можно, в частности, узнать
имя свойства). Например:

val e = Example()
println(e.p)

Этот код выведет


Example@33a17727, спасибо за делегирование мне
‘p’!
Похожим образом, когда мы обращаемся к p, вызывается метод
setValue(). Два первых параметра — такие же, как у get(), а тре-
тий — присваиваемое значение свойства:

e.p = «NEW»

Этот код выведет


NEW было присвоено значению ‘p’ в Example@33a17727.

Начиная с версии Kotlin 1.1 вы можете объявлять делегированные


свойства внутри функций или блоков кода, а не только внутри классов.

188
Глава 7. Классы и объекты

Стандартные делегаты

Стандартная библиотека Kotlin предоставляет несколько полезных


видов делегатов:
• Ленивые свойства (lazy properties)
• Наблюдаемые свойства (Observable properties)
• Хранение свойств в ассоциативном списке

Ленивые свойства (lazy properties)

lazy()- это функция, которая принимает


лямбду и возвращает экземпляр класса
Lazy<T>, который служит делегатом для
реализации ленивого свойства: первый вызов
get() запускает лямбда-выражение, переданное
lazy() в качестве аргумента, и запоминает
полученное значение, а последующие вызовы
просто возвращают вычисленное значение.
val lazyValue: String by lazy {
println(“computed!”)
“Hello”
}
fun main(args: Array<String>) {
println(lazyValue)
println(lazyValue)
}

Этот код выведет:


computed!
Hello
Hello

По умолчанию вычисление ленивых свойств синхронизировано: зна-


чение вычисляется только в одном потоке выполнения, и все осталь-
ные потоки могут видеть одно и то же значение. Если синхронизация
не требуется, передайте LazyThreadSafetyMode.PUBLICATION
в качестве параметра в функцию lazy(), тогда несколько потоков
смогут исполнять вычисление одновременно. Или если вы уверены, что
инициализация всегда будет происходить в одном потоке исполнения,

189
Язык программирования Kotlin

вы можете использовать режим LazyThreadSafetyMode.NONE, ко-


торый не гарантирует никакой потокобезопасности.

Наблюдаемые свойства (observable properties)

Функция Delegates.observable() принимает два аргумента:


начальное значение свойства и обработчик (лямбда), который вызыва-
ется при изменении свойства. У обработчика три параметра: описание
свойства, которое изменяется, старое значение и новое значение.

import kotlin.properties.Delegates
class User {
var name: String by Delegates.
observable(“<no name>”) {
prop, old, new ->
println(“$old -> $new”)
}
}
fun main(args: Array<String>) {
val user = User()
user.name = “first”
user.name = “second”
}

Этот код выведет:


<no name> -> first
first -> second
Если вам нужно иметь возможность запретить присваивание не-
которых значений, используйте функцию vetoable() вместо
observable().

Хранение свойств в ассоциативном списке

Один из самых частых сценариев использования делегированных


свойств заключается в хранении свойств в ассоциативном списке. Это
полезно в «динамическом» коде, например при работе с JSON:

class User(val map: Map<String, Any?>) {


val name: String by map

190
Глава 7. Классы и объекты

val age: Int by map


}
val user = User(mapOf(
“name” to “John Doe”,
“age” to 25
))

В этом примере конструктор принимает ассоциативный список. Де-


легированные свойства берут значения из этого ассоциативного списка
(по строковым ключам).

println(user.name) // Prints «John Doe»


println(user.age) // Prints 25

Также, если вы используете MutableMap вместо Map, поддержива-


ются изменяемые свойства (var):

class MutableUser(val map:


MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}

Локальные делегированные свойства

Начиная с версии 1.1 вы можете объявить локальные переменные


как делегированные свойства. Например, вы можете сделать локальную
переменную «ленивой»:

fun example(computeFoo: () -> Foo) {


val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isVal-
id()) {
memoizedFoo.doSomething()
}
}

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


ращении к ней. Если условие someCondition будет ложно, значение
переменной не будет вычислено вовсе.

191
Язык программирования Kotlin

Требования к делегированным свойствам

Для read-only свойства (например, val) делегат должен предо-


ставлять функцию getValue(), которая принимает следующие
параметры:
thisRef — должен иметь такой же тип или быть наследником типа
хозяина свойства (для расширений — тип, который расширяется)
property — должен быть типа KProperty<*> или его родитель-
ского типа. Эта функция должна возвращать значение того же типа, что
и свойство (или его родительского типа)
Для изменяемого свойства (var) делегат должен дополнительно
предоставлять функцию setValue(), которая принимает следующие
параметры:
thisRef — то же, что и у getValue()
property — то же, что и у getValue()
new value — должен быть того же типа, что и свойство (или его
родительского типа)
Функции getValue() и/или setValue() могут быть предостав-
лены либо как члены класса-делегата, либо как его расширения. По-
следнее полезно, когда вам нужно делегировать свойство объекту, ко-
торый изначально не имеет этих функций. Обе эти функции должны
быть отмечены с помощью ключевого слова operator.
Эти интерфейсы объявлены в стандартной библиотеке Kotlin:

interface ReadOnlyProperty<in R, out T> {


operator fun getValue(thisRef: R, prop-
erty: KProperty<*>): T
}
interface ReadWriteProperty<in R, T> {
operator fun getValue(thisRef: R, prop-
erty: KProperty<*>): T
operator fun setValue(thisRef: R, prop-
erty: KProperty<*>, value: T)
}

Правила трансляции свойств

Для каждого делегированного свойства компилятор Kotlin «за


кулисами» генерирует вспомогательное свойство и делегирует его.
Например, для свойства prop генерируется скрытое свойство

192
Глава 7. Классы и объекты

prop$delegate, и исполнение геттеров и сеттеров просто делегиру-


ется этому дополнительному свойству:

class C {
var prop: Type by MyDelegate()
}
// этот код генерируется компилятором:
class C {
private val prop$delegate = MyDele-
gate()
var prop: Type
get() = prop$delegate.
getValue(this, this::prop)
set(value: Type) = prop$delegate.
setValue(this, this::prop, value)
}

Компилятор Kotlin предоставляет всю необходимую информацию


о prop в аргументах: первый аргумент this ссылается на экземпляр
внешнего класса C и this::prop reflection-объект типа KProperty,
описывающий сам prop.

Предоставление делегата

Начиная с версии 1.1 в Kotlin доступно предоставление делега-


та. С помощью определения оператора provideDelegate вы може-
те расширить логику создания объекта, которому будет делегировано
свойство. Если объект, который используется справа от by, определяет
provideDelegate как член или как расширение, эта функция будет
вызвана для создания экземпляра делегата.
Один из возможных вариантов использования provideDelegate —
это проверка состояния свойства при его создании.
Например, если вы хотите проверить имя свойства перед связыва-
нием, вы можете написать что-то вроде:

class ResourceLoader<T>(id: ResourceID<T>) {


operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {

193
Язык программирования Kotlin

checkProperty(thisRef, prop.name)
// создание делегата
}
private fun checkProperty(thisRef:
MyUI, name: String) { ... }
}
fun <T> bindResource(id: ResourceID<T>):
ResourceLoader<T> { ... }
class MyUI {
val image by bindResource(ResourceID.
image_id)
val text by bindResource(ResourceID.
text_id)
}

provideDelegate() имеет те же параметры, что и getValue():


thisRef — должен иметь такой же тип или быть наследником типа
хозяина свойства (для расширений — тип, который расширяется)
property — должен быть типа KProperty<*> или его родительско-
го типа. Эта функция должна возвращать значение того же типа, что
и свойство (или его родительского типа)
Метод provideDelegate() вызывается для каждого свойства
во время создания экземпляра MyUI и сразу совершает необходимые
проверки.
Не будь этой возможности внедрения между свойством и делегатом,
для достижения той же функциональности вам бы пришлось переда-
вать имя свойства явно, что не очень удобно:

// Проверяем имя свойства без «provideDele-


gate»
class MyUI {
val image by bindResource(ResourceID.
image_id, “image”)
val text by bindResource(ResourceID.
text_id, “text”)
}
fun <T> MyUI.bindResource(
id: ResourceID<T>,
propertyName: String
): ReadOnlyProperty<MyUI, T> {
checkProperty(this, propertyName)

194
Глава 7. Классы и объекты

// создание делегата
}

В сгенерированном коде метод provideDelegate() вызывает-


ся для инициализации вспомогательного свойства prop$delegate.
Сравните сгенерированный для объявления свойства код val prop: Type
by MyDelegate() со сгенерированным кодом из Transaction Rules (когда
provideDelegate не представлен):

class C {
var prop: Type by MyDelegate()
}
// этот код будет сгенерирован компилятором
// когда функция ‘provideDelegate’ доступна:
class C {
// вызываем «provideDelegate» для
создания вспомогательного свойства «dele-
gate»
private val prop$delegate = MyDele-
gate().provideDelegate(this, this::prop)
val prop: Type
get() = prop$delegate.
getValue(this, this::prop)
}

Заметьте, что метод provideDelegate влияет только на создание


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

ОБОБЩЕНИЯ
Применение обобщений позволяет создавать классы, интерфейсы и ме-
тоды, работающие безопасными по отношению к типам способом с раз-
нообразными видами данных. Многие алгоритмы логически иден-
тичны, независимо от того, к данным каких типов они применяются.
Например, механизм, поддерживающий стеки, является одним и тем же
в стеках, хранящих элементы типа Int, String, или других. Благо-
даря обобщениям можно определить алгоритм один раз, независимо

195
Язык программирования Kotlin

от конкретного типа данных, а затем применять его к обширному раз-


нообразию типов данных без каких-либо дополнительных усилий.

Что такое обобщения

По существу, обобщения — это параметризованные типы. Такие


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

Простой пример обобщения

Начнем с простого примера обобщенного класса.

data class Gen<out T> (val t: T) {


fun showT() : T {
return t
}
}
fun main(args: Array<String>){
val a = Gen<Int>(100)
val b = Gen<Double>(100.0)
val c = Gen<String>(“Привет»)
println(«Тип для a: « + a.t::class.
java.simpleName + « значение t: ${a.t}»)
println(«Тип для b: « + b.t::class.
java.simpleName + « значение t: ${b.t}»)
println(«Тип для c: « + c.t::class.
java.simpleName + « значение t: ${c.
showT()}»)
}

Результат выполнения программы:


Тип параметра для a: Integer значение t: 100
Тип параметра для b: Double значение t: 100.0
Тип параметра для c: String значение t: Привет

196
Глава 7. Классы и объекты

Давайте проанализируем эту программу. Начнем с объявления клас-


са Gen:

data class Gen<T> (val t: T)

<T> обозначает имя параметра типа. Это имя используется в каче-


стве заполнителя, вместо которого в дальнейшем подставляется имя
конкретного типа, передаваемого классу Gen при создании объекта.
Это означает, что обозначение Т применяется в классе Gen всякий раз,
когда требуется параметр типа. Обратите внимание, что обозначение Т
заключено в угловые скобки (<>). Всякий раз, когда объявляется пара-
метр типа, он указывается в угловых скобках.
Далее тип Т используется для объявления свойства класса t.

val t: T

Как мы говорили выше, параметр типа Т — это место для подста-


новки конкретного типа, который указывается в дальнейшем при соз-
дании объекта класса Gen. Это означает, что свойство t получит тот
тип, который будет передан в качестве параметра типа Т.
Параметр типа также может быть указан в качестве типа, возвраща-
емого методом. Посмотрите на код метода showT() класса Gen:

fun showT() : T {...}

Теперь рассмотрим применение нашего класса Gen. В функции


main мы создаем три объекта класса Gen с разными параметрами ти-
пов: Int, Double, String:

val a = Gen<Int>(100)
val b = Gen<Double>(100.0)
val c = Gen<String>(“Привет»)

Стоит обратить внимание, что Kotlin умеет выводить тип из контек-


ста, поэтому объявления объектов можно было указать без конкрети-
зации типа:

val a = Gen(100)
val b = Gen(100.0)
val c = Gen(«Привет»)

197
Язык программирования Kotlin

Если посмотреть на вывод программы, то мы увидим, что несмотря


на то, что обобщенный класс Gen не имеет конкретного типа, все объ-
екты у нас строго типизированы.

Обобщенные типы различаются


по аргументам типа

В отношении обобщенных типов необходимо помнить, что ссылка


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

a = b

Несмотря на то что обе переменные относятся к типу Gen<T>, они


являются ссылками на разные типы объектов, поэтому их параметры
типов различаются.

Несколько параметров типа

Для обобщенного типа можно объявлять не только один параметр.


Два или более параметра типа можно указать списком через запятую.
Рассмотрим простой пример класса:

class GenTwo<T, V>(a: T, b: V) {


var t: T = a
var v: V = b
}
fun main(args: Array<String>){
val aTwo = GenTwo<Int, String>(1,
“Привет»)
println(«Тип для aTwo.t: « +
aTwo.t::class.java.simpleName)
println(«Тип для aTwo.v: « +
aTwo.v::class.java.simpleName)
}

Вывод программы:

198
Глава 7. Классы и объекты

Тип для aTwo.t: Integer


Тип для aTwo.v: String

Обратите внимание на объявление класса GenTwo:

class GenTwo<T, V>

В этом объявлении два параметра типа T и V задаются списком че-


рез запятую. И поскольку в объявлении этого класса указаны два пара-
метра типа, то при создании объекта типа GenTwo должны быть пере-
даны два аргумента типа:

val aTwo = GenTwo<Int, String>(1, “Привет»)

В этом случае тип Int подставляется вместо параметра типа T, а тип


String — вместо параметра типа V.
В данном примере оба аргумента типа отличаются, тем не менее
вполне допустимо передавать в качестве параметров два одинаковых
типа:

val bTwo = GenTwo<Int, Int>(1, 1)

Конечно, если оба аргумента имеют одинаковый тип, то два параме-


тра типа не нужны, тем не менее стоит знать, что такая возможность
есть.

Ограниченные типы

В предыдущих примерах параметры типов могли быть заменены ти-


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

class GenLim<T: Number>


fun <T : Comparable<T>> sort(list: List<T>)
{

199
Язык программирования Kotlin

// ...
}

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

Обобщенные методы

Как было показано ранее в примерах, в методах обобщенного клас-


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

fun <T> singletonList(item: T): List<T> {


// ...
}
fun <T> T.basicToString() : String { //
функция-расширение
// ...
}

Для вызова обобщенной функции необходимо указать тип аргумен-


тов на месте вызова после имени функции:

val l = singletonList<Int>(1)

В обобщенных методах, как и в обобщенных классах, мы можем


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

200
Глава 7. Классы и объекты

применять к любому типу массивов, при условии, что массив содержит


объекты, совместимые с типом искомого объекта.

fun <T: Comparable<T>> isIn(a: T, b:


Array<T>): Boolean {
(0..b.size-1).forEach { i -> if (a ==
b[i]) return true }
return false
}
fun main(args: Array<String>){
val nums = arrayOf(1, 2, 3, 4, 5)
val strs = arrayOf(“один», «два»,
«три», «четыре», «пять»)
if (isIn(2, nums))
println(“Число 2 содержится
в массиве nums”)
if (!isIn(7, nums))
println(“Число 7 отсутствует
в массиве nums”)
if (isIn(“три”, strs))
println(“три содержится в массиве
strs”)
if (!isIn(“восемь”, strs))
println(“восемь отсутствует
в массиве strs”)
}

Программа выводит следующий результат:


Число 2 содержится в массиве nums
Число 7 отсутствует в массиве nums
три содержится в массиве strs
восемь отсутствует в массиве strs

Обратите внимание, что в примере функция isIn() вызывается


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

201
Язык программирования Kotlin

if (isIn<Int>(2, nums))
println(«Число 2 содержится в массиве
nums»)

Только одна верхняя граница может быть указана в угловых скобках.


В случае если один параметризованный тип требует больше, чем одной
верхней границы, необходимо использовать ключевое слово where:

fun <T> cloneWhenGreater(list: List<T>,


threshold: T): List<T>
where T : Comparable,
T : Cloneable {
return list.filter { it > threshold }.map
{ it.clone() }
}

Обобщенные интерфейсы

Помимо классов, методов и функций обобщенными можно объяв-


лять интерфейсы. Обобщенные интерфейсы объявляются таким же об-
разом, как и обобщенные классы. Рассмотрим пример:

interface MinMax<T: Comparable<T>> {


fun min(): T
fun max(): T
}
class GenClass<T: Comparable<T>>(val vals:
Array<T>): MinMax<T> {
override fun min(): T {
var v = vals[0]
(0..vals.size-1).forEach { i -> if
(vals[i] < v) v = vals[i] }
return v
}
override fun max(): T {
var v = vals[0]
(0..vals.size-1).forEach { i -> if
(vals[i] > v) v = vals[i] }
return v
}

202
Глава 7. Классы и объекты

}
fun main(args: Array<String>){
val iob = GenClass(arrayOf(3, 6, 2, 8,
6))
val cob = GenClass(arrayOf(‘b’, ‘r’,
‘p’, ‘w’))
println(“Максимум в массиве iob: ${iob.
max()}»)
println(«Минимум в массиве iob: ${iob.
min()}»)
println(«Максимум в массиве cob: ${cob.
max()}»)
println(«Минимум в массиве cob: ${cob.
min()}»)
}

В данном примере мы создали обобщенный интерфейс MinMax,


в котором объявлены методы min()и max(), которые должны воз-
вращать минимальное и максимальное значения из некоторого множе-
ства объектов.
Обобщенный интерфейс объявляется таким же образом, как и обоб-
щенный класс. В данном случае параметр типа Т ограничивается сверху
интерфейсом Comparable. Этот интерфейс определен в пакете java.
lang для целей сравнения объектов.
Затем интерфейс MinMax реализуется в классе GenClass таким же
образом, как и в обычных классах.
Обратите внимание, что сначала параметр типа Т объявляет-
ся в классе GenClass, а затем передается интерфейсу MinMax. Ин-
терфейсу MinMax требуется тип класса, реализующего интерфейс
Comparable, поэтому в объявлении класса, реализующего интерфейс,
должно быть наложено такое же ограничение. Более того, однажды на-
ложенное ограничение уже не нужно повторять при имплементации
интерфейса в определении класса.
Класс, реализующий обобщенный интерфейс, должен быть также
обобщенным для случаев, когда параметр типа должен передаваться
в имплементации интерфейса.
Обобщенный интерфейс дает два преимущества:

• Может быть реализован для разных типов данных


• Позволяет наложить ограничения на типы данных, для которых
онам может быть реализован

203
Язык программирования Kotlin

Иерархии обобщенных классов

Обобщенные классы могут быть частью иерархии классов, как и лю-


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

open class GenSuperClass<T>


class GenSubClass<T> : GenSuperClass<T>()

В этом примере класс GenSubClass расширяет обобщенный


класс GenSuperClass. Параметр типа Т указан в объявлении клас-
са GenSubClass и передается классу GenSuperClass. Это означает
также, что тип, передаваемый классу GenSubClass, будет также пере-
дан классу GenSuperClass. Например:

val c = GenSubClass<Int>()

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


трами типа. Например:

class GenSubClass2<T, V>(val a: T, b: V) :


GenSuperClass<T>()

Обобщенный подкласс

Суперклассом для обобщенного подкласса вполне может служить


и необобщенный класс. Например:

open class MyClass


class GenSubClass3<T>: MyClass()

Класс MyClass является необобщенным, поэтому никаких аргумен-


тов типа в нем не указывается. И даже если в классе GenSubClass3
объявляется параметр типа Т, то он не требуется и не может быть

204
Глава 7. Классы и объекты

использован в классе MyClass. То есть, обобщенный класс наследует-


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

Вариативность

В языке Kotlin поддерживается вариантность на уровне объявле-


ния и проекции типов. Вариантность — перенос наследования ис-
ходных типов на производные от них типы. Под производными ти-
пами понимаются контейнеры, делегаты и обобщения. Различными
видами вариантности являются ковариантность, контравариантность
и инвариантность.
• Ковариантность — перенос наследования исходных типов
на производные от них типы в прямом порядке
• Контравариантность — перенос наследования исходных типов
на производные от них типы в обратном порядке
• Инвариантность — ситуация, когда наследование исходных ти-
пов не переносится на производные

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


что они ковариантны исходному типу. Если у производных типов на-
блюдается контравариантность, говорят, что они контравариантны ис-
ходному типу. Если у производных типов не наблюдается ни того, ни
другого, говорят, что они инвариантны.
В Kotlin существует способ объяснить вещь такого рода компилято-
ру. Он называется вариантность на уровне объявления: мы можем по-
метить аннотацией параметризованный тип T класса Source, чтобы
удостовериться, что он только возвращается (производится) членами
Source<T> и никогда не потребляется. Чтобы сделать это, нам необ-
ходимо использовать модификатор out:

abstract class Source<out T> {


abstract fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // Всё
в порядке, т.к. T — out-параметр
// ...
}

205
Язык программирования Kotlin

Общее правило таково: когда параметр T класса С объявлен как


out, он может использоваться только в out-местах в членах C. Но зато
C<Base> может быть родителем C<Derived>, и это будет безопасно.
Говоря «умными словами», класс C ковариантен в параметре T; или:
T является ковариантным параметризованным типом.
Модификатор out называют вариативной аннотацией, и так как он
указывается на месте объявления типа параметра, речь идет о вариа-
тивности на месте объявления. Эта концепция противопоставлена ва-
риативности на месте использования из Java, где маски при использова-
нии типа делают типы ковариантными.
В дополнение к out Kotlin предоставляет дополнительную вариа-
тивную аннотацию in. Она делает параметризованный тип контрава-
риантным: он может только потребляться, но не может производиться.
Comparable является хорошим примером такого класса:

abstract class Comparable<in T> {


abstract fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0)
val y: Comparable<Double> = x
}

В приведенном примере значение 1.0 имеет тип Double, расширя-


ющий Number. Таким образом, мы можем присвоить значение x пере-
менной типа Comparable<Double>.

Проекции типов

Объявлять параметризованный тип T как out очень удобно: при его


использовании не будет никаких проблем с подтипами. И это действи-
тельно так в случае с классами, которые могут быть ограничены только
на возвращение T. А как быть с теми классами, которые еще и принима-
ют T? Пример — класс Array:

class Array<T>(val size: Int) {


fun get(index: Int): T { /* ... */ }
fun set(index: Int, value: T) { /* ...
*/ }
}

206
Глава 7. Классы и объекты

Этот класс не может быть ни ко-, ни контравариантным в T, что ведет


к некоторому снижению гибкости. Рассмотрим следующую функцию:

fun copy(from: Array<Any>, to: Array<Any>) {


assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}

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


сива в другой. Давайте попробуем сделать это на практике:

val ints: Array<Int> = arrayOf(1, 2, 3)


val any = Array<Any>(3)
copy(ints, any) // Ошибка: ожидалось
(Array<Any>, Array<Any>)

Здесь мы попадаем в уже знакомую нам проблему: Array<T> ин-


вариантен в T, таким образом, Array<Int> не является подтипом
Array<Any>. Почему? Опять же, потому, что копирование может со-
творить плохие вещи, например может произойти попытка записать,
скажем, значение типа String в from. И если мы на самом деле пе-
редадим туда массив Int, через некоторое время будет выброшен
ClassCastException.
Тогда единственное, в чем мы хотим удостовериться, это то, что
copy() не сделает ничего плохого. Мы хотим запретить методу запи-
сывать в from, и мы можем это сделать:

fun copy(from: Array<out Any>, to:


Array<Any>) {
// ...
}

То, что мы написали выше, называется проекция типов: мы сказа-


ли, что from — не просто массив, а ограниченный (спроецированный):
мы можем вызывать только те методы, которые возвращают параме-
тризованный тип T, что в этом случае означает, что мы можем вызы-
вать только get().
Подобное проецирование можно делать и с in:

207
Язык программирования Kotlin

fun fill(dest: Array<in String>, value:


String) {
// ...
}

«Звездные» проекции

Иногда возникает ситуация, когда вы ничего не знаете о типе аргу-


мента, но все равно хотите использовать его безопасным образом. Этой
безопасности можно добиться путем определения такой проекции па-
раметризованного типа, при которой его экземпляр будет подтипом
этой проекции.
Kotlin предоставляет так называемый star-projection синтаксис для
этого:

• Для Foo<out T>, где T — ковариантный параметризованный


тип с верхней границей TUpper, Foo<*> является эквивален-
том Foo<out TUpper>. Это значит, что когда T неизвестен, вы
можете безопасно читать значения типа TUpper из Foo<*>
• Для Foo<in T>, где T — ковариантный параметризованный
тип, Foo<*> является эквивалентом Foo<in Nothing>. Это
значит, что вы не можете безопасно писать в Foo<*> при неиз-
вестном T
• Для Foo<T>, где T — инвариантный параметризованный тип
с верхней границей TUpper, Foo<*> является эквивалентом
Foo<out TUpper> при чтении значений и Foo<in Nothing>
при записи значений

Если параметризованный тип имеет несколько параметров, каж-


дый из них проецируется независимо. Например, если тип объявлен
как interface Function<in T, out U>, мы можем представить следующую
«звездную» проекцию:

• Function<*, String> означает Function<in Nothing, String>


• Function<Int, *> означает Function<Int, out Any?>
• Function<*, *> означает Function<in Nothing, out Any?>

208
Глава 7. Классы и объекты

РАСШИРЕНИЯ
Аналогично таким языкам программирования, как C# и Gosu, Kotlin по-
зволяет расширять класс путем добавления нового функционала, не на-
следуясь от такого класса и не используя паттерн «Декоратор». Это реали-
зовано с помощью специальных выражений, называемых расширения.
Kotlin поддерживает функции-расширения и свойства-расширения.

Функции-расширения

Для того чтобы объявить функцию-расширение, нам нужно ука-


зать в качестве приставки возвращаемый тип, то есть тип, кото-
рый мы расширяем. Следующий пример добавляет функцию swap
к MutableList<Int>:

fun MutableList<Int>.swap(index1: Int, in-


dex2: Int) {
val tmp = this[index1] // ‘this’ даёт
ссылку на Int
this[index1] = this[index2]
this[index2] = tmp
}

Ключевое слово this внутри функции-расширения соотносится


с получаемым объектом (его тип ставится перед точкой). Теперь мы мо-
жем вызывать такую функцию в любом MutableList<Int>:

val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // ‘this’ внутри ‘swap()’
не будет содержать значение ‘l’

Разумеется, эта функция имеет смысл для любого MutableList<T>,


и мы можем сделать ее обобщенной:

fun <T> MutableList<T>.swap(index1: Int, in-


dex2: Int) {
val tmp = this[index1] // ‘this’
относится к листу
this[index1] = this[index2]

209
Язык программирования Kotlin

this[index2] = tmp
}

Мы объявляем обобщенный тип-параметр перед именем функции


для того, чтобы он был доступен в получаемом типе-выражении.
Расширения на самом деле не проводят никаких модификаций
с классами, которые они расширяют. Объявляя расширение, вы создае-
те новую функцию, а не новый член класса. Такие функции могут быть
вызваны через точку, применимо к конкретному типу.
Стоит подчеркнуть, что расширения имеют статическую реализа-
цию: это значит, что вызванная функция-расширение определена ти-
пом вызывающего ее выражения, а не типом результата, который полу-
чается в ходе выполнения программы. К примеру:

open class C
class D: C()
fun C.foo() = “c”
fun D.foo() = “d”
fun printFoo(c: C) {
println(c.foo())
}
printFoo(D())

Этот пример выведет нам “с” на экран потому, что вызванная функ-
ция-расширение зависит только от объявленного параметризованного
типа c, который является C-классом.
Если в классе есть и член в виде обычной функции, и функция-рас-
ширение с тем же возвращаемым типом, таким же именем и применя-
емая с такими же аргументами, то обычная функция имеет более высо-
кий приоритет и будет вызвана. К примеру:

class C {
fun foo() { println(“member”) }
}
fun C.foo() { println(“extension”) }

Если мы вызовем c.foo() любого объекта c с типом C, на экран


выведется «member», а не «extension».
Однако для функций-расширений совершенно нормально пере-
гружать функции-члены, которые имеют такое же имя, но другую
сигнатуру:

210
Глава 7. Классы и объекты

class C {
fun foo() { println(“member”) }
}
fun C.foo(i: Int) { println(“extension”) }

Обращение к C().foo(1) выведет на экран надпись «extension».


Функции-расширения могут быть объявлены с возможностью по-
лучения null в качестве возвращаемого значения. Такие расширения
могут ссылаться на переменные объекта, даже если их значение null.
В таком случае есть возможность провести проверку this == null
внутри тела функции. Благодаря этому метод toString() в язы-
ке Kotlin вызывается без проверки на null: она проходит внутри
функции-расширения:

fun Any?.toString(): String {


if (this == null) return “null”
return toString()
}

Свойства-расширения

Аналогично функциям Kotlin поддерживает расширения свойств:

val <T> List<T>.lastIndex: Int


get() = size - 1

Так как расширения на самом деле не добавляют никаких членов


к классам, свойство-расширение не может иметь backing field. Вот поче-
му запрещено использовать инициализаторы для свойств-расширений.
Их поведение может быть определено только явным образом, с указа-
нием геттеров/сеттеров.

Расширение вспомогательных объектов

Если у класса есть вспомогательный объект, вы также можете опре-


делить функции и свойства для такого объекта:

class MyClass {
companion object { } // называется

211
Язык программирования Kotlin

«сompanion»
}
fun MyClass.Companion.foo() {
// ...
}

Как и обычные члены вспомогательного объекта, они могут быть


вызваны с помощью имени класса в качестве точки доступа:

MyClass.foo()

Область объявления расширений

Чаще всего расширения объявляются на самом верхнем уровне, сра-


зу под пакетами:

package foo.bar

fun Baz.goo() { ... }

Для того чтобы использовать такое расширение вне пакета, в ко-


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

package com.example.usage
import foo.bar.goo // импортировать все
расширения за именем «goo»
// или
import foo.bar.* // импортировать все из
«foo.bar»
fun usage(baz: Baz) {
baz.goo()
)

Определение расширений как членов класса

В языке Kotlin расширение может также быть объявлено как член


класса. Другими словами, мы можем внутри класса реализовать метод,
который будет расширением другого класса.

212
Глава 7. Классы и объекты

class D {
fun bar() { ... }
}
class C {
fun baz() { ... }
fun D.foo() {
bar() // вызывает D.bar
baz() // вызывает C.baz
}
fun caller(d: D) {
d.foo() // вызов функции-
расширения
}
}

В случае конфликта имен между членами класса, к которому отсыла-


ется расширение, и членами класса, в котором оно вызывается, в прио-
ритете будут имена класса, принимающего расширение.

class C {
fun D.foo() {
toString() // вызывает
D.toString()
this@C.toString() // вызывает
C.toString()
}
}

Расширения, объявленные как члены класса, могут иметь модифика-


тор видимости open и быть переопределены в унаследованных классах.
Это означает, что виртуально такая отсылка происходит с учетом типа,
к которому она отсылает, но статически — с учетом типа, возвращаемо-
го таким расширением.

open class D {
}
class D1 : D() {
}
open class C {
open fun D.foo() {
println(“D.foo in C”)

213
Язык программирования Kotlin

}
open fun D1.foo() {
println(“D1.foo in C”)
}
fun caller(d: D) {
d.foo() // вызов функции-расширения
}
}
class C1 : C() {
override fun D.foo() {
println(“D.foo in C1”)
}
override fun D1.foo() {
println(“D1.foo in C1”)
}
}
C().caller(D()) // prints “D.foo in C”
C1().caller(D()) // prints “D.foo in C1” -
получатель отсылки вычислен виртуально
C().caller(D1()) // prints “D.foo in C” -
получатель расширения вычислен статически

УЛУЧШАЕМ КЛАСС STACK


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

interface IStack<T> {
fun push(item: T)
fun pop(): T?
}
class CoolStack<T>(val size: Int = 10):
IStack<T> {
@Suppress(“UNCHECKED_CAST”)
private val stck =
arrayOfNulls<Any>(size) as Array<T>
private var tos: Int = -1

214
Глава 7. Классы и объекты

override fun push(item: T) {


if (tos == size - 1)
println(“Стек заполнен»)
else
stck[++tos] = item
}
override fun pop(): T? {
if (tos < 0) {
println(“Стек пуст»)
return null
}
return stck[tos--]
}
}
fun main(args: Array<String>){
val size = 5
val stackInt = CoolStack<Int>(size)
val stackChar = CoolStack<Char>(size)
println(“Целочисленный стек: «)
for (i in 0..size - 1)
stackInt.push(i)
for (i in 0..size - 1)
print(“ “ + stackInt.pop())
println()
println(“Символьный стек: «)
for (i in 0..size - 1)
stackChar.push((i + 65).toChar())
for (i in 0..size - 1)
print(“ “ + stackChar.pop())
println()
}

В данном примере мы, во-первых, создали и использовали интер-


фейс IStack, в котором определили необходимость реализации двух
методов: push() и pop(). Во-вторых, наш класс реализован с исполь-
зованием обобщений, что позволило нам использовать один класс для
создания стеков, содержащих объекты разных типов.

215
Язык программирования Kotlin

ГЛАВА 8.
ОБРАБОТКА ИСКЛЮЧЕНИЙ

ИСКЛЮЧЕНИЯ В KOTLIN
В этой главе рассматривается механизм обработки исключительных си-
туаций в языке Kotlin. Исключение — это ненормальная ситуация, воз-
никающая во время выполнения последовательности кода. Иными сло-
вами, это ошибка, возникающая в программе во время выполнения.
Kotlin предоставляет механизм обработки исключительных ситуаций,
который призван облегчить обработку ошибок.

Основы обработки исключений

Исключение в языке Kotlin представляет собой объект, описываю-


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

216
Глава 8. Обработка исключений

Управление обработкой исключений в Kotlin осуществляется с по-


мощью следующих ключевых слов: try, catch, finally и throw. Ра-
ботают они следующим образом. Операторы программы, которые тре-
буется отслеживать на предмет исключений, размещаются в блоке try.
Если исключение возникает в блоке try, оно генерируется и приклад-
ной код может его перехватить в блоке catch, а затем обработать его
некоторым способом. Системные исключения генерируются исполни-
тельной системой Kotlin. Для генерации исключения вручную служит
ключевое слово throw:

throw MyException(«Hi There!»)

Любой код, который должен непременно выполниться по завер-


шении блока try, размещается в блоке finally. Общая форма блока
обертки исключительной ситуации представлена ниже:

try {
//.. блок кода
}
catch (исключение: тип) {
//.. обработчик исключения
}
finally {
//.. блок кода
}

В коде может быть любое количество блоков catch (такие блоки


могут и вовсе отсутствовать). Блоки finally могут быть опущены. Од-
нако должен быть использован как минимум один блок catch или
finally.

КЛАССЫ ИСКЛЮЧЕНИЙ
Все исключения в Kotlin являются наследниками класса Throwable.
Это означает, что класс Throwable находится на вершине иерархии
классов исключений. Сразу же за классом Throwable ниже по иерар-
хии следуют два подкласса, разделяющие все исключения на две отдель-
ные ветки.

217
Язык программирования Kotlin

Одну ветвь возглавляет класс Exception. Он служит для исключи-


тельных ситуаций, которые должна перехватывать программа. Именно
от этого класса вам надлежит наследовать свои подклассы при создании
собственных типов исключений. У класса Exception имеется важный
подкласс — RuntimeException. Исключения этого типа автоматиче-
ски определяются для создаваемых Вами прикладных программ и охва-
тывают такие ошибки, как деление на нуль, ошибочная индексация мас-
сивов и другие.
Другая ветвь возглавляется классом Error, определяющим исклю-
чения, появление которых не предполагается при нормальном выпол-
нении программы. Исключения типа Error используются исполняю-
щей системой Kotlin для обозначения ошибок, происходящих в самой
исполняющей системе.
У каждого исключения есть сообщение, трассировка стека, а также
причина, по которой это исключение, вероятно, было вызвано.

НЕОБРАБАТЫВАЕМЫЕ
ИСКЛЮЧЕНИЯ
Прежде чем перейти непосредственно к обработке исключений, имеет
смысл продемонстрировать, что происходит, когда исключение не об-
рабатываются. Ниже приведен пример программы, в которой намерен-
но введен оператор, вызывающий ошибку деления на нуль.

fun main(args: Array<String>){


val d = 0
val a = 42 / d
println(“Значение а равно $a»)
}

Когда исполняющая система обнаруживает попытку деления


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

218
Глава 8. Обработка исключений

в конечном итоге будет перехвачено и обработано этим стандартным


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

Exception in thread «main» java.lang.Arith-


meticException: / by zero
at test.Test_exceptionsKt.main(test_excep-
tions.kt:5)

ОБРАБОТКА ИСКЛЮЧЕНИЙ
Стандартный обработчик исключений, предоставляемый исполняю-
щей средой, конечно же, может быть использован для отладки, но, как
правило, обрабатывать исключения приходится вручную. Это дает два
существенных преимущества. Во-первых, появляется возможность
исправить ошибку. И во-вторых, предотвращается автоматическое пре-
рывание программы.
Чтобы вручную перехватить и обработать исключительную ситу-
ацию, достаточно разместить контролируемый код в блоке оператора
try. Сразу за блоком оператора try должен идти блок catch, где ука-
зывается тип перехватываемого исключения. Ниже приведен пример,
показывающий, насколько просто это делается.

fun main(args: Array<String>){


val d = 0
var a: Int = 0
try {
a = 42 / d
} catch (e: ArithmeticException) {
println(“Деление на нуль”)
}
println(“Жизнь после оператора catch”)
}

Программа выведет следующий результат:


Деление на нуль
Жизнь после оператор catch

219
Язык программирования Kotlin

Как только возникнет исключение, управление сразу будет передано


в блок catch. По завершении блока оператора catch управление пе-
редается в строку кода, следующую после всего блока операторов try/
catch. При этом работа программы не прерывается.
Операторы try и catch составляют единое целое. Область дей-
ствия блока оператора catch не распространяется на операторы, пред-
шествующие блоку оператора try. Оператор catch не в состоянии
перехватить исключение, переданное другим оператором try, кро-
ме описываемых далее вложенных операторов try. Операторы, защи-
щаемые блоком оператора try, должны быть заключены в фигурные
скобки.
Целью большинства правильно построенных операторов try/
catch является разрешение исключительных ситуаций и продолжение
нормальной работы программы, как если бы ошибки вообще не было.
Если нам необходимо в случае возникновения исключительной си-
туации любым способом выполнить некий код, то для этого служит
блок оператора finally:

fun main(args: Array<String>){


val d = 0
var a: Int = 0
try {
a = 42 / d
}
finally {
println(“Жизнь после оператора finally»)
}
println(“Жизнь после оператора try”)
}

В примере выше мы не обрабатываем исключительную ситуацию,


но хотим, чтобы программа вывела нам строку «Жизнь после опера-
тора finally». Для этого мы помещаем вывод в блок оператора finally.
В результате выполнения программы мы видим следующий вывод
в консоль:
Жизнь после оператора finally
Exception in thread «main» java.lang.
ArithmeticException: / by zero
at exceptions.FinallyKt.main(finally.kt:8)

220
Глава 8. Обработка исключений

TRY — ЭТО ВЫРАЖЕНИЕ


В языке Kotlin оператор try является выражением. Т.е. он может воз-
вращать значение:

val a: Int? = try { parseInt(input) } catch


(e: NumberFormatException) { null }

Возвращаемым значением будет либо последнее выражение в блоке


try, либо последнее выражение в блоке catch (или блоках). Содер-
жимое finally блока никак не повлияет на результат try-выражения.

НЕСКОЛЬКО
ОПЕРАТОРОВ CATCH
Иногда в одном фрагменте кода может возникнуть несколько исключе-
ний. Чтобы справиться с такой ситуацией, можно указать два или бо-
лее операторов catch, каждый из которых предназначен для перехвата
отдельного типа исключения. Когда генерируется исключение, каждый
оператор catch проверяется по порядку и выполняется тот из них, ко-
торый совпадает по типу с возникшим исключением. По завершении
одного из операторов catch все остальные пропускаются и выполне-
ние программы продолжается с оператора, следующего сразу за блоком
операторов try/catch. Ниже приведен бестолковый пример програм-
мы, единственная польза от которого — продемонстрировать использо-
вание нескольких операторов catch.

fun main(args: Array<String>){


val a = args.size
var b: Int = 0
val c: IntArray = intArrayOf(1)
try {
println(“a = $a”)
b = 42/ a
c[42] = 99
}
catch (e: ArithmeticException) {

221
Язык программирования Kotlin

println(“Деление на нуль: « + e)
}
catch (e: ArrayIndexOutOfBoundsExcep-
tion) {
println(“Индекс за пределами
массива: « + e)
}
println(«Жизнь после try/catch»)
}

Вывод программы без параметра:


a = 0
Деление на нуль: java.lang.ArithmeticException: /
by zero
Жизнь после try/catch

Вывод программы с параметром:


a = 1
Индекс за пределами массива: java.lang.
ArrayIndexOutOfBoundsException: 42
Жизнь после try/catch

В этой программе происходит деление на нуль, если она за-


пущена без параметров (аргументов командной строки).
Если параметры переданы, то мы поймаем исключение типа
ArrayIndexOutOfBoundsException, поскольку длина массива це-
лых чисел равна 1, а мы пытаемся присвоить значение элементу масси-
ва c[42].
Применяя несколько операторов catch, важно помнить, что пе-
рехват исключений из подклассов должен следовать ДО перехвата ис-
ключений из суперклассов. Это необходимо делать потому, что catch,
в котором перехватывается исключение из суперкласса, будет перехва-
тывать все исключения из суперкласса плюс все исключения из под-
классов. Кроме того, вы можете в этом случае получить недостижимый
код, что хоть и не является в Kotlin ошибкой в отличие от Java, но так-
же является плохой ситуацией.

222
Глава 8. Обработка исключений

ВЛОЖЕННЫЕ ОПЕРАТОРЫ TRY


Операторы try могут быть вложенными. Это означает, что один опе-
ратор try может находиться в блоке другого оператора try. Каждый
раз, когда управление передается блоку оператора try, контекст со-
ответствующего исключения размещается в стеке. Если во вложенном
операторе try отсутствует оператор catch или контекст этого опера-
тора catch не соответствует контексту исключения, стек разворачива-
ется и проверяется на соответствие оператора catch из внешнего бло-
ка оператора try, и так до тех пор, пока не будет найден подходящий
оператор catch или не будут исчерпаны все уровни вложенности опе-
раторов try. Если подходящий оператор catch так и не будет найден,
то исключение будет обработано исполняющей системой.
Ниже приведен пример, демонстрирующий вложенные операторы
try.

fun main(args: Array<String>){


try {
var a = args.size
val b = 42 / a
println(“a = $a”)
println(“b = $b”)
try {
if (a == 1) a /= (a - a)
if (a == 2) {
val c = arrayOf(1)
c[42] = 99
}
} catch (e: ArrayIndexOutOfBound-
sException) {
println(“Индекс за пределами
массива: “ + e)
}
}
catch (e: ArithmeticException) {
println(“Деление на нуль: « + e)
}
}

223
Язык программирования Kotlin

Как видите, в этой программе один оператор try вложен в блок


другого оператора try. Программа работает следующим образом. Ког-
да она запускается на выполнение без аргументов командной строки,
во внешнем блоке оператора try генерируется исключение в связи
с тем, что происходит деление на нуль, так как размер массива аргумен-
тов командной строки будет равен нулю. Если программа будет запу-
щена с одним аргументом, то исключение деление на нуль будет сгене-
рировано во внутреннем блоке try, но так как внутренний блок try
не имеет нужного оператора catch, управление будет передано опе-
ратору catch более высокого уровня. Если же программе предаются
два аргумента, то будет сгенерировано исключение во внутреннем бло-
ке try в связи с выходам за пределы индекса массива. Ниже представ-
лен вывод программы во всех трех случаях:
Деление на нуль: java.lang.ArithmeticException: /
by zero
a = 1
b = 42
Деление на нуль: java.lang.ArithmeticException: /
by zero
a = 2
b = 21
Индекс за пределами массива: java.lang.ArrayIn-
dexOutOfBoundsException: 42

Вложения операторов try могут быть не столь очевидны при вы-


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

ОПЕРАТОР THROW
В приведенных ранее примерах перехватывались только исключения,
которые генерировались исполняющей системой. Но исключения мож-
но генерировать и непосредственно в прикладной программе. Для это-
го служит оператор throw. Его общая форма выглядит следующим
образом:

224
Глава 8. Обработка исключений

throw генерируемое_исключение

В данном случае генерируемое_исключение должно быть объектом


класса Throwable или производного от него класса. Получить объект
класса Throwable можно двумя способами:
• указать соответствующий параметр в операторе catch
• создав соответствующий объект

Поток исполнения программы останавливается сразу же после опе-


ратора throw, и все последующие операторы не выполняются. Тут
вступают в действие операторы try/catch или же стандартный обра-
ботчик исключений. Ниже приведен пример простой программы, гене-
рирующей исключения:

fun main(args: Array<String>){


class ThrowDemo {
fun proc() {
try {
throw
NullPointerException(“Демо NPE»)
} catch (e: NullPointerExcep-
tion) {
println(“Исключение
перехвачено в демо классе в методе”)
throw e
}
}
}
try {
ThrowDemo().proc()
} catch (e: NullPointerException) {
println(“Повторный перехват NPE»)
}
}

Программа выведет следующий результат:


Исключение перехвачено в демо-классе в методе
Повторный перехват NPE

225
Язык программирования Kotlin

Тип Nothing

Вы можете использовать выражение throw в качестве части


элвис-выражения:

val s = person.name ?: throw


IllegalArgumentException(“Name required”)

Типом выражения throw является специальный тип под названи-


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

fun fail(message: String): Nothing {


throw IllegalArgumentException(message)
}

ОПЕРАТОР FINALLY
Когда генерируется исключение, выполнение кода программы направ-
ляется по нелинейному пути, резко изменяющему нормальную после-
довательность выполнения операторов в коде программы. Например,
в зависимости от того, как написан метод (функция), исключение мо-
жет стать причиной преждевременного возврата из метода. В некото-
рых случаях это может вызвать серьезные осложнения. Так, если файл
открывается в начале метода и закрывается в конце, то вряд ли вас
устроит, что код, закрывающий файл, будет обойден механизмом об-
работки исключительных ситуаций. Для таких вот непредвиденных об-
стоятельств и служит оператор finally.
Оператор finally образует блок кода, который будет выпол-
нен по завершении блока операторов try/catch, но перед следую-
щим за ним кодом. Блок оператора finally выполняется независимо
от того, сгенерировано исключение или нет. Если исключение сгенери-
ровано, блок оператора finally выполнится даже при условии, что ни
один из операторов catch не совпадает с этим исключением. В любой
момент, когда происходит возврат вызывающему коду из блока опера-
торов try/catch, блок оператора finally выполняется перед возвра-
том управления.

226
Глава 8. Обработка исключений

Механизм работы оператора finally очень удобен, например, при


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

fun main(args: Array<String>){


val d = 0
var a: Int = 0
try {
a = 42 / d
} catch (e: Exception) {
println(“Мы перехватили
исключение”)
}
finally {
println(«Жизнь после оператора fi-
nally»)
}
println(«Жизнь после оператора try/
catch»)
}

ВСТРОЕННЫЕ ИСКЛЮЧЕНИЯ
В языке Kotlin определен ряд классов исключений, производных
от RuntimeException, которые покрывают большинство распро-
страненных ошибок вы можете использовать их в блоках try/catch.
Эти исключения являются алиасами для соответствующих типов ис-
ключений из Java.

227
Язык программирования Kotlin

СОЗДАНИЕ СОБСТВЕННЫХ
ИСКЛЮЧЕНИЙ
Встроенные исключения позволяют обрабатывать большинство рас-
пространенных ошибок. Тем не менее в прикладных программах воз-
можны особые ситуации, требующие наличия и обработки соответ-
ствующих исключений.
Для того чтобы создать класс собственного исключения, достаточ-
но определить его как производный от класса Exception, который,
в свою очередь, является наследником класса Throwable. В под-
классе собственных исключений совсем не обязательно что-нибудь реа-
лизовывать. Их присутствия в системе типов уже достаточно, чтобы их
использовать как исключения.
В приведенном ниже примере демонстрируется создание собствен-
ного класса исключения.

class MyException (val detail: Int): Excep-


tion() {
override fun toString(): String {
return “Мое исключение [$detail]»
}
}
fun main(args: Array<String>){
fun compute(a: Int) {
println(“Вызван метод compute($a)»)
if (a > 10)
throw MyException(a)
println(“Нормальное завершение”)
}
try {
compute(1)
compute(20)
} catch (e: MyException) {
println(«Перехвачено исключение:
$e»)
}
}

Программа выводит следующий результат:

228
Глава 8. Обработка исключений

Вызван метод compute(1)


Нормальное завершение
Вызван метод compute(20)
Перехвачено исключение: Мое исключение [20]

ЦЕПОЧКИ ИСКЛЮЧЕНИЙ
Давайте представим себе, что в методе генерируется исключение типа
ArithmeticException в связи с попыткой деления на нуль. Но ис-
тинная причина состоит в ошибке ввода-вывода, которая и приводит
к появлению неверного делителя. И хотя метод должен сгенерировать
исключение типа ArithmeticException, поскольку произошла
именно эта ошибка, тем не менее нам хотелось бы также сообщить пер-
вопричину этого безобразия. Язык Kotlin предоставляет нам такую воз-
можность, которая называется цепочки исключений.
Для организации цепочки исключений мы будет использовать метод
initCause() и свойство cause класса Throwable.
Метод initCause() связывает причину исключения с вызываю-
щим исключением и возвращает ссылку на исключение. Таким обра-
зом причину можно связать с исключением после его создания. Свой-
ство cause объекта исключения возвращает исключение, вызвавшее
текущее исключение. Пример ниже демонстрирует механизм цепочки
исключений:

fun demoChains(cause: String){


val e = NullPointerException()
e.initCause(ArithmeticException(cause))
throw e
}
fun main(args: Array<String>){
try {
demoChains(“Марс атакует»)
} catch (e: NullPointerException) {
println(“Перехвачено исключение:
$e”)
println(“Причиной стало: “ +
e.cause)
}
}

229
Язык программирования Kotlin

Цепочки исключений могут быть составлены на любую глубину. Это


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

230
Глава 9. Рефлексия и аннотации

ГЛАВА 9.
РЕФЛЕКСИЯ И АННОТАЦИИ

РЕФЛЕКСИЯ
Рефлексия — это набор возможностей языка и библиотек, который по-
зволяет интроспектировать программу (обращаться к ее структуре)
во время ее исполнения. В Kotlin функции и свойства первичны, и по-
этому их интроспекция (например, получение имени или типа во вре-
мя исполнения) сильно переплетена с использованием функциональ-
ной или реактивной парадигмы.

Ссылки на классы

Самая базовая возможность рефлексии — это получение ссылки


на Kotlin-класс. Чтобы получить ссылку на статический Kotlin-класс,
используйте синтаксис литерала класса:

val c = MyClass::class

Ссылка на класс имеет тип KClass.


Обратите внимание, что ссылка на Kotlin-класс — это не то же са-
мое, что ссылка на Java-класс. Для получения ссылки на Java-класс ис-
пользуйте свойство .java экземпляра KClass.

231
Язык программирования Kotlin

Ссылки на привязанные классы

Вы можете получить ссылку на класс определенного объекта с по-


мощью уже известного вам синтаксиса, вызвав ::class у нужного
объекта:

val widget: Widget = ...


assert(widget is GoodWidget) { “Bad widget:
${widget::class.qualifiedName}” }

Вы получите ссылку на точный класс объекта, например


GoodWidget или BadWidget, несмотря на тип объекта, участвующе-
го в выражении (Widget).

Ссылки на функции

Когда у нас есть именованная функция, объявленная следующим


образом:

fun isOdd(x: Int) = x % 2 != 0

мы можем как вызвать ее напрямую (isOdd(5)), так и передать ее


как значение, например, в другую функцию. Чтобы сделать это, исполь-
зуйте оператор (::):

val numbers = listOf(1, 2, 3)


println(numbers.filter(::isOdd)) // выведет
[1, 3]

Здесь, ::isOdd — значение функционального типа (Int) ->


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

fun isOdd(x: Int) = x % 2 != 0


fun isOdd(s: String) = s == “brillig” || s
== “slithy” || s == “tove”
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // ссылается
на isOdd(x: Int)

232
Глава 9. Рефлексия и аннотации

Также вместо этого вы можете указать нужный контекст путем со-


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

val predicate: (String) -> Boolean = ::isOdd


// ссылается на isOdd(x: String)

Если вы хотите использовать член класса или функцию-рас-


ширение, вам нужно обозначить это явным образом. Например,
String::toCharArray дает нам функцию-расширение для типа
String: String.() -> CharArray.

Композиция функций

Рассмотрим следующую функцию:

fun <A, B, C> compose(f: (B) -> C, g: (A)


-> B): (A) -> C {
return { x -> f(g(x)) }
}

Она возвращает композицию двух функций, переданных ей:


compose(f, g) = f(g(*)). Теперь вы можете применять ее
к ссылкам на функции:

fun length(s: String) = s.length


val oddLength = compose(::isOdd, ::length)
val strings = listOf(“a”, “ab”, “abc”)
println(strings.filter(oddLength)) // выведет
«[a, abc]»

Ссылки на свойства

Для доступа к свойствам как первичным объектам в Kotlin мы по-


прежнему можем использовать оператор (::):

var x = 1
fun main(args: Array<String>) {
println(::x.get()) // выведет «1»
::x.set(2)

233
Язык программирования Kotlin

println(x) // выведет “2”


}

Выражение ::x возвращает объект свойства типа KProperty<Int>,


который позволяет нам читать его значение с помощью get() или по-
лучать имя свойства при помощи обращения к свойству name. Для по-
лучения более подробной информации обратитесь к документации
класса KProperty.
Для изменяемых свойств, например var y = 1, ::y возвращает
значение типа KMutableProperty<Int>.
Ссылка на свойство может быть использована там, где ожидается
функция без параметров:

val strs = listOf(“a”, “bc”, “def”)


println(strs.map(String::length)) // выведет
[1, 2, 3]

Для доступа к свойству, которое является членом класса, мы указы-


ваем класс:

class A(val p: Int)


fun main(args: Array<String>) {
val prop = A::p
println(prop.get(A(1))) // выведет «1»
}

Для функции-расширения:

val String.lastChar: Char


get() = this[length - 1]
fun main(args: Array<String>) {
println(String::lastChar.get(“abc”)) //
выведет «c»
}

Взаимодействие с рефлексией Java

На платформе Java стандартная библиотека Kotlin содержит расши-


рения, которые сопоставляют расширяемые ими объекты рефлексии
Kotlin с объектами рефлексии Java (см. пакет kotlin.reflect.jvm).

234
Глава 9. Рефлексия и аннотации

К примеру, для нахождения поля или метода, который служит геттером


для Kotlin-свойства, вы можете написать что-то вроде этого:

import kotlin.reflect.jvm.*

class A(val p: Int)

fun main(args: Array<String>) {


println(A::p.javaGetter) // выведет
«public final int A.getP()»
println(A::p.javaField) // выведет
«private final int A.p»
}

Для получения класса Kotlin, соответствующего классу Java, исполь-


зуйте свойство-расширение kotlin:

fun getKClass(o: Any): KClass<Any> =


o.javaClass.kotlin

Ссылки на конструктор

К конструкторам можно обратиться так же, как и к методам или


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

class Foo
fun function(factory : () -> Foo) {
val x : Foo = factory()
}

Используя ::Foo, конструктор класса Foo без аргументов, мы мо-


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

function(::Foo)

235
Язык программирования Kotlin

Привязанные функции

Вы можете сослаться на экземпляр метода конкретного объекта.

val numberRegex = “\\d+”.toRegex()


println(numberRegex.matches(“29”)) //
выведет «true»

val isNumber = numberRegex::matches


println(isNumber(“29”)) // выведет «true»

Вместо вызова метода matches() напрямую мы храним ссылку


на него. Такие ссылки привязаны к объектам, к которым относятся:

val strings = listOf(«abc», «124», «a70»)


println(strings.filter(numberRegex::matches))
// выведет «[124]»

Сравним типы привязанных и соответствующих непривязанных


ссылок. Объект-приемник “прикреплен” к привязанной ссылке, поэто-
му тип приемника больше не является параметром:

val isNumber: (CharSequence) -> Boolean =


numberRegex::matches
val matches: (Regex, CharSequence) -> Bool-
ean = Regex::matches

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

val prop = “abc”::length


println(prop.get()) // выведет «3»

На заметку На платформе Java библиотека для


использования рефлексии находится в отдельном JAR-
файле (kotlin-reflect.jar). Это было сделано для
уменьшения требуемого размера runtime-библиотеки для
приложений, которые не используют рефлексию. Если вы
используете рефлексию, удостоверьтесь, что этот .jar
файл добавлен в classpath вашего проекта.

236
Глава 9. Рефлексия и аннотации

АННОТАЦИИ
В языке Kotlin поддерживается языковое свойство, позволяющее встра-
ивать справочную информацию в исходные файлы. Эта информация
называется аннотацией и не меняет порядок выполнения программы.
Это означает, что аннотация сохраняет неизменной семантику про-
граммы. Хотя аннотации не влияют на выполнение программы, тем
не менее эта информация может быть использована различными ин-
струментальными средствами на стадии разработки или развертыва-
ния прикладных программ на Kotlin.
Аннотации создаются с помощью механизма, основанного на клас-
сах, и являются специальной формой метаданных. Для объявления ан-
нотации используйте модификатор annotation перед именем класса.
Ниже приведен пример создания аннотации:

annotation class Fancy

Как только аннотация будет объявлена, ею можно воспользовать-


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

@Fancy class Foo {


@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)
}
}

Правила удержания аннотаций

Правила удержания аннотаций определяют момент, когда аннотация


отбрасывается. В языке Kotlin определены три таких правила, инкапсу-
лированных в тапе AnnotationRetention:

SOURCE — содержатся только в исходных файлах и отбрасываются


при компиляции
BINARY — сохраняются в скомпилированном классе, но недоступ-
ны для виртуальной машины

237
Язык программирования Kotlin

RUNTIME — сохраняются во время компиляции и доступны вирту-


альной машине JVM

Правило RUNTIME является правилом по умолчанию и предостав-


ляет аннотации наиболее высокую степень сохраняемости.
Правило удержания аннотации задается с помощью встроенной ан-
нотации Kotlin @Retention:

@Retention(правило_удержания)

В следующем примере устанавливается правило удержания BINARY


для аннотации MyAnnotation:

@Retention(AnnotationRetention.BINARY)
annotation class MyAnnotation

Дополнительные атрибу ты аннотаций

Дополнительные атрибуты аннотаций могут быть определены путем


аннотации класса-аннотации метааннотациями:

@Target определяет возможные виды элементов, которые могут


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

@Retention определяет, будет ли аннотация храниться в скомпи-


лированном классе и будет ли видима через рефлексию (по умолчанию
оба утверждения верны)

@Repeatable позволяет использовать одну и ту же аннотацию


на одном элементе несколько раз

@MustBeDocumented определяет то, что аннотация является ча-


стью публичного API и должна быть включена в сигнатуру класса или
метода, попадающую в сгенерированную документацию

Конструкторы аннотаций

Аннотации могут иметь конструкторы, принимающие параметры:

238
Глава 9. Рефлексия и аннотации

annotation class Special(val why: String)


@Special(“пример”) class Foo {}

Разрешены параметры следующих типов:


• типы, которые соответствуют примитивам Java (Int, Long
и т.д.);
• строки
• классы (Foo::class)
• перечисляемые типы
• другие аннотации
• массивы, содержащие значения приведенных выше типов

Параметры аннотаций не могут иметь nullable типы, потому что


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

annotation class ReplaceWith(val expression:


String)
annotation class Deprecated(
val message: String,
val replaceWith: ReplaceWith = Re-
placeWith(“”))
@Deprecated(“Эта функция устарела, вместо
нее используйте ===», ReplaceWith(«this ===
other»))

Если вам нужно определить класс как аргумент аннотации, исполь-


зуйте Kotlin-класс (KClass). Компилятор Kotin автоматически скон-
вертирует его в Java-класс, так что код на Java сможет видеть аннота-
ции и их аргументы.

import kotlin.reflect.KClass
annotation class Ann(val arg1: KClass<*>,
val arg2: KClass<out Any?>)
@Ann(String::class, Int::class) class
MyClass

239
Язык программирования Kotlin

Аннотирование лямбд

Аннотации также можно использовать с лямбдами. Они будут при-


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

annotation class Suspendable


val f = @Suspendable { Fiber.sleep(10) }

Аннотации с указаниями

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


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

class Example(@field:Ann val foo, //


аннотация для Java-поля
@get:Ann val bar, //
аннотация для Java-геттера
@param:Ann val quux) //
аннотация для параметра конструктора Java

Тот же синтаксис может быть использован для аннотации целого


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

@file:JvmName(«Foo»)
package org.jetbrains.demo

Если вы помечаете аннотацией несколько элементов, вы можете


избежать повторения путем указания всех аннотаций в квадратных
скобках:

class Example {
@set:[Inject VisibleForTesting]

240
Глава 9. Рефлексия и аннотации

var collaborator: Collaborator


}

Полный список поддерживаемых указаний:

file

property (такие аннотации не будут видны


в Java)

field

get (геттер)

set (сеттер)

receiver (параметр-приёмник расширения)

param (параметр конструктора)

setparam (параметр сеттера)

delegate (поле, которое хранит экземпляр


делегата для делегированного свойства)

Чтобы пометить аннотацией параметр-приемник функции-расши-


рения, используйте следующий синтаксис:

fun @receiver:Fancy String.myExtension() { }

Если вы не сделали указание, аннотация будет применена к элемен-


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

param

property

field

241
Язык программирования Kotlin

@Target

Класс AnnotationTarget cодержит список элементов кода, кото-


рые являются возможными целями аннотации:

CLASS

ANNOTATION_CLASS

TYPE_PARAMETER

PROPERTY

FIELD

LOCAL_VARIABLE

VALUE_PARAMETER

CONSTRUCTOR

FUNCTION

PROPERTY_GETTER

PROPERTY_SETTER

TYPE

EXPRESSION

FILE

TYPEALIAS

Встроенные аннотации

@Deprecated

242
Глава 9. Рефлексия и аннотации

Отмечает аннотированный класс, функцию, свойство, переменную


или параметр как устаревшие.

@DslMarker

При применении к аннотации класс X указывает, что X определяет


язык DSL

@ExtensionFunctionType

Указывает, что аннотированный функциональный тип представляет


собой функцию-расширения.

@ParameterName

Аннотирует аргументы функционального типа и содержит соответ-


ствующее имя параметра, указанное пользователем в объявлении типа
(если оно есть).

@PublishedApi

Указывает, что эта часть внутреннего API эффективно открыта пу-


блике, используя встроенную функцию public.

@ReplaceWith

Определяет фрагмент кода, который можно использовать для за-


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

@SinceKotlin

Указывает первую версию Kotlin, где появилось объявление. Исполь-


зование объявления и указание более старой версии API (с помощью
опции командной строки -api) приведет к ошибке.

@Suppress

Подавляет данные предупреждения компиляции в аннотированном


элементе.

243
Язык программирования Kotlin

@UnsafeVariance

Подавляет ошибки about variance conflict

Получение аннотаций во время выполнения

Аннотации предназначены в основном для использования в инстру-


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

val c = MyClass::class

Имея в своем распоряжении объект типа KClass, можно восполь-


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

Свойство annotations

С помощью свойства annotations объекта типа KClass можно


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

abstract val annotations: List<Annotation>

Рассмотрим небольшой пример, который показывает процесс полу-


чения списка аннотаций для объекта:

import kotlin.reflect.full.memberProperties
@Retention(AnnotationRetention.RUNTIME)
annotation class MyAnno(val src: String)
@MyAnno(“Это аннотация для класса”) class
MyAnnoClass {
@MyAnno(«Это аннотация для свойства»)
val property = 1

244
Глава 9. Рефлексия и аннотации

}
fun main(args: Array<String>){
val c = MyAnnoClass()
val m = c::class
val p = m.memberProperties
val a = m.findAnnotation<MyAnno>()
println(m.annotations)
p.map {
println(it.annotations)
}
println(a)
}

Результат выполнения программы:

[@annotations.MyAnno(src=Это аннотация для


класса)]
[@annotations.MyAnno(src=Это аннотация для
свойства)]

Функция-расширение findAnnotation()

Еще один способ получить информацию об аннотации — это ис-


пользовать функцию-расширение findAnnotation(). Эта функция
ищет аннотацию определенного типа и определена следующим образом:

fun <T : Annotation> KAnnotatedElement.find-


Annotation(): T?

Функцию можно вызвать у любого объекта, реализующего интер-


фейс KAnnotatedElement. В приведенном выше примере мы ищем анно-
тацию следующим вызовом:

val m = c::class
val a = m.findAnnotation<MyAnno>()

Так как функция findAnnotation() реализована как обобщенная,


мы должны указать тип аннотации, который нам необходимо найти.

245
Язык программирования Kotlin

ГЛАВА 10.
СОПРОГРАММЫ

ВВЕДЕНИЕ В СОПРОГРАММЫ

Экспериментальный статус сопрограмм

Дизайн сопрограмм носит статус experimental, из чего следует воз-


можность его изменения в будущих релизах. При составлении сопро-
граммы в Kotlin по умолчанию выводится предупреждение: The feature
«coroutines» is experimental. Чтобы убрать предупреждение, необходимо
указать опцию opt-in flag.
Из-за экспериментального статуса сопрограмм все связанные
API собраны в стандартной библиотеке как пакет kotlin.coroutines.
experimental. Когда дизайн будет стабилизирован и его эксперимен-
тальный статус снят, окончательный API будет перенесен в пакет kotlin.
coroutines, а экспериментальный пакет будет храниться (возможно, как
отдельный артефакт) в целях обеспечения обратной совместимости.

Сопрограммы (кору тины)

Некоторые API инициируют долго протекающие операции (такие


как сетевой ввод-вывод, файловый ввод-вывод, интенсивная обработ-
ка на CPU или GPU и др.), которые требуют блокировки вызывающе-
го кода в ожидании завершения операций. Сопрограммы обеспечива-
ют возможность избежать блокировки исполняющегося потока путем

246
Глава 10. Сопрограммы

использования более дешевой и управляемой операции: приостановки


(suspend) сопрограммы.
Сопрограммы (корутины) упрощают асинхронное программирова-
ние, оставив все осложнения внутри библиотек. Логика программы мо-
жет быть выражена последовательно в сопрограммах, а базовая библи-
отека будет ее реализовывать асинхронно для нас. Библиотека может
обернуть соответствующие части кода пользователя в обратные вызо-
вы (callbacks), подписывающиеся на соответствующие события, и дис-
петчеризовать исполнение на различные потоки (или даже на разные
машины!). Код при этом останется столь же простой, как если бы ис-
полнялся строго последовательно.
Многие асинхронные механизмы, доступные в других языках про-
граммирования, могут быть реализованы в качестве библиотек с помо-
щью сопрограмм Kotlin. Это включает в себя async/await, chan-
nels и select, generators/yield.
Сопрограммы, с другой стороны, выглядят как простой последова-
тельный код, пряча всю сложность внутри библиотек. В то же время
они предоставляют возможность запускать асинхронный код без вся-
ких блокировок, что открывает большие возможности для различных
приложений. Вместо блокировки потоков вычисления становятся пре-
рываемыми. JetBrains описывают корутины как «легковесные пото-
ки», конечно, не те Threads, что мы знаем в Java. Корутины очень деше-
вы в создании, и накладные расходы в сравнении с потоками не идут
ни в какое сравнение. Как вы дальше увидите, корутины запускают-
ся в Threads под управлением библиотеки. Другое весомое отличие —
ограничения. Количество потоков ограничено, так как они на самом
деле соответствуют нативным потокам. Создание корутины, с другой
стороны, практически бесплатно, и даже тысячи их могут быть легко
запущены.

Блокирование против приостановки

Главным отличительным признаком сопрограмм является то, что


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

247
Язык программирования Kotlin

С другой стороны, приостановка сопрограммы обходится практи-


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

Функции приостановки

Приостановка происходит в случае вызова функции, обозначенной


специальным модификатором suspend:

suspend fun doSomething(foo: Foo): Bar {


...
}

Такие функции называются функциями остановки (приостановки),


поскольку их вызовы могут приостановить выполнение сопрограммы
(библиотека может принять решение продолжать работу без приоста-
новки, если результат вызова уже доступен). Функции остановки могут
иметь параметры и возвращать значения точно так же, как и все обыч-
ные функции, но они могут быть вызваны только из сопрограмм или
других функций остановки. В конечном итоге, при старте сопрограммы
она должна содержать как минимум одну функцию остановки, и функ-
ция эта обычно анонимная (лямбда-функция остановки).
Рассмотрим в качестве примера упрощенную функцию async() из
библиотеки kotlinx.coroutines:

fun <T> async(block: suspend () -> T)

Здесь async() является обычной функцией (не функцией останов-


ки), но параметр block имеет функциональный тип с модификатором
suspend: suspend () -> T. Таким образом, когда мы передаем
лямбда-функцию в async(), она является анонимной функцией оста-
новки, и мы можем вызывать функцию остановки изнутри ее:

248
Глава 10. Сопрограммы

async {
doSomething(foo)
...
}

await() может быть функцией остановки (также может вызывать-


ся из блока async {...}), которая приостанавливает сопрограмму
до тех пор, пока некоторые вычисления не будут выполнены, и затем
возвращает их результат:

async {
...
val result = await(...)
...
}

Отметим, что функции приостановки await() и doSomething()


не могут быть вызваны из обыкновенных функций, подобных main():

fun main(args: Array<String>) {


doSomething() // ERROR: Suspending
function called from a non-coroutine context
}

Также обратите внимание, что приостанавливающие функции могут


быть виртуальными, а при их переопределении должен быть указан мо-
дификатор suspend:

interface Base {
suspend fun foo()
}
class Derived: Base {
override suspend fun foo() { ... }
}

Аннотация @RestrictsSuspension

Функции-расширения и лямбды также могут быть помечены как


suspended, как и обычные функции. Это позволяет создавать DSL
и другие API, которые пользователи могут расширить. В некоторых

249
Язык программирования Kotlin

случаях автору библиотеки необходимо запретить пользователю добав-


лять новые способы приостановки сопрограммы. Чтобы осуществить
это, можно использовать аннотацию @RestrictsSuspension. Когда целе-
вой класс или интерфейс R аннотируется подобным образом, все расши-
рения приостановки должны делегироваться либо из членов R, либо из
других его расширений. Поскольку расширения не могут делегировать
друг друга до бесконечности (иначе программа никогда не завершится),
гарантируется, что все приостановки пройдут посредством вызова чле-
на R, так что автор библиотеки может полностью их контролировать.
Это актуально в тех редких случаях, когда каждая приостановка об-
рабатывается специальным образом в библиотеке. Например, при ре-
ализации генераторов через buildSequence() функцию, описан-
ную ниже, мы должны быть уверены, что любой приостанавливаемый
вызовов в сопрограмме завершается вызовом либо yield(), либо
yieldAll(), а не какой-то другой функции. Именно по этой причине
SequenceBuilder аннотирован с @RestrictsSuspension:

@RestrictsSuspension
public abstract class SequenceBuilder<in T>
{
...
}

Вну треннее функционирование сопрограмм

Подробное объяснение того, как сопрограммы работают изнутри,


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

250
Глава 10. Сопрограммы

переменные. Типом таких объектов является Continuation, а пре-


образование кода, описанное здесь, соответствует классическому
Continuation-passing style. Следовательно, приостанавливаемые функ-
ции принимают дополнительный параметр типа Continuation (со-
храненное состояние).

Низкоуровневый API

Низкоуровневый API относительно мал и должен использоваться


ТОЛЬКО для создания библиотек высокого уровня. Он содержит два
главных пакета:
kotlin.coroutines.experimental — главные типы и при-
митивы, такие как: createCoroutine(), startCoroutine(),
suspendCoroutine()
kotlin.coroutines.experimental.intrinsics —
встроенные функции еще более низкого уровня, такие как
suspendCoroutineOrReturn

Генераторы

Это функции исключительно «уровня приложения» в kotlin.


coroutines.experimental:

buildSequence()

buildIterator()

По сути, эти функции (и мы можем ограничиться здесь рассмотре-


нием только buildSequence()) реализуют генераторы, т.е. предо-
ставляют легкую возможность построить ленивые последовательности:

import kotlin.coroutines.experimental.*
fun main(args: Array<String>) {
//sampleStart
val fibonacciSeq = buildSequence {
var a = 0
var b = 1

yield(1)

251
Язык программирования Kotlin

while (true) {
yield(a + b)

val tmp = a + b
a = b
b = tmp
}
}
//sampleEnd
// Print the first five Fibonacci numbers
println(fibonacciSeq.take(8).toList())
}

Это сгенерирует ленивую, потенциально бесконечную последова-


тельность Фибоначчи, используя сопрограмму, которая дает последова-
тельные числа Фибоначчи, вызывая функцию yield (). При итериро-
вании такой последовательности на каждом шаге итератор выполняет
следующую часть сопрограммы, которая генерирует следующее число.
Таким образом, мы можем взять любой конечный список чисел из этой
последовательности, например fibonacciSeq.take(8).toList(),
дающий в результате [1, 1, 2, 3, 5, 8, 13, 21]. И сопрограммы достаточно
дешевы, чтобы сделать это практичным.
Чтобы продемонстрировать реальную ленивость такой последова-
тельности, давайте напечатаем некоторые отладочные результаты изну-
три вызова buildSequence():

import kotlin.coroutines.experimental.*
fun main(args: Array<String>) {
//sampleStart
val lazySeq = buildSequence {
print(“START “)
for (i in 1..5) {
yield(i)
print(“STEP “)
}
print(“END”)
}
// Print the first three elements of the
sequence
lazySeq.take(3).forEach { print(“$it “)

252
Глава 10. Сопрограммы

}
//sampleEnd
}

Запустите приведенный выше код, чтобы убедиться, что если мы


будем печатать первые три элемента, цифры чередуются со STEP-ами
по ветвям цикла. Это означает, что вычисления действительно лени-
вые. Для печати 1 мы выполняем только до первого yield(i) и печатаем
START по ходу дела. Затем, для печати 2, нам необходимо переходить
к следующему yield(i) и здесь печатать STEP. То же самое и для 3. И сле-
дующий STEP никогда не будет напечатан (точно так же как и END),
поскольку мы никогда не запрашиваем дополнительных элементов
последовательности.
Чтобы сразу породить всю коллекцию (или последовательность) зна-
чений, доступна функция yieldAll():

import kotlin.coroutines.experimental.*
fun main(args: Array<String>) {
//sampleStart
val lazySeq = buildSequence {
yield(0)
yieldAll(1..10)
}
lazySeq.forEach { print(“$it “) }
//sampleEnd
}

Функция buildIterator() во всем подобна buildSequence(),


но только возвращает ленивый итератор.
Вы могли бы добавить собственную логику выполнения функции
buildSequence(), написав приостанавливаемое расширение класса
SequenceBuilder:

import kotlin.coroutines.experimental.*
//sampleStart
suspend fun SequenceBuilder<Int>.
yieldIfOdd(x: Int) {
if (x % 2 != 0) yield(x)
}
val lazySeq = buildSequence {
for (i in 1..10) yieldIfOdd(i)

253
Язык программирования Kotlin

}
//sampleEnd
fun main(args: Array<String>) {
lazySeq.forEach { print(“$it “) }
}

API высокого уровня

Только базовые API, связанные с сопрограммами, доступны непо-


средственно из стандартной библиотеки Kotlin. Они преимуществен-
но состоят из основных примитивов и интерфейсов, которые, вероят-
но, будут использоваться во всех библиотеках на основе сопрограмм.
Большинство API уровня приложений, основанные на сопрограм-
мах, реализованы в отдельной библиотеке kotlinx.coroutines. Эта библи-
отека содержит в себе:

• Платформенно-зависимое асинхронное программирование


с помощью kotlinx-coroutines-core: этот модуль включает Go-
подобные каналы, которые поддерживают select и другие удач-
ные примитивы
• API, основанные на CompletableFuture из JDK 8:
kotlinx-coroutines-jdk8
• Неблокирующий ввод-вывод (NIO), основанный на API из JDK 7
и выше: kotlinx-coroutines-nio
• Поддержка Swing (kotlinx-coroutines-swing) и JavaFx
(kotlinx-coroutines-javafx)
• Поддержка RxJava: kotlinx-coroutines-rx

Исчерпывающее руководство по библиотеке kotlinx.coroutines, вклю-


чая подробные примеры, доступно по адресу: https://github.com/Kotlin/
kotlinx.coroutines/blob/master/coroutines-guide.md

Сопрограммы: выводы

Корутины — очень мощный функционал, который появился


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

254
Глава 10. Сопрограммы

называемыми «легковесными потоками», подчеркивая тем самым, что


они не соотносятся один к одному на нативные потоки и не подверже-
ны таким проблемам, как deadlocks, starvation и т.д. С корутинами мож-
но не волноваться о блокировке потоков, синхронизации, они выглядят
более прямолинейно.
Корутины также позволяют использовать различные подходы для
написания конкурентного кода, каждый из которых либо уже реализо-
ван в библиотеке (kotlinx.coroutine), либо может быть легко воплощен
с ее помощью.

255
Язык программирования Kotlin

ГЛАВА 11.
КОЛЛЕКЦИИ

ВВЕДЕНИЕ
Язык Kotlin, как и многие другие языки программирования, реализует
прикладной интерфейс для работы с коллекциями.
Коллекция — программный объект, содержащий в себе, тем или
иным образом, набор значений одного или различных типов и позво-
ляющий обращаться к этим значениям.
Коллекция дает возможность записывать в себя значения и извле-
кать их. Назначение коллекции — служить хранилищем объектов и обе-
спечивать доступ к ним. Обычно коллекции используются для хране-
ния групп однотипных объектов, подлежащих стереотипной обработке.
Для обращения к конкретному элементу коллекции могут использо-
ваться различные методы, в зависимости от ее логической организа-
ции. Реализация может допускать выполнение отдельных операций над
коллекциями в целом. Наличие операций над коллекциями во многих
случаях способно существенно упростить программирование.
Прикладной интерфейс для работы с коллекциями в Kotlin разрабо-
тан для достижения нескольких целей:

• Он должен обеспечить высокую производительность при работе


с наборами данных
• Обеспечивать единообразное функционирование коллекций
с высокой степенью взаимодействия
• Должен допускать простое расширение и/или адаптацию

256
Глава 11. Коллекции

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


специального назначения, а также частичные реализации, которые обе-
спечивают возможность создания собственных коллекций.
Алгоритмы составляют важную часть API коллекций. Они определе-
ны в виде методов и функций-расширений. Алгоритмы доступны всем
коллекциям и не требуют реализации их собственной версии в каждом
классе коллекции. Алгоритмы предоставляют стандартные средства для
манипулирования коллекциями.
Важным элементом API коллекций является интерфейс Iterator,
который определяет итератор, обеспечивающий общий, стандартизи-
рованный способ поочередного доступа к элементам коллекций.
Основными типами коллекций являются: Collection, List, Set
и Map.
В отличие от многих языков Kotlin различает изменяемые и неизме-
няемые коллекции (списки, множества, ассоциативные списки и т.д.).
Точный контроль над тем, когда именно коллекции могут быть измене-
ны, полезен для устранения багов и разработки хорошего API.
Важно понимать различие между read-only представлением изменя-
емой коллекции и фактически неизменяемой коллекцией. Их легко соз-
дать, но вот система типов не выражает различие между ними, поэтому
следить за этим должны вы (если это необходимо).

COLLECTION
И MUTABLECOLLECTION
Общая коллекция элементов. Методы в этом интерфейсе поддержива-
ют доступ только для чтения к коллекции. Доступ для чтения / записи
поддерживается через интерфейс MutableCollection.

interface Collection<out E> : Iterable<E>


interface MutableCollection<E> :
Collection<E>,
MutableIterable<E>

Этот интерфейс служит основанием, на котором построен весь API


для работы с коллекциями, поскольку он должен быть реализован все-
ми классами коллекций. Интерфейс Collection является обобщен-
ным и реализует интерфейс Iterable. Это означает, что все коллекции

257
Язык программирования Kotlin

можно перебирать неким образом, например организовать цикл for


или воспользоваться функциями-расширениями, такими как forEach()
или map().
Большинство методов для работы с коллекциями реализованы
в виде функций-расширений. Ниже представлен исчерпывающий спи-
сок таких функций:

all Возвращает true, если все эле-


менты соответствуют заданному
предикату.
any Возвращает true, если коллекция
имеет хотя бы один элемент.
asIterable Возвращает эту коллекцию как
Iterable.
asSequence Создает экземпляр Sequence, ко-
торый обертывает исходную кол-
лекцию, возвращающую ее элемен-
ты при повторении.
associate Возвращает Map, содержащий пары
ключ-значение, предоставляемые
функцией преобразования, при-
меняемой к элементам данной
коллекции.
associateBy Возвращает Map, содержащий эле-
менты из данной коллекции, ин-
дексированные ключом, возвра-
щаемым функцией keySelector,
применяемой к каждому элементу.
associateByTo Заполняет и возвращает целевой
изменяемый Map с парами ключ-
значение, где ключ предоставля-
ется функцией keySelector, при-
меняемой к каждому элементу
данной коллекции, а значение -
это сам элемент.
associateTo Заполняет и возвращает целе-
вой изменяемый Map с парами
ключ-значение, предоставляемыми
функцией преобразования, приме-
няемой к каждому элементу дан-
ной коллекции.

258
Глава 11. Коллекции

contains Возвращает true, если элемент


найден в коллекции.
containsAll Проверяет, содержатся ли все
элементы указанной коллекции
в этой коллекции.
count Возвращает количество элементов
в этой коллекции.
distinct Возвращает список, содержащий
только уникальные элементы из
данной коллекции.
distinctBy Возвращает список, содержащий
только элементы из данного на-
бора, имеющие уникальные клю-
чи, возвращаемые данной функци-
ей селектора.
drop Возвращает список, содержащий
все элементы, кроме первых n
элементов.
dropWhile Возвращает список, содержащий
все элементы, кроме первых эле-
ментов, которые удовлетворяют
заданному предикату.
elementAt Возвращает элемент по заданному
индексу или генерирует исключе-
ние IndexOutOfBoundsException,
если индекс выходит за рамки
этой коллекции.
elementAtOrElse Возвращает элемент в указан-
ном индексе или результат вы-
зова функции defaultValue, если
индекс выходит за рамки этой
коллекции.
elementAtOrNull Возвращает элемент по указанно-
му индексу или null, если ин-
декс не соответствует этой
коллекции.
filter Возвращает список, содержащий
только элементы, соответствую-
щие данному предикату.
filterIndexed Возвращает список, содержащий
только элементы, соответствую-
щие данному предикату.

259
Язык программирования Kotlin

filterIndexedTo Добавляет все элементы, соот-


ветствующие данному предикату,
для данного адресата.
filterIsInstance Возвращает список, содержащий
все элементы, являющиеся эк-
земплярами указанного параме-
тра типа.
filterIsInstanceTo Добавляет все элементы, кото-
рые являются экземплярами ука-
занного параметра R для данно-
го адресата.
filterNot Возвращает список, содержащий
все элементы, не соответствую-
щие данному предикату.
filterNotNull Возвращает список, содержащий
все элементы, которые не явля-
ются null.
filterNotNullTo Добавляет все элементы, которые
не являются нулевыми для данно-
го адресата.
filterNotTo Добавляет все элементы, не со-
ответствующие данному предика-
ту, для данного адресата.
filterTo Добавляет все элементы, соот-
ветствующие данному предикату,
для данного адресата.
find Возвращает первый элемент, со-
ответствующий данному предика-
ту, или null, если такой эле-
мент не найден.
findLast Возвращает последний элемент,
соответствующий данному пре-
дикату, или null, если такой
элемент не найден.
first Возвращает первый элемент.
firstOrNull Возвращает первый элемент или
null, если коллекция пуста.
flatMap Возвращает единственный спи-
сок всех элементов, полученных
из результатов функции преобра-
зования, вызываемых для каждого
элемента исходной коллекции.

260
Глава 11. Коллекции

flatMapTo Добавляет все элементы, полу-


ченные из результатов функции
преобразования, вызываемой для
каждого элемента исходного на-
бора, для данного адресата.
fold Накапливает значение, начиная
с начального значения и при-
меняя операцию слева направо
к текущему значению накопления
и каждому элементу.
foldIndexed Накапливает значение, начиная
с начального значения и при-
меняя операцию слева направо
к текущему значению накопления,
и каждый элемент с его индек-
сом в исходной коллекции.
forEach Выполняет данное действие для
каждого элемента.
forEachIndexed Выполняет данное действие для
каждого элемента, обеспечивая
последовательный индекс с помо-
щью элемента.
groupBy Группирует элементы исходной
коллекции ключом, возвращаемым
данной функцией keySelector,
применяемой к каждому элементу,
и возвращает карту, где каждый
групповой ключ связан со спи-
ском соответствующих элементов.
groupByTo Группирует элементы исходной
коллекции ключом, возвращаемым
данной функцией keySelector,
применяемой к каждому элементу,
и помещает на карту назначения
каждый групповой ключ, связан-
ный со списком соответствующих
элементов.
groupingBy Создает источник группиров-
ки из коллекции, которая будет
использоваться позже, с одной
из групповых операций с ис-
пользованием указанной функции
keySelector для извлечения клю-
ча из каждого элемента.

261
Язык программирования Kotlin

indexOf Возвращает первый индекс эле-


мента или -1, если коллекция
не содержит элемент.
indexOfFirst Возвращает индекс первого эле-
мента, соответствующего дан-
ному предикату, или -1, если
коллекция не содержит такого
элемента.
indexOfLast Возвращает индекс последнего
элемента, соответствующего дан-
ному предикату, или -1, если
коллекция не содержит такого
элемента.
intersect Возвращает набор, содержащий
все элементы, содержащиеся как
этим набором, так и указанной
коллекцией.
isNotEmpty Возвращает true, если коллекция
не пуста.
joinTo Добавляет строку из всех эле-
ментов, разделенных с помощью
разделителя, и с использованием
заданного префикса и постфикса,
если они поставляются.
joinToString Создает строку из всех элемен-
тов, разделенных с помощью раз-
делителя, и с использованием
заданного префикса и постфикса,
если они поставляются.
last Возвращает последний элемент.
lastIndexOf Возвращает последний индекс
элемента или -1, если коллекция
не содержит элемент.
lastOrNull Возвращает последний элемент
или null, если коллекция пуста.
map Возвращает список, содержащий
результаты применения данной
функции преобразования к каж-
дому элементу и его индексу
в исходной коллекции.

262
Глава 11. Коллекции

mapIndexed Возвращает список, содержащий


результаты применения данной
функции преобразования к каж-
дому элементу и его индексу
в исходной коллекции.
mapIndexedNotNull Возвращает список, содержа-
щий только ненулевые результа-
ты применения данной функции
преобразования к каждому эле-
менту и его индексу в исходной
коллекции.
mapIndexedNotNullTo Применяет данную функцию пре-
образования к каждому элементу
и его индексу в исходной кол-
лекции и добавляет только нуле-
вые результаты к данному месту
назначения.
mapIndexedTo Применяет данную функцию пре-
образования к каждому элементу
и его индексу в исходной кол-
лекции и добавляет результаты
к данному месту назначения.
mapNotNull Возвращает список, содержа-
щий только ненулевые результаты
применения данной функции пре-
образования к каждому элементу
исходной коллекции.
mapNotNullTo Применяет заданную функцию пре-
образования к каждому элементу
в исходной коллекции и добав-
ляет только нулевые результаты
для данного адресата.
mapTo Применяет данную функцию пре-
образования к каждому элемен-
ту исходной коллекции и добав-
ляет результаты к данному месту
назначения.
max Возвращает наибольший элемент
или null, если нет элементов.
maxBy Возвращает первый элемент, даю-
щий наибольшее значение данной
функции, или null, если нет
элементов.

263
Язык программирования Kotlin

maxWith Возвращает первый элемент, име-


ющий наибольшее значение в со-
ответствии с предоставленным
компаратором, или null, если
нет элементов.
min Возвращает наименьший элемент
или null, если нет элементов.
minBy Возвращает первый элемент, даю-
щий наименьшее значение данной
функции, или null, если нет
элементов.
minWith Возвращает первый элемент, име-
ющий наименьшее значение в со-
ответствии с предоставленным
компаратором, или null, если
нет элементов.
minus Возвращает список, содержащий
все элементы исходной коллек-
ции без первого вхождения дан-
ного элемента.
minusElement Возвращает список, содержащий
все элементы исходной коллек-
ции без первого вхождения дан-
ного элемента.
none Возвращает true, если в коллек-
ции нет элементов.
orEmpty Возвращает эту коллекцию, если
она не равна null и пустой -
в противном случае
partition Разделяет исходную коллек-
цию в пару списков, где пер-
вый список содержит элементы,
для которых предикат дал true,
а второй список содержит эле-
менты, для которых предикат дал
false.
plus Возвращает список, содержащий
все элементы исходной коллек-
ции, а затем данный элемент.
plusElement Возвращает список, содержащий
все элементы исходной коллек-
ции, а затем данный элемент.

264
Глава 11. Коллекции

reduce Начисляет значение, начиная


с первого элемента и применяя
операцию слева направо к те-
кущему значению аккумулятора
и каждому элементу.
reduceIndexed Начисляет значение, начиная
с первого элемента и применяя
операцию слева направо к те-
кущему значению аккумулятора
и каждому элементу с его ин-
дексом в исходной коллекции.
requireNoNulls Возвращает исходную коллек-
цию, содержащую все ненуле-
вые элементы, бросая исключение
IllegalArgumentException, если
есть нулевые элементы.
reversed Возвращает список с элементами
в обратном порядке.
single Возвращает одиночный элемент
или генерирует исключение, если
коллекция пуста или имеет более
одного элемента.
singleOrNull Возвращает одиночный элемент
или null, если коллекция пуста
или имеет несколько элементов.
sorted Возвращает список всех элемен-
тов, отсортированных в соответ-
ствии с их естественным поряд-
ком сортировки.
sortedBy Возвращает список всех элемен-
тов, отсортированных по есте-
ственному порядку сортировки
значения, возвращаемого указан-
ной функцией выбора.
sortedByDescending Возвращает список всех отсорти-
рованных по убыванию элемен-
тов в соответствии с порядком
естественного порядка значения,
возвращаемого указанной функци-
ей выбора.
sortedDescending Возвращает список всех отсорти-
рованных элементов по их есте-
ственному порядку сортировки.

265
Язык программирования Kotlin

sortedWith Возвращает список всех элемен-


тов, отсортированных в соответ-
ствии с указанным компаратором.
subtract Возвращает набор, содержа-
щий все элементы, содержащиеся
в этой коллекции и не содержа-
щие указанную коллекцию.
sumBy Возвращает сумму всех значений,
созданных функцией селектора,
применяемых к каждому элементу
коллекции.
sumByDouble Возвращает сумму всех значений,
созданных функцией селектора,
применяемых к каждому элементу
коллекции.
take Возвращает список, содержащий
первые n элементов.
takeWhile Возвращает список, содержащий
первые элементы, удовлетворяю-
щие данному предикату.
toBooleanArray Возвращает массив Boolean, со-
держащий все элементы этой
коллекции.
toByteArray Возвращает массив Byte, со-
держащий все элементы этой
коллекции.
toCharArray Возвращает массив Char, со-
держащий все элементы этой
коллекции.
toCollection Добавляет все элементы в данную
целевую коллекцию.
toDoubleArray Возвращает массив Double, со-
держащий все элементы этой
коллекции.
toFloatArray Возвращает массив Float, со-
держащий все элементы этой
коллекции.
toHashSet Возвращает HashSet всех
элементов.
toIntArray Возвращает массив Int, со-
держащий все элементы этой
коллекции.

266
Глава 11. Коллекции

toList Возвращает List, содержащий все


элементы.
toLongArray Возвращает массив Long, со-
держащий все элементы этой
коллекции.
toMutableList Возвращает MutableList, запол-
ненный всеми элементами этой
коллекции.
toMutableSet Возвращает MutableSet, содержа-
щий все отдельные элементы из
данной коллекции.
toSet Возвращает Set из всех
элементов.
toShortArray Возвращает массив Short, со-
держащий все элементы этой
коллекции.
toSortedSet Возвращает SortedSet из всех
элементов.
toTypedArray Возвращает типизированный мас-
сив, содержащий все элементы
этой коллекции.
union Возвращает lazy Iterable из
IndexedValue для каждого эле-
мента исходной коллекции.
zip Возвращает список пар, постро-
енных из элементов обеих кол-
лекций с одинаковыми индексами.
Список имеет длину самой корот-
кой коллекции.

LIST И MUTABLELIST
Общий упорядоченный набор элементов. Методы в этом интерфейсе
поддерживают только доступ для чтения к списку. Доступ для чтения /
записи поддерживается через интерфейс MutableList.

interface List<out E> : Collection<E>


inline fun <T> MutableList(
size: Int,
init: (index: Int) -> T
): MutableList<T>

267
Язык программирования Kotlin

Интерфейс List расширяет интерфейс Collection и определя-


ет такое поведение коллекций, которое сохраняет последовательность
элементов. Доступ к элементам осуществляется по индексу, начиная
с нуля.

SET И MUTABLESET
Общий неупорядоченный набор элементов, который не поддерживает
повторяющиеся элементы. Методы в этом интерфейсе поддерживают
доступ только для чтения к набору; Доступ для чтения / записи поддер-
живается через интерфейс MutableSet.

interface Set<out E> : Collection<E>


interface MutableSet<E> : Set<E>,
MutableCollection<E>

Интерфейс Set определяет множество. Он расширяет интерфейс


Collection и определяет поведение коллекций, не допускающих ду-
блирование элементов.

ИСПОЛЬЗОВАНИЕ КОЛЛЕКЦИЙ
Тип List<out T> в Kotlin — интерфейс, который предоставляет read-
only операции, такие как size, get и другие. Так же как и в Java, он на-
следуется от Collection<T>, а значит, и от Iterable<T>. Методы,
которые изменяют список, добавлены в интерфейс MutableList<T>.
То же самое относится и к Set<out T>/MutableSet<T>, Map<K,
out V>/MutableMap<K, V>.
Ниже приведен пример базового использования списка (List)
и множества (Set):

fun main(args: Array<String>){


val numbers: MutableList<Int> = muta-
bleListOf(1, 2, 3)
val readOnlyView: List<Int> = numbers
println(numbers)
numbers.add(4)

268
Глава 11. Коллекции

println(readOnlyView)
val strings = hashSetOf(“a”, “b”, “c”,
“c”)
assert(strings.size == 3)
println(strings)
}

Программа выведет следующий результат:


[1, 2, 3]
[1, 2, 3, 4]
[a, b, c]

Kotlin не имеет специальных синтаксических конструкций для соз-


дания списков или множеств. Используйте методы из стандартной
библиотеки, такие как listOf(), mutableListOf(), setOf(),
mutableSetOf(), mapOf().
В языке Kotlin read-only типы коллекций ковариантны. Это зна-
чит, что вы можете взять List<Rectangle> (список прямоугольни-
ков) и присвоить его List<Shape> (списку фигур), предполагая, что
Rectangle наследуется от Shape. Такое присвоение было бы запреще-
но с изменяемыми коллекциями, потому что в этом случае появляется
риск возникновения ошибок времени исполнения.

Доступ к коллекциям через итератор

Нередко требуется перебрать все элементы коллекции, например,


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

Методы
hasNext Возвращает true, если итерация имеет
больше элементов.

next Возвращает следующий элемент


в итерации.

269
Язык программирования Kotlin

Функции-расширения
asSequence Создает последовательность, кото-
рая возвращает все элементы из этого
итератора. Последовательность огра-
ничена повторением только один раз.

forEach Выполняет данную операцию для каждо-


го элемента этого итератора.

iterator Возвращает данный итератор. Это по-


зволяет использовать экземпляр ите-
ратора в цикле for.

withIndex Возвращает Iterator, обертываю-


щий каждое значение, созданное этим
Iterator, с индексом IndexedValue,
содержащим значение и его индекс.

Прежде чем обратиться к коллекции через итератор, следует получить


его. В каждом классе коллекций предоставляется метод iterator(),
возвращающий итератор на начало коллекции. Используя объект ите-
ратора, можно получить доступ к каждому элементу коллекции по оче-
реди. В общем, применение итератора для перебора содержимого кол-
лекции сводится к выполнению следующих шагов:

• Установить итератор на начало коллекции


• Организовать цикл, в котором вызывается метод hasNext()
• Получить в цикле каждый элемент коллекции, вызывая метод
next()

Другой способ обхода элементов коллекции можно реализовать че-


рез организацию цикла for. Так как все коллекции реализуют интер-
фейс Iterable, следовательно, ими можно оперировать в цикле for:

val num: List<Int> = listOf(1, 2, 3, 4, 5,


6, 7, 8, 9)
for (n in num)
print(n)

Еще один способ доступа к элементам коллекции заключается в ис-


пользовании функций расширения map(), forEach()

270
Глава 11. Коллекции

num.forEach {
print(it)
}
num.map {
print(it)
}

MAP И MUTABLEMAP

Отображения

Отображение представляет собой объект, сохраняющий связи меж-


ду ключами и значениями в виде пар ключ-значение. По заданному
ключу можно найти его значение. Ключи и значения являются объек-
тами. В отношении отображений следует иметь ввиду, что они не реа-
лизуют интерфейс Iterable. Это значит, что напрямую организовать
перебор значений циклом for не получится. Но в стандартной библио-
теке Kotlin реализован набор функций-расширений (они представлены
ниже), которые позволяют осуществлять работу с Map как с коллекци-
ями и осуществлять перебор значений.

Map и MutableMap

Коллекция, которая содержит пары объектов (ключи и значения)


и поддерживает эффективное извлечение значения, соответствующего
каждому ключу. Ключи Map уникальны. Map содержит только одно зна-
чение для каждого ключа. Методы в этом интерфейсе поддерживают
доступ только для чтения к Map. Доступ на чтение и запись поддержи-
вается через интерфейс MutableMap.

interface Map<K, out V>


interface MutableMap<K, V> : Map<K, V>

Интерфейс Map отображает однозначные ключи на значения.


Ключ — это объект, используемый для последующего извлечения дан-
ных. Задавая ключ и значение, можно размещать значение в отображе-
нии, представленном объектом типа Map. Сохранив значение по ключу,

271
Язык программирования Kotlin

можно получить его обратно по этому же ключу. Интерфейс Map явля-


ется обобщенным.
Обращение с отображениями опирается на две основные операции,
выполняемые методами get() и put().
Хотя отображения и не реализуют интерфейс Collection, тем
не менее можно получить представление отображения в виде коллек-
ции. Для этого можно воспользоваться свойствами: entries, keys,
values.
entries - возвращает Set для всех пар из Map
keys - возвращает Set всех ключей из Map
values - возвращает Collection всех значений из Map

Методы Map
containsKey Возвращает true, если Map содержит
указанный ключ.
containsValue Возвращает true, если Map содержит
один или несколько ключей с указанным
значением.
get Возвращает значение, соответствующее
заданному ключу, или null, если такой
ключ отсутствует на Map.
getOrDefault Возвращает значение, соответствующее
данному ключу, или значение по умол-
чанию, если такой ключ отсутствует
в Map.
isEmpty Возвращает true, если Map не содер-
жит элементов, в противном случае
- false.

Методы MutableMap
clear Удаляет все элементы
put Связывает указанное значение с ука-
занным ключом
putAll Обновляет Map с помощью пар ключ /
значение из указанной Map.
remove Удаляет указанный ключ и его соответ-
ствующее значение из Map.

272
Глава 11. Коллекции

Функции-расширения для Map


all Возвращает true, если все записи со-
ответствуют заданному предикату.
any Возвращает true, если Map имеет
хотя бы одну запись.
asIterable Создает экземпляр Iterable, который
обертывает исходный Map, возвращая ее
записи при повторении.
asSequence Создает экземпляр Sequence, который
обертывает Map, возвращающую ее запи-
си при повторении.
contains Проверяет, содержит ли Map данный
ключ.
containsKey Возвращает true, если Map содержит
указанный ключ.
containsValue Возвращает true, если Map содержит
один или несколько ключей с указанным
значением.
count Возвращает количество записей в Map.
filter Возвращает новую Map, содержащую все
пары ключ-значение, соответствующие
данному предикату.
filterKeys Возвращает Map, содержащую все пары
ключ-значение, с ключами, соответ-
ствующими заданному предикату.
filterNot Возвращает новую Map, содержащую все
пары ключ-значение, не соответствую-
щие данному предикату.
filterNotTo Добавляет все записи, не соответ-
ствующие данному предикату, в Map
назначения.
filterTo Добавляет все записи, соответствующие
данному предикату, в измененную Map,
указанную как параметр назначения.
filterValues Возвращает Map, содержащую все пары
ключ-значение со значениями, соответ-
ствующими заданному предикату.
flatMap Возвращает единый список всех элемен-
тов, полученных из результатов функ-
ции преобразования, вызываемых при
каждой записи исходной Map.

273
Язык программирования Kotlin

flatMapTo Добавляет все элементы, полученные


от результатов функции преобразова-
ния, вызываемых при каждой записи ис-
ходной Map, данному адресату.
forEach Выполняет данное действие для каждой
записи.
get Возвращает значение, соответствующее
заданному ключу, или null, если такой
ключ отсутствует на Map.
getOrDefault Возвращает значение, к которому сопо-
ставляется указанный ключ, или зна-
чение по умолчанию, если эта Map
не содержит сопоставления для ключа.
getOrElse Возвращает значение для данного клю-
ча или результат функции значения
по умолчанию, если для данного ключа
не было записи.
getValue Возвращает значение свойства для дан-
ного объекта из этой Map только для
чтения.
isNotEmpty Возвращает true, если эта Map
не пуста.
iterator Возвращает итератор по элементам
на Map.
map Возвращает список, содержащий резуль-
таты применения данной функции пре-
образования к каждой записи в исход-
ной Map.
mapKeys Возвращает новую Map с записями, име-
ющими ключи, полученные путем приме-
нения функции преобразования к каждой
записи на этой Map и значениям этой
Map.
mapKeysTo Заполняет данную Map назначения с по-
мощью записей, имеющих ключи, полу-
ченные путем применения функции пре-
образования к каждой записи на этой
Map и значениям этой Map.
mapNotNull Возвращает список, содержащий только
ненулевые результаты применения дан-
ной функции преобразования к каждой
записи в исходной Map.

274
Глава 11. Коллекции

mapNotNullTo Применяет данную функцию преобразо-


вания к каждой записи в исходной Map
и добавляет только нулевые результаты
для данного адресата.
mapTo Применяет данную функцию преобразо-
вания к каждой записи исходной Map
и добавляет результаты к данному ме-
сту назначения.
mapValues Возвращает новую Map с элементами,
имеющими ключи этой Map, и значени-
ями, полученными приложением функ-
ции преобразования к каждой записи
на этой Map.
mapValuesTo Задает заданную Map назначения с за-
писями, имеющими ключи этой Map,
и значениями, полученными путем при-
менения функции преобразования к каж-
дой записи на этой Map.
maxBy Возвращает первую запись, дающую наи-
большее значение данной функции, или
null, если нет записей.
maxWith Возвращает первую запись с наибольшим
значением в соответствии с предостав-
ленным компаратором или null, если
нет записей.
minBy Возвращает первую запись с наименьшим
значением данной функции или null,
если нет записей.
minWith Возвращает первую запись с наименьшим
значением в соответствии с предостав-
ленным компаратором или null, если
нет записей.
minus Возвращает Map, содержащую все запи-
си исходной Map, кроме записи с дан-
ным ключом.
none Возвращает true, если Map не имеет
записей.
onEach Выполняет данное действие для каждой
записи и затем возвращает Map.
orEmpty Возвращает Map, если она не является
нулевой или пустой Map.

275
Язык программирования Kotlin

plus Создает новую Map только для чтения,


заменяя или добавляя запись на эту
карту из данной пары ключ-значение.
toList Возвращает список, содержащий все
пары ключ-значение.
toMap Возвращает новую Map только для чте-
ния, содержащую все пары ключ-
значение из исходной Map.
toMutableMap Возвращает новую изменчивую Map, со-
держащую все пары ключ-значение из
исходной Map.
toProperties Преобразует эту Map в объект
Properties.
toSortedMap Преобразует эту Map в SortedMap, по-
этому последовательность итераций бу-
дет в порядке.
withDefault Возвращает оболочку этой Map толь-
ко для чтения, имеющую неявное значе-
ние по умолчанию, указанное с указан-
ной функцией defaultValue.

API для работы с коллекциями в Kotlin предоставляет эффективный


ряд тщательно спроектированных решений, связанных с сохранением
и извлечением данных в/из коллекций.

276
Глава 12. Другие особенности языка

ГЛАВА 12.
ДРУГИЕ ОСОБЕННОСТИ
ЯЗЫКА

КЛЮЧЕВОЕ СЛОВО THIS


Иногда нам необходимо сослаться на объект, с которым мы работаем.
Для этой цели в языке Kotlin служит ключевое слово this:

• Внутри класса ключевое слово this ссылается на объект этого


класса
• В функциях-расширениях или в литерале функции с принимаю-
щим объектом this обозначает принимающий объект, который
передается слева от точки

Если ключевое слово this не имеет определителей, то оно ссылает-


ся на область самого глубокого замыкания. Чтобы сослаться на this
в одной из внешних областей, используются метки-определители.
Чтобы получить доступ к this из внешней области (класса, функ-
ции-расширения или именованных литералов функций с принимаю-
щим объектом), мы пишем this@label, где @label — это метка об-
ласти, из которой нужно получить this:

class A { // неявная метка @A


inner class B { // неявная метка @B
fun Int.foo() { // неявная метка @
foo

277
Язык программирования Kotlin

val a = this@A // this из A


val b = this@B // this из B
val c = this // принимающий
объект функции foo(), типа Int
val c1 = this@foo //
принимающий объект функции foo(), типа Int
val funLit = lambda@ fun
String.() {
val d = this //
принимающий объект литерала funLit
}

val funLit2 = { s: String ->


// принимающий объект
функции foo(),
// т.к. замыкание лямбды
не имеет принимающего объекта
val d1 = this
}
}
}
}

ИНТЕРВАЛЫ
В языке Kotlin реализована очень полезная функциональность под
названием интервалы. Интервалы оформлены с помощью функций
rangeTo() и имеют оператор в виде (..), который дополняется
in и !in. Они применимы ко всем сравниваемым (comparable) типам.
В общем виде интервал может быть представлен следующим образом:

variable in | !in from[..to[downTo]|until]


[step]

Интервалы реализуют интерфейс ClosedRange<T>.


Говоря математическим языком, интерфейс ClosedRange<T>
обозначет ограниченный отрезок и предназначен для типов,

278
Глава 12. Другие особенности языка

подлежащих сравнению. У него есть две контрольные точки: start


и endInclusive. Главной операцией является contain. Чаще всего
она используется вместе с операторами in/!in.
Целочисленные последовательности (IntProgression,
LongProgression, CharProgression) являются арифметически-
ми. Последовательности определены элементами first, last и нену-
левым значением increment. Элемент first является первым, по-
следующими являются элементы, полученные при инкрементации
предыдущего элемента с помощью increment. Если последователь-
ность не является пустой, то элемент last всегда достигается в резуль-
тате инкрементации.
Последовательность является подтипом Iterable<N>, где N — это
Int, Long или Char. Таким образом, ее можно использовать в циклах
for и функциях типа map, filter и т.п.
Для целочисленных типов оператор (..) создает объект, который
реализует в себе ClosedRange<T> и *Progression. К примеру,
IntRange наследуется от класса IntProgression и реализует ин-
терфейс ClosedRange<Int>. Поэтому все операторы, обозначенные
для IntProgression, также доступны и для IntRange. Результатом
функций downTo() и step() всегда будет *Progression.
Последовательности спроектированы с использованием функции
fromClosedRange в их вспомогательном объекте (companion object):

IntProgression.fromClosedRange(start, end,
increment)

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


ся элемент last. Для последовательности с положительным инкрементом
этот элемент вычисляется так, чтобы он был не больше элемента end. Для
тех последовательностей, где инкремент отрицательный — не меньше.

Вспомогательные функции

rangeTo()

Операторы rangeTo() для целочисленных типов просто вызывают


конструктор класса *Range:

279
Язык программирования Kotlin

class Int {
//...
operator fun rangeTo(other: Long): Lon-
gRange = LongRange(this, other)
//...
operator fun rangeTo(other: Int): In-
tRange = IntRange(this, other)
//...
}

Числа с плавающей точкой (Double, Float) не имеют своего опе-


ратора rangeTo. Такой оператор обозначен для них в дженериках типа
Comparable стандартной библиотеки:

public operator fun <T: Comparable<T>>


T.rangeTo(that: T): ClosedRange<T>

Интервал, полученный с помощью такой функции,


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

downTo()

Функция-расширение downTo() задана для любой пары целочис-


ленных типов, вот два примера:

fun Long.downTo(other: Int): LongProgression


{
return LongProgression.
fromClosedRange(this, other, -1.0)
}
fun Byte.downTo(other: Int): IntProgression
{
return IntProgression.
fromClosedRange(this, other, -1)
}

280
Глава 12. Другие особенности языка

reversed()

Функция reversed() расширяет класс *Progression таким об-


разом, что все экземпляры этого класса возвращают обратные последо-
вательности при ее вызове:

fun IntProgression.reversed(): IntProgres-


sion {
return IntProgression.
fromClosedRange(last, first, -increment)
}

step()

Функция-расширение step() также определена для классов


*Progression. Она возвращает последовательность с измененным
значением шага step (параметр функции). Значение шага всегда долж-
но быть положительным числом для того, чтобы функция никогда
не меняла направления своей итерации:

fun IntProgression.step(step: Int): IntPro-


gression {
if (step <= 0) throw
IllegalArgumentException(“Step must be pos-
itive, was: $step”) //шаг должен быть
положительным
return IntProgression.
fromClosedRange(first, last, if (increment >
0) step else -step)
}
fun CharProgression.step(step: Int):
CharProgression {
if (step <= 0) throw
IllegalArgumentException(“Step must be posi-
tive, was: $step”)
return CharProgression.
fromClosedRange(first, last, step)
}

281
Язык программирования Kotlin

Элемент last

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


элемент last. Обратите внимание, что значение элемента last в воз-
вращенной последовательности может отличаться от значения last
первоначальной последовательности с тем, чтобы предотвратить инва-
риант (last - first) % increment == 0.

(1..12 step 2).last == 11 //


последовательность чисел со значениями [1,
3, 5, 7, 9, 11]
(1..12 step 3).last == 10 //
последовательность чисел со значениями [1,
4, 7, 10]
(1..12 step 4).last == 9 //
последовательность чисел со значениями [1,
5, 9]

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

// equivalent of 1 <= i && i <= 10


if (i in 1..10) {
println(i)
}
for (i in 1..4) print(i) // prints “1234”
for (i in 4..1) print(i) // prints nothing
for (i in 4 downTo 1) print(i) // prints
“4321”
for (i in 1..4 step 2) print(i) // prints
“13”
for (i in 4 downTo 1 step 2) print(i) //
prints “42”
for (i in 1 until 10) { // i in [1, 10),
10 is excluded
println(i)
}

282
Глава 12. Другие особенности языка

NULL-БЕЗОПАСНОСТЬ
Система типов в языке Kotlin нацелена на то, чтобы искоренить опас-
ность обращения к null значениям, более известную как «Ошибка
на миллион».
Самым распространенным подводным камнем многих языков про-
граммирования, в том числе Java, является попытка произвести доступ
к null значению. Это приводит к ошибке. В Java такая ошибка называ-
ется NullPointerException (сокр. «NPE»).
Язык Kotlin призван исключить ошибки подобного рода. NPE могут
возникать только в случае:
• Явного указания throw NullPointerException()
• Использования оператора !!
• Эту ошибку вызвал внешний код
• Есть какое-то несоответствие при инициализации данных
(в конструкторе использована ссылка this на данные, которые
не были еще проинициализированы)

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


значение null (nullable ссылки), и те, которые таковыми быть не мо-
гут (non-null ссылки). К примеру, переменная часто используемого типа
String не может быть null:

var a: String = “abc”


a = null // ошибка компиляции

Для того чтобы разрешить null значение, мы можем объявить эту


строковую переменную как String?:

var b: String? = “abc”


b = null // ok

Теперь при вызове метода с использованием переменной a исключе-


ны какие-либо NPE. Вы спокойно можете писать:

val l = a.length

Но в случае, если вы захотите получить доступ к значению b, это бу-


дет небезопасно. Компилятор предупредит об ошибке:

283
Язык программирования Kotlin

val l = b.length // ошибка: переменная `b`


может быть null

Но нам по-прежнему надо получить доступ к этому свойству/значе-


нию. Есть несколько способов этого достичь:
• Проверка на null
• Безопасные вызовы
• Элвис-оператор
• Оператор !!
• Безопасные приведения типов

Проверка на null

Вы можете явно проверить b на null значение и обработать два ва-


рианта по отдельности:

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

Компилятор отслеживает информацию о проведенной вами провер-


ке и позволяет вызывать lenght внутри блока if. Обратите внима-
ние: это работает только в том случае, если b является неизменной пе-
ременной. Например, если это локальная переменная, значение которой
не изменяется в период между его проверкой и использованием. Так-
же такой переменной может служить val. В противном случае может
так оказаться, что переменная b изменила свое значение на null по-
сле проверки.

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

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

val l = b?.length

Этот код возвращает b.lenght в том случае, если b не имеет зна-


чение null. Иначе он возвращает null. Типом этого выражения бу-
дет Int?.
Такие безопасные вызовы полезны в цепочках. К примеру, если
Bob, Employee (работник), может быть прикреплен (или нет) к отделу
Department и у отдела может быть управляющий, другой Employee. Для

284
Глава 12. Другие особенности языка

того чтобы обратиться к имени этого управляющего (если такой есть),


напишем:

bob?.department?.head?.name

Такая цепочка вернет null в случае, если одно из свойств имеет зна-
чение null.
Для проведения каких-либо операций над non-null значениями вы
можете использовать let оператор вместе с оператором безопасного
вызова:

val listWithNulls: List<String?> =


listOf(“A”, null)
for (item in listWithNulls) {
item?.let { println(it) } // выводит A
и игнорирует null
}

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

Если у нас есть nullable ссылка r, мы можем либо провести проверку


этой ссылки и использовать ее, либо использовать non-null значение x:

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


-1

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

val l = b?.length ?: -1

Если выражение, стоящее слева от элвис-оператора, не является


null, то элвис-оператор его вернет. В противном случае в качестве воз-
вращаемого значения послужит то, что стоит справа. Обращаем ваше
внимание на то, что часть кода, расположенная справа, выполняется
ТОЛЬКО в случае, если слева получается null.
Так как throw и return тоже являются выражениями в Kotlin, их
также можно использовать справа от элвис-оператора. Это может быть
крайне полезным для проверки аргументов функции:

285
Язык программирования Kotlin

fun foo(node: Node): String? {


val parent = node.getParent() ?: return
null
val name = node.getName() ?: throw
IllegalArgumentException(“name expected”)
// ...
}

Оператор !!

Для любителей NPE сушествует еще один способ. Мы можем напи-


сать b!!, и это либо вернет нам non-null значение b (в нашем при-
мере вернется String), либо выкинет NPE:

val l = b!!.length

В случае если вам нужен NPE, вы можете заполучить его только пу-
тем явного указания.

Безопасные приведения типов

Обычное приведение типа может вызвать ClassCastException


в случае, если объект имеет другой тип. Можно использовать безопас-
ное приведение, которое вернет null, если попытка не удалась:

val aInt: Int? = a as? Int

286
Глава 13. Грамматика языка

ГЛАВА 13.
ГРАММАТИКА ЯЗЫКА

ГРАММАТИКА
В этом разделе представлена грамматическая нотация языка Kotlin.

Syntax
start
kotlinFile
: preamble topLevelObject*
;
start
script
: preamble expression*
;
preamble (used by script, kotlinFile)
: fileAnnotations? packageHeader? import*
;
fileAnnotations (used by preamble)
: fileAnnotation*
;
fileAnnotation (used by fileAnnotations)
: “@” “file” “:” (“[“ unescapedAnnotation+ “]”
| unescapedAnnotation)
;
packageHeader (used by preamble)
: modifiers “package” SimpleName{“.”} SEMI?

287
Язык программирования Kotlin

;
import (used by preamble)
: “import” SimpleName{“.”} (“.” “*” | “as”
SimpleName)? SEMI?
;

topLevelObject (used by kotlinFile)


: class
: object
: function
: property
: typeAlias
;
typeAlias (used by memberDeclaration, declara-
tion, topLevelObject)
: modifiers “typealias” SimpleName typeParame-
ters? “=” type
;
Classes
class (used by memberDeclaration, declaration,
topLevelObject)
: modifiers (“class” | “interface”) SimpleName
typeParameters?
primaryConstructor?
(“:” annotations delegationSpecifi-
er{“,”})?
typeConstraints
(classBody? | enumClassBody)
;
primaryConstructor (used by class, object)
: (modifiers “constructor”)? (“(“ functionPa-
rameter{“,”} “)”)
;
classBody (used by objectLiteral, enumEntry,
class, companionObject, object)
: (“{“ members “}”)?
;
members (used by enumClassBody, classBody)
: memberDeclaration*
;
delegationSpecifier (used by objectLiteral,

288
Глава 13. Грамматика языка

class, companionObject, object)


: constructorInvocation
: userType
: explicitDelegation
;
explicitDelegation (used by delegationSpecifier)
: userType “by” expression
;
typeParameters (used by typeAlias, class, prop-
erty, function)
: “<” typeParameter{“,”} “>”
;
typeParameter (used by typeParameters)
: modifiers SimpleName (“:” userType)?
;
typeConstraints (used by class, property, func-
tion)
: (“where” typeConstraint{“,”})?
;
typeConstraint (used by typeConstraints)
: annotations SimpleName “:” type
;
Class members
memberDeclaration (used by members)
: companionObject
: object
: function
: property
: class
: typeAlias
: anonymousInitializer
: secondaryConstructor
;
anonymousInitializer (used by memberDeclaration)
: “init” block
;
companionObject (used by memberDeclaration)
: modifiers “companion” “object” SimpleName?
(“:” delegationSpecifier{“,”})? classBody?
;
valueParameters (used by secondaryConstructor,

289
Язык программирования Kotlin

function)
: “(“ functionParameter{“,”}? “)”
;
functionParameter (used by valueParameters, pri-
maryConstructor)
: modifiers (“val” | “var”)? parameter (“=”
expression)?
;
block (used by catchBlock, anonymousInitializer,
secondaryConstructor, functionBody, controlStruc-
tureBody, try, finallyBlock)
: “{“ statements “}”
;
function (used by memberDeclaration, declara-
tion, topLevelObject)
: modifiers “fun”
typeParameters?
(type “.”)?
SimpleName
typeParameters? valueParameters (“:”
type)?
typeConstraints
functionBody?
;
functionBody (used by getter, setter, function)
: block
: “=” expression
;
variableDeclarationEntry (used by for, lambdaPa-
rameter, property, multipleVariableDeclarations)
: SimpleName (“:” type)?
;
multipleVariableDeclarations (used by for, lamb-
daParameter, property)
: “(“ variableDeclarationEntry{“,”} “)”
;
property (used by memberDeclaration, declara-
tion, topLevelObject)
: modifiers (“val” | “var”)
typeParameters?
(type “.”)?

290
Глава 13. Грамматика языка

(multipleVariableDeclarations | vari-
ableDeclarationEntry)
typeConstraints
(“by” | “=” expression SEMI?)?
(getter? setter? | setter? getter?) SEMI?
;
getter (used by property)
: modifiers “get”
: modifiers “get” “(“ “)” (“:” type)? func-
tionBody
;
setter (used by property)
: modifiers “set”
: modifiers “set” “(“ modifiers (SimpleName |
parameter) “)” functionBody
;
parameter (used by functionType, setter, func-
tionParameter)
: SimpleName “:” type
;
object (used by memberDeclaration, declaration,
topLevelObject)
: “object” SimpleName primaryConstructor? (“:”
delegationSpecifier{“,”})? classBody?
secondaryConstructor (used by memberDeclaration)
: modifiers “constructor” valueParameters (“:”
constructorDelegationCall)? block
;
constructorDelegationCall (used by secondaryCon-
structor)
: “this” valueArguments
: “super” valueArguments
;
Enum classes
enumClassBody (used by class)
: “{“ enumEntries (“;” members)? “}”
;
enumEntries (used by enumClassBody)
: (enumEntry{“,”} “,”? “;”?)?
;
enumEntry (used by enumEntries)

291
Язык программирования Kotlin

: modifiers SimpleName (“(“ arguments “)”)?


classBody?
;
Types
type (used by namedInfix, simpleUserType, getter,
atomicExpression, whenCondition, property, type-
Arguments, function, typeAlias, parameter, func-
tionType, variableDeclarationEntry, lambdaParam-
eter, typeConstraint)
: typeModifiers typeReference
;
typeReference (used by typeReference, nullable-
Type, type)
: “(“ typeReference “)”
: functionType
: userType
: nullableType
: “dynamic”
;
nullableType (used by typeReference)
: typeReference “?”
;
userType (used by typeParameter, catchBlock,
callableReference, typeReference, delegationSpec-
ifier, constructorInvocation, explicitDelegation)
: simpleUserType{“.”}
;
simpleUserType (used by userType)
: SimpleName (“<” (optionalProjection type |
“*”){“,”} “>”)?
;
optionalProjection (used by simpleUserType)
: varianceAnnotation
;
functionType (used by typeReference)
: (type “.”)? “(“ parameter{“,”}? “)” “->”
type?
;
Control structures
controlStructureBody (used by whenEntry, for,
if, doWhile, while)

292
Глава 13. Грамматика языка

: block
: blockLevelExpression
;
if (used by atomicExpression)
: “if” “(“ expression “)” controlStructureBody
SEMI? (“else” controlStructureBody)?
;
try (used by atomicExpression)
: “try” block catchBlock* finallyBlock?
;
catchBlock (used by try)
: “catch” “(“ annotations SimpleName “:”
userType “)” block
;
finallyBlock (used by try)
: “finally” block
;
loop (used by atomicExpression)
: for
: while
: doWhile
;
for (used by loop)
: “for” “(“ annotations (multipleVari-
ableDeclarations | variableDeclarationEntry)
“in” expression “)” controlStructureBody
;
while (used by loop)
: “while” “(“ expression “)” controlStructure-
Body
;
doWhile (used by loop)
: “do” controlStructureBody “while” “(“ ex-
pression “)”
;
Expressions
Precedence
Precedence Title Symbols
Highest Postfix ++, --, ., ?., ?
Prefix -, +, ++, --, !, labelDefinition@
Type RHS :, as, as?

293
Язык программирования Kotlin

Multiplicative *, /, %
Additive +, -
Range ..
Infix function SimpleName
Elvis ?:
Named checks in, !in, is, !is
Comparison <, >, <=, >=
Equality ==, \!==
Conjunction &&
Disjunction ||
Lowest Assignment =, +=, -=, *=, /=, %=
Rules
expression (used by for, atomicExpression, long-
Template, whenCondition, functionBody, doWhile,
property, script, explicitDelegation, jump,
while, arrayAccess, blockLevelExpression, if,
when, valueArguments, functionParameter)
: disjunction (assignmentOperator disjunc-
tion)*
;
disjunction (used by expression)
: conjunction (“||” conjunction)*
;
conjunction (used by disjunction)
: equalityComparison (“&&” equalityCompari-
son)*
;
equalityComparison (used by conjunction)
: comparison (equalityOperation comparison)*
;
comparison (used by equalityComparison)
: namedInfix (comparisonOperation namedInfix)*
;
namedInfix (used by comparison)
: elvisExpression (inOperation elvisExpres-
sion)*
: elvisExpression (isOperation type)?
;
elvisExpression (used by namedInfix)
: infixFunctionCall (“?:” infixFunctionCall)*
;

294
Глава 13. Грамматика языка

infixFunctionCall (used by elvisExpression)


: rangeExpression (SimpleName rangeExpres-
sion)*
;
rangeExpression (used by infixFunctionCall)
: additiveExpression (“..” additiveExpres-
sion)*
;
additiveExpression (used by rangeExpression)
: multiplicativeExpression (additiveOperation
multiplicativeExpression)*
;
multiplicativeExpression (used by additiveEx-
pression)
: typeRHS (multiplicativeOperation typeRHS)*
;
typeRHS (used by multiplicativeExpression)
: prefixUnaryExpression (typeOperation prefixU-
naryExpression)*
;
prefixUnaryExpression (used by typeRHS)
: prefixUnaryOperation* postfixUnaryExpression
;
postfixUnaryExpression (used by prefixUnaryExpres-
sion, postfixUnaryOperation)
: atomicExpression postfixUnaryOperation*
: callableReference postfixUnaryOperation*
;
callableReference (used by postfixUnaryExpres-
sion)
: (userType “?”*)? “::” SimpleName typeArgu-
ments?
;
atomicExpression (used by postfixUnaryExpression)
: “(“ expression “)”
: literalConstant
: functionLiteral
: “this” labelReference?
: “super” (“<” type “>”)? labelReference?
: if
: when

295
Язык программирования Kotlin

: try
: objectLiteral
: jump
: loop
: SimpleName
;
labelReference (used by atomicExpression, jump)
: “@” ++ LabelName
;
labelDefinition (used by prefixUnaryOperation, an-
notatedLambda)
: LabelName ++ “@”
;
literalConstant (used by atomicExpression)
: “true” | “false”
: stringTemplate
: NoEscapeString
: IntegerLiteral
: HexadecimalLiteral
: CharacterLiteral
: FloatLiteral
: “null”
;
stringTemplate (used by literalConstant)
: “\”” stringTemplateElement* “\””
;
stringTemplateElement (used by stringTemplate)
: RegularStringPart
: ShortTemplateEntryStart (SimpleName |
“this”)
: EscapeSequence
: longTemplate
;
longTemplate (used by stringTemplateElement)
: “${“ expression “}”
;
declaration (used by statement)
: function
: property
: class
: typeAlias

296
Глава 13. Грамматика языка

: object
;
statement (used by statements)
: declaration
: blockLevelExpression
;
blockLevelExpression (used by statement, con-
trolStructureBody)
: annotations (“\n”)+ expression
;
multiplicativeOperation (used by multiplicative-
Expression)
: “*” : “/” : “%”
;
additiveOperation (used by additiveExpression)
: “+” : “-”
;
inOperation (used by namedInfix)
: “in” : “!in”
;
typeOperation (used by typeRHS)
: “as” : “as?” : “:”
;
isOperation (used by namedInfix)
: “is” : “!is”
;
comparisonOperation (used by comparison)
: “<” : “>” : “>=” : “<=”
;
equalityOperation (used by equalityComparison)
: “!=” : “==”
;
assignmentOperator (used by expression)
: “=”
: “+=” : “-=” : “*=” : “/=” : “%=”
;
prefixUnaryOperation (used by prefixUnaryExpres-
sion)
: “-” : “+”
: “++” : “--”
: “!”

297
Язык программирования Kotlin

: annotations
: labelDefinition
;
postfixUnaryOperation (used by postfixUnaryExpres-
sion)
: “++” : “--” : “!!”
: callSuffix
: arrayAccess
: memberAccessOperation postfixUnaryExpression
;
callSuffix (used by constructorInvocation, post-
fixUnaryOperation)
: typeArguments? valueArguments annotatedLamb-
da
: typeArguments annotatedLambda
;
annotatedLambda (used by callSuffix)
: (“@” unescapedAnnotation)* labelDefinition?
functionLiteral
memberAccessOperation (used by postfixUnaryOpera-
tion)
: “.” : “?.” : “?”
;
typeArguments (used by callSuffix, callableRefer-
ence, unescapedAnnotation)
: “<” type{“,”} “>”
;
valueArguments (used by callSuffix, construc-
torDelegationCall, unescapedAnnotation)
: “(“ (SimpleName “=”)? “*”? expression{“,”}
“)”
;
jump (used by atomicExpression)
: “throw” expression
: “return” ++ labelReference? expression?
: “continue” ++ labelReference?
: “break” ++ labelReference?
;
functionLiteral (used by atomicExpression, anno-
tatedLambda)
: “{“ statements “}”

298
Глава 13. Грамматика языка

: “{“ lambdaParameter{“,”} “->” statements “}”


;
lambdaParameter (used by functionLiteral)
: variableDeclarationEntry
: multipleVariableDeclarations (“:” type)?
;
statements (used by block, functionLiteral)
: SEMI* statement{SEMI+} SEMI*
;
constructorInvocation (used by delegationSpecifi-
er)
: userType callSuffix
;
arrayAccess (used by postfixUnaryOperation)
: “[“ expression{“,”} “]”
;
objectLiteral (used by atomicExpression)
: “object” (“:” delegationSpecifier{“,”})?
classBody
;
When-expression
when (used by atomicExpression)
: “when” (“(“ expression “)”)? “{“
whenEntry*
“}”
;
whenEntry (used by when)
: whenCondition{“,”} “->” controlStructureBody
SEMI
: “else” “->” controlStructureBody SEMI
;
whenCondition (used by whenEntry)
: expression
: (“in” | “!in”) expression
: (“is” | “!is”) type
;
Modifiers
modifiers (used by typeParameter, getter, pack-
ageHeader, class, property, function, typeAlias,
secondaryConstructor, enumEntry, setter, compan-
ionObject, primaryConstructor, functionParameter)

299
Язык программирования Kotlin

: (modifier | annotations)*
;
typeModifiers (used by type)
: (suspendModifier | annotations)*
;
modifier (used by modifiers)
: classModifier
: accessModifier
: varianceAnnotation
: memberModifier
: parameterModifier
: typeParameterModifier
: functionModifier
: propertyModifier
;
classModifier (used by modifier)
: “abstract”
: “final”
: “enum”
: “open”
: “annotation”
: “sealed”
: “data”
;
memberModifier (used by modifier)
: “override”
: “open”
: “final”
: “abstract”
: “lateinit”
;
accessModifier (used by modifier)
: “private”
: “protected”
: “public”
: “internal”
;
varianceAnnotation (used by modifier, optional-
Projection)
: “in”
: “out”

300
Глава 13. Грамматика языка

;
parameterModifier (used by modifier)
: “noinline”
: “crossinline”
: “vararg”
;
typeParameterModifier (used by modifier)
: “reified”
;
functionModifier (used by modifier)
: “tailrec”
: “operator”
: “infix”
: “inline”
: “external”
: suspendModifier
;
propertyModifier (used by modifier)
: “const”
;
suspendModifier (used by typeModifiers, function-
Modifier)
: “suspend”
;
Annotations
annotations (used by catchBlock, prefixUnaryOper-
ation, blockLevelExpression, for, typeModifiers,
class, modifiers, typeConstraint)
: (annotation | annotationList)*
;
annotation (used by annotations)
: “@” (annotationUseSiteTarget “:”)? unes-
capedAnnotation
;
annotationList (used by annotations)
: “@” (annotationUseSiteTarget “:”)? “[“ unes-
capedAnnotation+ “]”
;
annotationUseSiteTarget (used by annotation, an-
notationList)
: “field”

301
Язык программирования Kotlin

: “file”
: “property”
: “get”
: “set”
: “receiver”
: “param”
: “setparam”
: “delegate”
;
unescapedAnnotation (used by annotation, fileAn-
notation, annotatedLambda, annotationList)
: SimpleName{“.”} typeArguments? valueArgu-
ments?
;
Lexical structure
helper
Digit (used by IntegerLiteral, HexDigit)
: [“0”..”9”];
IntegerLiteral (used by literalConstant)
: Digit (Digit | “_”)*
FloatLiteral (used by literalConstant)
: <Java double literal>;
helper
HexDigit (used by RegularStringPart, Hexadeci-
malLiteral)
: Digit | [“A”..”F”, “a”..”f”];
HexadecimalLiteral (used by literalConstant)
: “0x” HexDigit (HexDigit | “_”)*;
CharacterLiteral (used by literalConstant)
: <character as in Java>;
NoEscapeString (used by literalConstant)
: <”””-quoted string>;
RegularStringPart (used by stringTemplateEle-
ment)
: <any character other than backslash, quote,
$ or newline>
ShortTemplateEntryStart:
: “$”
EscapeSequence:
: UnicodeEscapeSequence | RegularEscapeSe-
quence

302
Глава 13. Грамматика языка

UnicodeEscapeSequence:
: “\u” HexDigit{4}
RegularEscapeSequence:
: “\” <any character other than newline>
SEMI (used by whenEntry, if, statements, pack-
ageHeader, property, import)
: <semicolon or newline>;
SimpleName (used by typeParameter, catchBlock,
simpleUserType, atomicExpression, LabelName,
packageHeader, class, object, infixFunctionCall,
function, typeAlias, parameter, callableRefer-
ence, variableDeclarationEntry, stringTemplateEl-
ement, enumEntry, setter, import, companionOb-
ject, valueArguments, unescapedAnnotation,
typeConstraint)
: <java identifier>
: “`” <java identifier> “`”
;
LabelName (used by labelReference, labelDefini-
tion)
: “@” SimpleName;

303
Учебная литература

Сергей Пименов

Язык программирования
Kotlin
Литературный редактор С. Альперт
Дизайн обложки С. Пименов
Верстка В. Мартыновский

Подписано к печати 11.09.17. Формат 70×100/16


Печать офсетная. Бумага книжная
Усл. печ. л. 24,7. Гарнитура мириад
Тираж 1000. Заказ №175.

Издательство ООО «Агентство «IPIO»


01042, г. Киев, ул. Академика Филатова, 10-А, оф. 2/47
Свидетельство субъекта издательского дела ДК 5142
www.ipio-books.com

Отпечатано в ООО «БИ ТУ БИ ГРУПП»


01033, г. Киев, ул. Владимирская, 69, оф. 428

Вам также может понравиться