Объектно-ориентированное программирование
Классы и объекты
Kotlin поддерживает объектно-ориентированную парадигму программирования, а это значит, что программу на данном языке можно представить в виде взаимодействующих между собой объектов.
Класс фактически представляет новый тип данных, поэтому мы можем определять переменные этого типа:
В функции main определены три переменных типа Person. Стоит также отметить, что в отличие от других объектно-ориентированных языков (как C# или Java), функция main в Kotlin не помещается в отдельных класс, а всегда определяется вне какого-либо класса.
Для создания объекта класса необходимо вызвать конструктор данного класса. Конструктор фактически представляет функцию, которая называется по имени класса и которая выполняет инициализацию объекта. По умолчанию для класса компилятор генерирует пустой конструктор, который мы можем использовать:
Часть кода после знака равно Person() как раз и представляет вызов конструктора, который создает объект класса Person. До вызова конструктора переменная класса не указывает ни на какой объект.
Например, создадим три объекта класса Person:
Свойства
Свойство должно быть инициализировано, то есть обязательно должно иметь начальное значение. Например, определим пару свойств:
В данном случае в классе Person, который представляет человека, определены свойства name (имя человека) и age (возраст человека). И эти свойства инициализированы начальными значениями.
Для обращения к свойствам используется имя переменной, которая предствляет объект, и после точки указывается имя свойства. Например, получение значения свойства:
Установка значения свойства:
Функции класса
Класс также может содержать функции. Функции определяют поведение объектов данного класса. Такие функции еще называют member functions или функции-члены класса. Например, определим класс с функциями:
В функциях, которые определены внутри класса, доступны свойства этого класса. Так, в данном случае в функциях можно обратиться к свойствам name и age, которые определены в классе Person.
Для обращения к функциям класса необходимо использовать имя объекта, после которого идет название функции и в скобках значения для параметров этой функции:
Kotlin. Ключевое слово object.
Ключевое слово object позволяет одновременно объявить класс и создать его экземпляр (или другими словами, объект). При этом использовать его можно по-разному:
Объявление объекта
Наверняка вам приходилось хоть раз создавать такой класс, который должен существовать в одном экземпляре. Обычно это реализуется при помощи паттерна проектирования синглтон. В Kotlin же предлагается использовать объявление объекта, которое одновременно сочетает в себе и объявление класса, и создание его единственного экземпляра.
Объекты можно объявлять внутри класса. Такие объекты тоже существуют в единственном экземпляре, т.е. у любого экземпляра класса будет один и тот же экземпляр объекта.
Объект-компаньон (Companion Object)
Как правило объекты-компаньоны используются для объявления переменных и функций, к которым требуется обращаться без создания экземпляра класса. Либо для объявления констант. По сути они своего рода замена статическим членам класса (в отличие от Java, в Kotlin нет статики).
Объект-выражение
При разработке приложений анонимный объект чаще всего используется для реализации обработчика событий (клики по компонентам экрана).
В данном примере создаётся объект, который реализует интерфейс View.OnClickListener и передаётся функции setOnClickListener() в качестве параметра. Этот параметр и является объектом-выражением.
Обратите внимание, что для объекта-выражения не указывается имя. Зачем оно ему, если объект просто передается функции в качестве параметра?
Если же объекту всё таки требуется имя, то его можно сохранить в переменной.
В объекте-выражении можно обращаться к переменным, которые находятся в той же функции, в которой был создан анонимный объект.
Анонимный объект может реализовывать несколько интерфейсов, тогда они перечисляются через запятую. А может и вовсе не реализовывать ни один.
Обратите внимание, что анонимные объекты не являются синглтонами. Каждый раз при выполнении объекта-выражения создаётся новый объект.
Полезные ссылки
Object expressions and declarations
Sometimes you need to create an object that is a slight modification of some class, without explicitly declaring a new subclass for it. Kotlin can handle this with object expressions and object declarations.
Object expressions
Object expressions create objects of anonymous classes, that is, classes that aren’t explicitly declared with the class declaration. Such classes are useful for one-time use. You can define them from scratch, inherit from existing classes, or implement interfaces. Instances of anonymous classes are also called anonymous objects because they are defined by an expression, not a name.
Creating anonymous objects from scratch
Object expressions start with the object keyword.
If you just need an object that doesn’t have any nontrivial supertypes, write its members in curly braces after object :
Inheriting anonymous objects from supertypes
To create an object of an anonymous class that inherits from some type (or types), specify this type after object and a colon ( : ). Then implement or override the members of this class as if you were inheriting from it:
If a supertype has a constructor, pass appropriate constructor parameters to it. Multiple supertypes can be specified as a comma-delimited list after the colon:
Using anonymous objects as return and value types
When an anonymous object is used as a type of a local or private but not inline declaration (function or property), all its members are accessible via this function or property:
If this function or property is public or private inline, its actual type is:
Any if the anonymous object doesn’t have a declared supertype
The declared supertype of the anonymous object, if there is exactly one such type
The explicitly declared type if there is more than one declared supertype
In all these cases, members added in the anonymous object are not accessible. Overridden members are accessible if they are declared in the actual type of the function or property:
Accessing variables from anonymous objects
The code in object expressions can access variables from the enclosing scope:
Object declarations
The Singleton pattern can be useful in several cases, and Kotlin makes it easy to declare singletons:
This is called an object declaration, and it always has a name following the object keyword. Just like a variable declaration, an object declaration is not an expression, and it cannot be used on the right-hand side of an assignment statement.
The initialization of an object declaration is thread-safe and done on first access.
To refer to the object, use its name directly:
Such objects can have supertypes:
Object declarations can’t be local (that is, they can’t be nested directly inside a function), but they can be nested into other object declarations or non-inner classes.
Companion objects
An object declaration inside a class can be marked with the companion keyword:
Members of the companion object can be called simply by using the class name as the qualifier:
The name of the companion object can be omitted, in which case the name Companion will be used:
Class members can access the private members of the corresponding companion object.
The name of a class used by itself (not as a qualifier to another name) acts as a reference to the companion object of the class (whether named or not):
Note that even though the members of companion objects look like static members in other languages, at runtime those are still instance members of real objects, and can, for example, implement interfaces:
However, on the JVM you can have members of companion objects generated as real static methods and fields if you use the @JvmStatic annotation. See the Java interoperability section for more detail.
Semantic difference between object expressions and declarations
There is one important semantic difference between object expressions and object declarations:
Object expressions are executed (and initialized) immediately, where they are used.
Object declarations are initialized lazily, when accessed for the first time.
A companion object is initialized when the corresponding class is loaded (resolved) that matches the semantics of a Java static initializer.
Объект в Kotlin и паттерн «Одиночка»


Объект в Kotlin и паттерн «Одиночка»
Сегодня вы изучите, как использовать ключевое слово object в Kotlin для определения одиночных, сопутствующих и анонимных объектов и обеспечения взаимодействия с Java.
Итак, начнем
Откройте свой проект в Android Studio 4.1 или выше. Выберите «Open an Existing Project», а затем выберите имя начального проекта.
Открыв стартовый проект, вы обнаружите там уже некую структуру:
Далее соберите и запустите проект. Вы увидите экран с разнообразными Android- тематическими продуктами для продажи:
Тем не менее вы пока ничего не можете купить. Продолжайте чтение, чтобы добавить возможность покупок, параллельно изучая способы использования object в Kotlin.
Использование синглотонов (одиночек) в Kotlin
Используя слово object в приложении, вы определяете синглтон (одиночку). Одиночка – это шаблон проектирования, в котором данный класс имеет только один экземпляр внутри всего приложения.
Одиночка чаще всего применяется:
Держите в уме, что одиночки не решение по хранению данных. Данные в них живут только пока живо ваше приложение в памяти.
Читайте далее, как определить синглтон!
Использование Object для определения синглтона корзины покупок
На данный момент нет способа добавить предметы в вашу тележку и отобразить в приложении. Для осуществления затеи необходимо место для помещения информации и способ поделиться ей на других экранах приложения.
Android Studio создаст файл с пустым object для вас. Это будет выглядеть так:
Вы можете многое узнать об этой разнице, посмотрев на эквивалентный код Java. К счастью для вас, в Android Studio есть способ сделать это.
Перейдите в Tools ▸ Kotlin ▸ Show Kotlin Bytecode и нажмите кнопку Decompile в верхней части окна Kotlin Bytecode. Вы увидите что-то вроде следующего:
На что обратить внимание:
Помните об этом, когда пытаетесь использовать Kotlin вместе с Java!
Создание публичного интерфейса синглтона
Теперь, создав синглтон, необходимо создать его публичный интерфейс, чтобы другие классы могли общаться с ним. Для этого добавьте следующий код внутрь ShoppingCart :
Добавив этот код, вы:
Далее вы узнаете, как получить доступ к публичному интерфейсу только что определенного синглтона.
Получение публичного интерфейса синглтона
Теперь добавьте некоторую логику для получения одиночки. Для этого:
Для визуализации обратной связи, позволяющей убедиться, что вы успешно добавили продукт, вы также показываете Toast.
Теперь соберите и запустите. Нажмите на продукт, и вы увидите Toast, подтверждающий, что вы добавили его в корзину:
Работа с сопутствующими объектами
Это будет важно, когда вы узнаете, как получить доступ к сопутствующим объектам в следующем разделе. А пока сосредоточитесь на определении сопутствующего объекта.
Определение сопутствующего объекта
Для определения сопутствующего объекта откройте ShoppingCartActivity и найдите // TOD O перед последней закрывающей скобкой. Замените на следующее:
Если Android Studio побуждает так сделать, импортируйте android.content.Context и android.content.Intent для Context и Intent соответственно.
Поздравляю, вы определили сопутствующий объект!
Заметка
Основное различие между объявлениями объектов и сопутствующими объектами заключается в том, как они инициализируются. Объявления объектов инициализируются при первом использовании. Сопутствующие объекты инициализируются при загрузке содержащего их класса, что соответствует поведению статических инициализаторов Java.
Получение сопутствующего объекта для старта задачи
Теперь соберите проект и запустите его. Нажмите на Go to Cart и вы увидите, что приложение теперь показывает новый пустой экран:
Но куда же подевались все товары из вашей корзины? Не волнуйтесь, вы займетесь этим в следующем разделе.
Отображение товаров из корзины
Теперь вы создадите экран корзины для покупок, используя:
Для начала вы примените Java класс для подсчета итоговой цены корзины. В процессе вы изучите, как совместимость между Kotlin и Java одиночками устроена.
Получение Kotlin одиночки из Java для подсчета цены
Начните с создания нового Java класса для выполнения этих вычислений, используя следующие шаги:
Пока этот метод отрабатывает, вы можете его немного подчистить, используя @JvmStatic :
Добавив аннотацию, объявление products должно выглядеть так:
Отображение товаров в корзине
Вам нужно будет подключить все, чтобы отображались товары и общая цена в корзине. Для этого:
Вот что делает приведенный выше код:
Теперь соберите и запустите проект. Убедитесь, что он работает, добавив несколько продуктов в корзину и нажав Go to Cart. Вы увидите экран, на котором отображается корзина с товарами плюс общая цена:
Вы только что узнали, как заставить Java получить доступ к данным из синглтона Kotlin. Далее вы узнаете, как удалить товары из корзины с помощью кнопки Clear Cart.
Определение слушателей объектов
Когда пользователь очищает корзину покупок, ShoppingCartActivity должен обновиться, чтобы отобразить пустую корзину вместо корзины с продуктами. Но синглтоны не уведомляют своих слушателей об изменениях автоматически, поэтому вам необходимо реализовать это поведение, чтобы пользователь мог очистить свою корзину.
Уведомление слушателей об изменениях корзины
Чтобы уведомить слушателей об изменениях корзины, откройте ShoppingCart и добавьте следующий код перед последней закрывающей скобкой:
Здесь вы определяете интерфейс, который ShoppingCart будет использовать для уведомления слушателей об изменении своих данных.
Затем добавьте следующий код между var products и addProduct() в ShoppingCart :
В приведенном коде вы определили слабую ссылку на слушателя. Это заставляет ShoppingCart уведомлять этого слушателя всякий раз, когда данные меняются.
Слабая ссылка не дает одиночке сильно удерживаться за действие, что может вызвать утечку памяти. Подробнее об этом позже!
Теперь, когда у вас есть слушатель, вы должны его уведомить! Еще в ShoppingCart добавьте следующую функцию:
Это добавляет частный метод к одиночке. Он уведомляет вашего слушателя об изменении списка продуктов. Поскольку только синглтон может обновлять список, именно он должен иметь возможность запускать это уведомление. Подобные частные методы не будут видны извне одиночки, поэтому они идеально подходят для внутренней логики.
Теперь, когда бы пользователь не добавил товар в корзину, ShoppingCart уведомит его слушателей.
Прослушивание изменений корзины: анонимные объекты
Для этого вернитесь в ShoppingCartActivity и добавьте следующее свойство между var products и onCreate() :
Вы определили этот анонимный объект как свойство своего ShoppingCartActivity. Это означает, что переопределенный onCartChanged() может получить доступ к любым функциям и свойствам в действии.
Теперь вам нужно указать одиночке использовать только что созданное свойство в качестве его слушателя. Найдите // Your code внутри onCreate() и замените его на:
Далее найдите setupClearCartButton() и замените // TODO на:
Этот код вызывает clear() в синглтон, когда пользователь нажимает кнопку Clear Cart.
Когда пользователь очищает корзину для покупок, одиночка уведомляет своего слушателя. Поскольку слушатель был настроен на анонимный объект в ShoppingCartActivity, именно об этом и отправляется уведомление.
Теперь соберите и запустите приложение. Добавьте товары в корзину, нажмите Go to Cart и Clear Cart. Это очистит корзину и обновит view:
И вот: финальный вид вашей корзины покупок!
Далее вы изучите некоторые лучшие приемы по работе с Kotlin объектами.
Лучшие приемы для синглтонов и сопутствующих объектов
Прежде чем закончить статью, уделите немного времени рассмотрению некоторых передовых моментов.
Kotlin, компиляция в байткод и производительность (часть 2)
Это продолжение публикации. Первую часть можно посмотреть тут
Содержание:
Циклы:
В языке Kotlin отсутствует классический for с тремя частями, как в Java. Кому-то это может показаться проблемой, но если подробнее посмотреть все случаи использования такого цикла, то можно увидеть, что по большей части он применяется как раз для перебора значений. На смену ему в Kotlin есть упрощенная конструкция.
1..10 тут это диапазон по которому происходит итерация. Компилятор Kotlin достаточно умный, он понимает что мы собираемся в данном случае делать и поэтому убирает весь лишний оверхед. Код компилируется в обычный цикл while с переменной счетчика цикла. Никаких итераторов, никакого оверхеда, все достаточно компактно.
Похожий цикл по массиву (который в Kotlin записывается в виде Array ), компилируется аналогичным образом в цикл for.
Немного другая ситуация возникает, когда происходит перебор элементов из списка:
В этом случае приходится использовать итератор:
Таким образом, в зависимости от того по каким элементам происходит перебор, компилятор Kotlin сам выбирает самый эффективный способ преобразовать цикл в байткод.
Ниже приведено сравнение производительности для циклов с аналогичными решениями в Java:
Циклы
Как видно разница между Kotlin и Java минимальна. Байткод получается очень близким к тому что генерирует javac. По словам разработчиков они еще планируют улучшить это в следующих версиях Kotlin, чтобы результирующий байткод был максимально близок к тем паттернам, которые генерирует javac.
When — это аналог switch из Java, только с большей функциональностью. Рассмотрим ниже несколько примеров и то, во что они компилируются:
Для такого простого случая результирующий код компилируется в обычный switch, тут никакой магии не происходит:
Если же немного изменить пример выше, и добавить константы:
То код в этом случае уже компилируется в следующий вид:
Это происходит потому, что на данный момент компилятор Kotlin не понимает, что значения являются константами, и вместо преобразования к switch, код преобразуется к набору сравнений. Поэтому вместо константного времени происходит переход к линейному (в зависимости от количества сравнений). По словам разработчиков языка, в будущем это может быть легко исправлено, но в текущей версии это пока так.
Существует также возможность использовать модификатор const для констант, известных на момент компиляции.
Тогда в этом случае компилятор уже правильно оптимизирует when:
Если же заменить константы на Enum:
То код, также как в первом случае, будет компилироваться в switch (практический такой же как в случае перебора enum в Java).
По ordinal номеру элемента определяется номер ветки в switch, по которому далее и происходит выбор нужной ветви.
Посмотрим на сравнение производительности решений на Kotlin и Java:
Как видно простой switch работает точно также. В случае, когда компилятор Kotlin не смог определить что переменные константы и перешел к сравнениям, Java работает чуть быстрее. И в ситуации, когда перебираем значения enum, также есть небольшая потеря на возню с определением ветви по значению ordinal. Но все эти недостатки будут исправлены в будущих версиях, и к тому же потеря в производительности не очень большая, а в критичных местах можно переписать код на другой вариант. Вполне разумная цена за удобство использования.
Делегаты
Делегирование — это хорошая альтернатива наследованию, и Kotlin поддерживает его прямо из коробки. Рассмотрим простой пример с делегированием класса:
Класс Derived в конструкторе получает экземпляр класса, реализующий интерфейс Base, и в свою очередь делегирует реализацию всех методов интерфейса Base к передаваемому экземпляру. Декомпилированный код класса Derived будет выглядеть следующим образом:
В конструктор класса передается экземпляр класса, который запоминается в неизменяемом внутреннем поле. Также переопределяется метод print интерфейса Base, в котором просто происходит вызов метода из делегата. Все достаточно просто.
Существует также возможность делегировать не только реализацию всего класса, но и отдельных его свойств (а с версии 1.1 еще возможно делегировать инициализацию в локальных переменных).
Компилируется в код:
При инициализации класса DeleteExample создается экземпляр класса Delegate, сохраняемый в поле name$delegate. И далее вызов функции getName переадресовывается к вызову функции getValue из name$delegate.
В Kotlin есть уже несколько стандартных делегатов:
— lazy, для ленивых вычислений значения поля.
— observable, который позволяет получать уведомления обо всех изменения значения поля
— map, используемый для инициализации значений поля из значений Map.
Object и companion object
В Kotlin нет модификатора static для методов и полей. Вместо них, по большей части, рекомендуется использовать функции на уровне файла. Если же нужно объявить функции, которые можно вызывать без экземпляра класса, то для этого есть object и companion object. Рассмотрим на примерах как они выглядят в байткоде:
Простое объявление object с одним методом выглядит следующим образом:
В коде дальше можно обращаться к методу objectFun без создания экземпляра ObjectExample. Код компилируется в практически каноничный синглтон:
Компилируется к вызову INSTANCE:
companion object используется для создания аналогичных методов только уже в классе, для которого предполагается создание экземпляров.
Обращение к методу companionFun также не требует создания экземпляра класса, и в Kotlin будет выглядеть как простое обращение к статическому методу. Но на самом деле происходит обращение к компаньону класса. Посмотрим декомпилированный код:
Компилятор Kotlin упрощает вызовы, но из Java, правда, выглядит уже не так красиво. К счастью, есть возможность объявить методы по настоящему статическими. Для этого существует аннотация @JvmStatic. Ее можно добавить как к методам object, так и к методам companion object. Рассмотрим на примере object:
В этом случае метод staticFun будет действительно объявлен статическим:
Для методов из companion object тоже можно добавить аннотацию @JvmStatic:
Для такого кода будет также создан статичный метод companionFun. Но сам метод все равно будет вызывать метод из компаньона:
Как показано выше, Kotlin предоставляет различные возможности для объявления как статических методов так и методов компаньонов. Вызов статических методов чуть быстрее, поэтому в местах, где важна производительность, все же лучше ставить аннотации @JvmStatic на методы (но все равно не стоит рассчитывать на большой выигрыш в быстродействии)
lateinit свойства
Иногда возникает ситуация, когда нужно объявить notnull свойство в классе, значение для которого мы не можем сразу указать. Но при инициализации notnull поля мы обязаны присвоить ему значение по умолчанию, либо сделать свойство Nullable и записать в него null. Чтобы не переходить к nullable, в Kotlin существует специальный модификатор lateinit, который говорит компилятору Kotlin о том, что мы обязуемся сами позднее инициализировать свойство.
Если же мы попробуем обратиться к свойству без инициализации, то будет брошено исключение UninitializedPropertyAccessException. Подобная функциональность работает достаточно просто:
В getter вставляется дополнительная проверка значения свойства, и если в нем хранится null, то кидается исключение. Кстати именно из-за этого в Kotlin нельзя сделать lateinit свойство с типом Int, Long и других типов, которые соответствуют примитивным типам Java.
coroutines
В версии Kotlin 1.1 появилась новая функциональность, называемая корутины (coroutines). С ее помощью можно легко писать асинхронный код в синхронном виде. Помимо основной библиотеки (kotlinx-coroutines-core) для поддержки прерываний, есть еще и большой набор библиотек с различными расширениями:
kotlinx-coroutines-jdk8 — дополнительная библиотека для JDK8
kotlinx-coroutines-nio — расширения для асинхронного IO из JDK7+.
kotlinx-coroutines-reactive — утилиты для реактивных стримов
kotlinx-coroutines-reactor — утилиты для Reactor
kotlinx-coroutines-rx1 — утилиты для RxJava 1.x
kotlinx-coroutines-rx2 — утилиты для RxJava 2.x
kotlinx-coroutines-android — UI контекст для Android.
kotlinx-coroutines-javafx — JavaFx контекст для JavaFX UI приложений.
kotlinx-coroutines-swing — Swing контекст для Swing UI приложений.
Примечание: Функциональность пока находится в экспериментальной стадии, поэтому все сказанное ниже еще может измениться.
Для того, чтобы обозначить, что функция может быть прервана и использована в контексте прерывания, используется модификатор suspend
Декомпилированный код выглядит следующим образом:
Получается практически исходная функция, за исключением того, что еще передается один дополнительный параметр, реализующий интерфейс Continuation.
В нем хранится контекст выполнения, определена функция возвращения результата и функция возвращения исключения, в случае ошибки.
Корутины компилируются в конечный автомат (state machine). Рассмотрим на примере:
Функции foo и bar возвращают CompletableFuture, на которых вызывается suspend функция await. Декомпилировать в Java такой код не получится (по большей части из-за goto), поэтому рассмотрим его в псевдокоде:
Сами корутины могут выполняться в различных потоках, есть удобный механизм для управления этим при помощи указания пула в контексте запуска корутины. Можно посмотреть подробный гайд с большим количеством примеров и описанием их использования.
Все исходные коды на Kotlin доступны в github. Можно открыть их у себя и поэкспериментировать с кодом, параллельно просматривая, в какой итоговый байткод компилируются исходники.
Выводы
Производительность приложений на Kotlin будет не сильно хуже, чем на Java, а с использованием модификатора inline может даже оказаться лучше. Компилятор во всех местах старается генерировать наиболее оптимизированный байткод. Поэтому не стоит бояться, что при переходе на Kotlin вы получите большое ухудшение производительности. А в особо критичных местах, зная во что компилируется Kotlin, всегда можно переписать код на более подходящий вариант. Небольшая плата за то, что язык позволяет реализовывать сложные конструкции в достаточно лаконичном и простом виде.
Спасибо за внимание! Надеюсь вам понравилась статья. Прошу всех тех, кто заметил какие-либо ошибки или неточности написать мне об этом в личном сообщении.












