Kotlin. Отложенная и ленивая инициализация свойств
Разработчики Kotlin крайне серьёзно относятся к проверкам на null. Поэтому, как правило, свойства, которые по логике программы должны хранить ненулевые значения инициализируются в конструкторе.
Тем не менее бывают ситуации, когда такой подход не особо удобен. Например, если вы хотите инициализировать свойства через внедрение зависимостей. Kotlin предусматривает такую возможность и предлагает использовать отложенную (позднюю) инициализацию. Осуществляется это с помощью модификатора lateinit.
Модификатор lateinit говорит о том, что данная переменная будет инициализирована позже. При этом инициализировать свойство можно из любого места, откуда она видна.
Правила использования модификатора lateinit:
- используется только совместно с ключевым словом var;
- свойство может быть объявлено только внутри тела класса (не в основном конструкторе);
- тип свойства не может быть нулевым и примитивным;
- у свойства не должно быть пользовательских геттеров и сеттеров;
- с версии Kotlin 1.2 можно применять к свойствам верхнего уровня и локальным переменным.
Если обратиться к свойству с модификатором lateinit до того, как оно будет проинициализировано, то получите ошибку, которая явно указывает, что свойство не было определено:
В версии Kotlin 1.2 модификатор был улучшен. Теперь перед обращением к переменной можно проверить была ли она инициализирована. Осуществляется это с помощью метода .isInitialized . Данная функция вернет true, если переменная инициализирована и false, если нет.
Когда стоит использовать?
Для того чтобы ответить на этот вопрос, нужно сначала понять, почему и откуда взялось это lateinit .
По факту lateinit появился с целью облегчить инъекцию зависимостей через Dagger. До его появления приходилось свойства, которые будут инъектиться, объявлять как nullable — ведь мы не можем такому свойству задать какое-либо значение кроме null . Это приводило к тому, что все вызовы этого свойства должны были сопровождаться проверкой на null . Так и появился lateinit .
Соответственно из этого можно сделать вывод: по возможности избегайте использования lateinit . По факту только в одном случае никак не избежать его использования — при инъекции зависимостей. В остальных случаях постарайтесь найти другой выход, например, используйте “ленивую” инициализацию (о ней ниже) или инициализируйте поле с начальным значением null . В этом случае по крайней мере вам компилятор будет подсказывать о необходимости проверки на null .
Но почему стоит избегать? В основном из-за того, что lateinit часто используют неправильно.
Пример из андроида: у вас во фрагменте есть lateinit -переменная, которая инициализируется в onCreateView . А теперь по шагам:
- Фрагмент создался.
- Создалась view для фрагмента. В lateinit -переменную было сохранено значение из view .
- Фрагмент ушел в backstack (например, был заменён на другой фрагмент). Вызывается метод onDestroyView (но не onDestroy ), который уничтожит view , но не ссылку на него в lateinit -переменной.
- При возвращении к фрагменту в lateinit -переменную присвоится новая ссылка на view , тогда как старый объект будет ещё какое-то время висеть в памяти.
Если эти шаги повторить, скажем, 10 раз подряд, то у вас в памяти будет висеть уже 10 бесполезных объектов, которые уничтожатся только с уничтожением самого фрагмента.
Поэтому используйте lateinit с осторожностью, чтобы потом не удивляться от возникновения неожиданных последствий.
Для более подробной информации рекомендую ознакомиться с видео — lateinit — это зло и «костыль» Kotlin. Dagger 2 всему виной.
Помимо отложенной инициализации в Kotlin существует ленивая инициализация свойств. Такая инициализация осуществляется с помощью функции lazy() , которая принимает лямбду, а возвращает экземпляр класса Lazy<T> . Данный объект реализует ленивое вычисление значения свойства: при первом обращении к свойству метод get() запускает лямбда-выражение (переданное lazy() в качестве аргумента) и запоминает полученное значение, а последующие вызовы просто возвращают запомненное значение.
Ленивая инициализация может быть использована только совместно с ключевым словом val.
Свойство, инициализированное подобным образом, называется делегированным свойством. Потому что мы делегировали вычисление значения классу-делегату Lazy<T> . Данный класс является частью стандартной библиотеки Kotlin и именно в нем реализован get-метод вычисляющий и возвращающий значение.
По умолчанию вычисление ленивых свойств синхронизировано: значение вычисляется только в одном потоке, а все остальные потоки могут видеть одно и то же значение. Однако способом вычисления можно управлять. Для этого функции lazy() нужно передать один из параметров:
Вопросы и ответы для собеседования по Kotlin. Часть 2
Модификаторы доступа — private, protected, internal, public
Классы, объекты, интерфейсы, конструкторы, функции, свойства и их сеттеры могут иметь модификаторы доступа. Геттеры всегда имеют ту же видимость, что и свойства, к которым они относятся. Модификаторы доступа — это ключевые слова, с помощью которых можно задать область действия данных. Они позволяют регулировать уровень доступа к различным частям кода. Локальные переменные, функции и классы не могут иметь модификаторов доступа.
В Kotlin есть четыре модификатора доступа: private , protected , internal и public .
Если модификатор явно не указан, то присваивается значение по умолчанию — public .
Private — доступ к членам класса только в пределах самого класса. То есть, поля и методы с модификатором private недоступны из других классов и даже из наследников.
Protected — доступ к членам класса только в пределах класса и его наследников. То есть, поля и методы с модификатором protected доступны из класса и его наследников, но не из других классов.
Internal — доступ к членам модуля (module). Модуль — это набор файлов, компилирующихся вместе, поэтому все классы, объявленные внутри модуля, могут иметь доступ к членам с модификатором internal .
Public — не ограничивает доступ к членам класса. Поля и методы с модификатором public доступны из любого места программы, включая другие модули.
1. Модификатор private
Private — самый строгий модификатор доступа. При его использовании данные будут доступны только в пределах конкретного класса или файла.
По сути, главное предназначение данного модификатора — реализация инкапсуляции в программе.
2. Модификатор protected
Данные, отмеченные модификатором protected будут видны:
внутри класса, в котором они объявлены;
в дочерних классах.
При этом нельзя отметить модификатором protected данные высокого уровня. К таким данным относятся классы, а также переменные или функции, объявленные вне класса.
Если в дочернем классе мы переопределим метод с модификатором protected, то он унаследует модификатор доступа от родителя и будет виден только внутри дочернего класса. Несмотря на то, что модификатор не будет указан явно.
Помимо модификатора protected такому методу можно задать модификатор public. При использовании остальных модификаторов Kotlin ругается.
3. Модификатор internal
Как правило, при разработке проекта мы делим его на независимые модули. Каждый модуль состоит из файлов, компилируемых вместе. Так вот модификатор internal позволяет сделать данные видимыми для всего модуля.
Данный модификатор можно применять ко всем типам данных. Однако он полезен только в том случае, если в проекте есть более одного модуля. Иначе используется модификатор public.
Например, в проекте есть два модуля — Module1 и Module2. В первом модуле есть класс Person() .
И еще в первом модуле есть такой файл:
Так как этот файл тоже находится в Module1, то мы можем получить доступ к переменным a и b . Но если попытаться к ним обратиться из Module2 — получим ошибку.
4. Модификатор public
Если при объявлении каких-либо данных использовать модификатор public, то они будут видны всем (даже в космосе). Еще public является модификатором по умолчанию для тех данных, которым модификатор явно не был указан.
Разница между var, val, const val
var — это изменяемая переменная. После инициализации мы можем изменять данные, хранящиеся в переменной.
Переменные val и const val доступны только для чтения — это неизменяемые переменные.
val — константа времени выполнения, т.е. значение можно назначить во время выполнения программы.
const val — константа времени компиляции, т.к. значения константам присваивается при компиляции (в момент, когда программа компилируется).
В отличие от val , значение const val должно быть известно во время компиляции.
Особенности const val :
могут получать значение только базовых типов: Int, Double, Float, Long, Short, Byte, Char, String, Boolean .
объявляются в глобальной области видимости, то есть за пределами функции main() или любой другой функции.
нет пользовательского геттера.
Чтобы объявить константу, нужно использовать модификатор const совместно с ключевым словом val . Переменные, отмеченные модификатором const , также называют константами времени компиляции. Это означает, что значения таких переменных известны во время компиляции. Отсюда следует, что они должны соответствовать следующим требованиям:
находиться на самом верхнем уровне (вне класса) или быть членом объекта ( object или companion object )
тип данных должен соответствовать одному из примитивных (например, String )
не иметь геттера
Здесь мы в объекте-компаньоне объявили константу, значением которой является расширение фотографии. Помимо этого мы объявили неизменяемую переменную, которая будет хранить имя фотографии и инициализироваться с помощью get-метода.
Мы заранее (до компиляции) знаем, что расширение у всех фотографий будет одно и то же. Нам не нужно его вычислять. Поэтому логично будет его объявить как константу.
Имя же фотографии, несмотря на то что оно уникально для каждого отдельного файла, заранее неизвестно. Чтобы его задать, нам потребуется вычислить время, в которое был сделан снимок. То есть значение выбирается во время выполнения программы. Поэтому используется ключевое слово val .
После компиляции кода везде, где использовалась переменная-константа будет произведено замещение: вместо имени переменной будет подставлено значение этой переменной. Переменная, которая хранит имя файла останется как есть.
Как стоит объявлять свои константы в Kotlin — при помощи companion object или вне класса?
На самом деле оба эти подхода приемлемы. Однако, использование companion object может быть излишним: компилятор Kotlin преобразует companion object во вложенный класс. Слишком много кода для простой константы.
Если вам не требуется поведение, специфичное для companion object , объявляйте константы вне класса, так как это будет способствовать более эффективному байт-коду. Да и сам синтаксис объявления констант вне класса более чистый и читабельный.
Свойства, методы get и set
Свойства класса — это переменные, которые хранят состояние объекта класса. Как и любая переменная, свойство может иметь тип, имя и значение.
В классе можно объявить свойства с помощью ключевого слова var или val . Свойства, объявленные с var , могут быть изменены после их инициализации, а свойства, объявленные с val , только для чтения.
При создании своего класса мы хотим сами управлять его свойствами, контролируя то, какие данные могут быть предоставлены или перезаписаны. С этой целью создаются get и set методы (геттеры и сеттеры). Цель get-метода — вернуть значение, а set-метода — записать полученное значение в свойство класса.
В данном примере свойство name имеет тип String и начальное значение пустой строки. Геттер возвращает значение свойства, преобразованное к верхнему регистру. Сеттер устанавливает значение свойства с добавлением префикса «Name: » перед переданным значением. Слово field используется для обращения к текущему значению свойства.
Если get и set методы не были созданы вручную, то для таких свойств Kotlin незаметно сам их генерирует. При этом для свойства, объявленного с val , генерируется get-метод, а для свойства, объявленного с var — и get, и set методы.
В чем отличие field от property?
В Kotlin свойство (property) — это абстракция над полями (fields), которая позволяет обращаться к значению переменной через методы геттера и сеттера, вместо прямого доступа к полю.
Field — это переменная, которая содержит значение и может быть доступна напрямую или через геттер/сеттер.
Пример определения свойства с геттером и сеттером в классе:
В данном примере свойство name содержит поле, которое может быть доступно напрямую только внутри класса, и методы геттера и сеттера, которые позволяют получать и изменять значение свойства через специальные методы.
Отложенная и ленивая инициализация свойств (lateinit и by lazy)
Отложенная и ленивая инициализация свойств — это механизмы, которые позволяют отложить инициализацию переменных до момента их первого использования. Оба варианта позволяют экономить ресурсы, т.к. избегают необходимости создания объектов при инициализации класса.
1. lateinit
Модификатор lateinit говорит о том, что данная переменная будет инициализирована позже. При этом инициализировать свойство можно из любого места, откуда она видна.
Правила использования модификатора lateinit :
lateinit может использоваться только с var свойствами класса;
lateinit может быть применен только к свойствам, объявленным внутри тела класса (но не в основном конструкторе), а также к переменным на верхнем уровне и локальным переменным;
lateinit свойства могут иметь любой тип, кроме примитивных типов (таких как Int , Long , Double и т.д.);
lateinit свойства не могут быть nullable (т.е. обязательно должно быть объявлены без знака вопроса);
lateinit свойства не могут быть проинициализированы сразу при их объявлении;
lateinit свойства должны быть инициализированы до первого обращения к ним, иначе будет выброшено исключение UninitializedPropertyAccessException ;
Нельзя использовать lateinit для переменных, определенных внутри локальных областей видимости (например, внутри функций);
При использовании модификатора lateinit у свойства не должно быть пользовательских геттеров и сеттеров.
Для проверки факта инициализации переменной вызывайте метод isInitialized() . Функцию следует использовать экономно — не следует добавлять эту проверку к каждой переменной с отложенной инициализацией. Если вы используете isInitialized() слишком часто, то скорее всего вам лучше использовать тип с поддержкой null.
2. by lazy
Ленивая инициализация (lazy initialization) — это подход, при котором объект инициализируется только при необходимости, а не сразу после создания. В Kotlin для ленивой инициализации свойств используется делегат lazy .
Когда свойство объявляется с использованием делегата lazy , оно не инициализируется сразу, а только тогда, когда к нему происходит первое обращение. При этом инициализация выполняется единожды, и в дальнейшем значение свойства сохраняется для всех последующих обращений к нему. Таким образом, ленивая инициализация позволяет оптимизировать использование ресурсов приложения, не инициализируя объекты, которые не понадобятся в ходе выполнения программы.
При использовании ленивой инициализации свойств с помощью by lazy в Kotlin, создается объект типа Lazy<T> , где T — это тип свойства, и этот объект используется для хранения значения свойства.
Когда код доходит до места, где используется свойство, вызывается метод getValue() этого объекта Lazy<T> . Если значение свойства еще не было проинициализировано, то вызывается лямбда-выражение, переданное в lazy < >, и ее результат используется для инициализации свойства. Значение сохраняется в объекте Lazy<T> и возвращается как результат метода getValue() . Если значение уже было проинициализировано, то просто возвращается сохраненное значение. Например, если у нас есть свойство:
то при первом обращении к свойству myProperty будет выполнена функция computeValue() , а результат будет сохранен. При последующих обращениях к свойству будет возвращено сохраненное значение.
3. Сравнение ленивой и отложенной инициализации
ленивая инициализация является одним из Delegate
отложенная инициализация требует использования модификатора свойства
ленивая инициализация применяется только к val
отложенная инициализация применяется только к var
у нас может быть ленивое свойство примитивного типа
lateinit применяется только к ссылочным типам
Самое главное, когда мы реализуем свойство как ленивый делегат, мы фактически присваиваем ему своего рода значение. Вместо фактического значения мы помещаем туда функцию для его вычисления, когда оно нам понадобится.
С другой стороны, когда мы объявляем свойство как lateinit , мы просто отключаем одну из проверок компилятора, которая гарантирует, что программа не обращается ни к одной переменной до того, как она получит значение. Вместо этого мы обещаем сделать эту проверку сами.
Что такое делегированные свойства (Delegated properties)?
Делегированные свойства (Delegated properties) — это свойства, которые не хранят своё значение напрямую, а делегируют это значение другому объекту, который реализует интерфейс Delegate . При доступе к свойству, его значение запрашивается у делегата, который может выполнить какую-то дополнительную логику, а затем вернуть требуемое значение. Пример:
Ключевое слово by используется для обозначения свойств, методы чтения и записи которых реализованы другим объектом, который называют делегатом.
Синтаксис выглядит так: val/var <имя свойства>: <Тип> by <выражение> . Выражение после by — делегат, потому что обращения get() , set() к свойству будут делегированы его методам getValue() и setValue() . Делегат не обязан реализовывать какой-то интерфейс, достаточно, чтобы у него были метод getValue() (и setValue() для var ‘ов) с определённой сигнатурой.
В Kotlin существуют несколько встроенных делегатов для работы с делегированными свойствами:
lazy() — позволяет создавать лениво инициализированные свойства
observable() — позволяет реагировать на изменения свойства
vetoable() — позволяет отклонять изменения значения свойства на основе заданного условия
notNull() — гарантирует, что свойство не будет иметь значение null
map() — позволяет хранить значения свойств в словаре ( Map )
Кроме того, в Kotlin можно создавать свои собственные делегаты, реализуя интерфейс ReadOnlyProperty или ReadWriteProperty . Это дает возможность создавать кастомные поведения для свойств, например, кеширование значений или логирование операций чтения/записи.
Как реализовать кастомный делегат?
Чтобы написать кастомный делегат, нужно определить класс, который реализует интерфейс ReadOnlyProperty для делегата val или ReadWriteProperty для делегата var .
Классы, реализующие ReadOnlyProperty и ReadWriteProperty , содержат два метода:
getValue(thisRef: T, property: KProperty<*>): R , который должен возвращать значение свойства.
setValue(thisRef: T, property: KProperty<*>, value: R) , который должен устанавливать значение свойства.
Например, рассмотрим создание кастомного делегата для логирования изменения значения свойства:
Здесь мы определяем класс LoggingDelegate , который реализует интерфейс ReadWriteProperty . Метод getValue выводит в консоль текущее значение свойства и возвращает его, а метод setValue выводит новое значение свойства в консоль и сохраняет его в переменной value .
Затем мы можем использовать наш кастомный делегат следующим образом:
Здесь мы создаем экземпляр класса MyClass , который содержит свойство myProperty , использующее наш кастомный делегат LoggingDelegate . При установке значения свойства или получении его значения будут вызываться соответствующие методы нашего делегата, и мы увидим соответствующие сообщения в консоли.
Конструкторы. Какие типы конструкторов вы знаете?
Свойств у класса может быть столько, сколько ему нужно. Но все они должны быть инициализированы при создании экземпляра этого класса. Поэтому для удобства был придуман конструктор — специальный блок кода, который вызывается при создании экземпляра класса. Ему передаются необходимые значения, которые потом используются для инициализации свойств.
Класс в Kotlin может иметь основной конструктор (primary) и один или более вторичных конструкторов (secondary). У класса может и не быть конструктора, но Kotlin всё равно автоматически сгенерирует основной конструктор по умолчанию (без параметров).
1. Основной конструктор
Объявляется он сразу после имени класса и состоит из ключевого слова constructor и круглых скобок:
Можно обойтись и без ключевого слова constructor при условии, что нет аннотаций или модификаторов доступа.
Параметры, переданные в конструктор, можно использовать для инициализации свойств, объявленных в теле класса.
А можно упростить еще больше и из параметров конструктора сделать свойства класса. Для этого перед именем параметра нужно указать ключевое слово val (только для чтения) или var (для чтения и редактирования).
При этом любому из свойств можно присвоить значение по умолчанию. Тогда при создании экземпляра класса для этого свойства значение можно либо не указывать, либо указать, если оно отличается от стандартного.
У класса может быть суперкласс. Тогда его основной конструктор должен инициализировать свойства, унаследованные от суперкласса.
Конструктор можно сделать приватным. Тогда никто и ничто не сможет создать экземпляр этого класса.
2. Вторичный конструктор
Также известен как вспомогательный, дополнительный, secondary конструктор. Вторичный конструктор используется в том случае, когда необходимо определить альтернативный способ создания класса. В Kotlin это применяется редко, так как обычно основного конструктора бывает достаточно благодаря возможности добавлять значения по умолчанию и использовать именованные аргументы.
Объявляется вторичный конструктор внутри тела класса при помощи ключевого слова constructor .
При этом если у класса есть основной конструктор, то все вторичные конструкторы обязательно должны явно или косвенно его вызывать. Подразумевается, что либо вторичный конструктор сам вызывает основной конструктор, либо сначала вызывает другой вторичный конструктор, который в свою очередь обращается к основному конструктору. Обращение к основному конструктору осуществляется при помощи ключевого слова this .
Если основного конструктора нет, то и обращаться к нему не надо.
Во вторичном конструкторе нельзя объявлять свойства класса. Все передаваемые ему параметры можно использовать либо для передачи основному конструктору, либо для инициализации свойств, объявленных в теле класса.
Также во вторичный конструктор можно добавить какую-либо логику.
Если у класса есть суперкласс, но нет основного конструктора, то каждый вторичный конструктор должен обращаться к конструктору суперкласса при помощи ключевого слова super .
Блок инициализации (init блок)
Основной конструктор не может в себе содержать какую-либо логику по инициализации свойств (исполняемый код). Он предназначен исключительно для объявления свойств и присвоения им полученных значений. Поэтому вся логика может быть помещена в блок инициализации — блок кода, обязательно выполняемый при создании объекта независимо от того, с помощью какого конструктора этот объект создаётся. Помечается он словом init .
По сути блок инициализации — это способ настроить переменные или значения, а также проверить, что были переданы допустимые параметры. Код в блоке инициализации выполняется сразу после создания экземпляра класса, т.е. сразу после вызова основного конструктора. В классе может быть один или несколько блоков инициализации и выполняться они будут последовательно.
Блок инициализации может быть добавлен, даже если у класса нет основного конструктора. В этом случае его код будет выполнен раньше кода вторичных конструкторов.
Расскажите о Data классах. Какие преимущества они имеют?
Data класс предназначен исключительно для хранения каких-либо данных.
Основное преимущество: для параметров, переданных в основном конструкторе автоматически будут переопределены методы toString() , equals() , hashCode() , copy() .
Также для каждой переменной, объявленной в основном конструкторе, автоматически генерируются функции componentN() , где N — номер позиции переменной в конструкторе.
Благодаря наличию вышеперечисленных функций внутри data класса мы исключаем написание шаблонного кода.
Что такое мульти-декларации (destructuring declarations)?
Мульти-декларации (destructuring declarations или деструктуризирующее присваивание) — это способ извлечения значений из объекта и присвоения их сразу нескольким переменным. В Kotlin этот механизм поддерживается с помощью оператора распаковки (destructuring operator) — componentN() , где N — номер компонента.
При создании data класса Kotlin автоматически создает функции componentN() для каждого свойства класса, где N — номер позиции переменной в конструкторе. Функции componentN() возвращают значения свойств в порядке их объявления в конструкторе. Это позволяет использовать мульти-декларации для распаковки значений свойств и присваивания их отдельным переменным.
Например, если у нас есть data класс Person с двумя свойствами name и age , мы можем использовать мульти-декларации, чтобы извлечь эти свойства и присвоить их двум переменным:
Также можно использовать мульти-декларации в циклах, чтобы итерироваться по спискам объектов и распаковывать значения свойств:
Мульти-декларации также могут быть использованы с массивами и другими коллекциями:
Что делает функция componentN()?
Функция componentN() возвращает значение переменной и позволяет обращаться к свойствам объекта класса по их порядковому номеру. Генерируется автоматически только для data классов.
Также функцию componentN() можно создать самому для класса, который не является data классом.
Теперь можно использовать мульти-декларации для класса Person :
В данном примере мы определили функции component1() , component2() и component3() как операторы с ключевым словом operator . Они возвращают значения свойств firstName , lastName и age соответственно. После этого мы можем использовать мульти-декларации для разбивки объекта Person на отдельные переменные.
Какие требования должны быть соблюдены для создания data класса?
Класс должен иметь хотя бы одно свойство, объявленное в основном конструкторе.
Все параметры основного конструктора должны быть отмечены val или var (рекомендуется val ).
Классы данных не могут быть abstract , open , sealed или inner .
Можно ли наследоваться от data класса?
От data класса нельзя наследоваться т.к. он является final классом, но он может наследоваться от других классов.
Свойства
Свойства в классах Kotlin могут быть объявлены либо как изменяемые (mutable) и неизменяемые (read-only) — var и val соответственно.
Для того чтобы воспользоваться свойством, просто обратитесь к нему по имени.
Геттеры и сеттеры
Полный синтаксис объявления свойства выглядит так:
Инициализатор property_initializer , геттер и сеттер можно не указывать. Также необязательно указывать тип свойства, если он может быть выведен из инициализатора или из возвращаемого типа геттера.
Синтаксис объявления констант имеет два отличия от синтаксиса объявления изменяемых переменных: во-первых, объявление константы начинается с ключевого слова val вместо var , а во-вторых, объявление сеттера запрещено.
Вы можете самостоятельно определить методы доступа для свойства. Если вы определяете пользовательский геттер, он будет вызываться каждый раз, когда вы обращаетесь к свойству (таким образом, вы можете реализовать вычисляемое свойство). Вот пример пользовательского геттера:
Вы можете опустить тип свойства, если его можно определить с помощью геттера.
Если вы определяете пользовательский сеттер, он будет вызываться каждый раз, когда вы присваиваете значение свойству, за исключением его инициализации. Пользовательский сеттер выглядит так:
По договорённости, имя параметра сеттера — value , но вы можете использовать любое другое.
Если вам нужно изменить область видимости метода доступа или пометить его аннотацией, при этом не внося изменения в реализацию по умолчанию, вы можете объявить метод доступа без объявления его тела.
Теневые поля
В Kotlin поле используется только как часть свойства для хранения его значения в памяти. Поля не могут быть объявлены напрямую. Однако, когда свойству требуется теневое поле (backing field), Kotlin предоставляет его автоматически. На это теневое поле можно обратиться в методах доступа, используя идентификатор field :
Идентификатор field может быть использован только в методах доступа к свойству.
Теневое поле будет сгенерировано для свойства, если оно использует стандартную реализацию как минимум одного из методов доступа, либо если пользовательский метод доступа ссылается на него через идентификатор field .
Например, в примере ниже не будет никакого теневого поля:
Теневые свойства
Если вы хотите предпринять что-то такое, что выходит за рамки вышеуказанной схемы неявного теневого поля, вы всегда можете использовать теневое свойство (backing property).
On the JVM: Access to private properties with default getters and setters is optimized to avoid function call overhead. —>
В JVM: доступ к приватным свойствам со стандартными геттерами и сеттерами оптимизируется таким образом, что вызов функции не происходит.
Константы времени компиляции
Если значение константного (read-only) свойства известно во время компиляции, пометьте его как константы времени компиляции, используя модификатор const . Такие свойства должны соответствовать следующим требованиям:
- Находиться на самом высоком уровне или быть членами объявления object или вспомогательного объекта;
- Быть проинициализированными значением типа String или значением примитивного типа;
- Не иметь переопределённого геттера.
Такие свойства могут быть использованы в аннотациях.
Свойства и переменные с поздней инициализацией
Обычно, свойства, объявленные non-null типом, должны быть проинициализированы в конструкторе. Однако часто бывает так, что делать это неудобно. К примеру, свойства могут быть инициализированы через внедрение зависимостей или в установочном методе (ориг.: setup method) юнит-теста. В таком случае вы не можете обеспечить non-null инициализацию в конструкторе, но всё равно хотите избежать проверок на null при обращении внутри тела класса к такому свойству.
Для того чтобы справиться с такой задачей, вы можете пометить свойство модификатором lateinit .
Такой модификатор может быть использован только с var свойствами, объявленными внутри тела класса (не в основном конструкторе, и только тогда, когда свойство не имеет пользовательских геттеров и сеттеров), со свойствами верхнего уровня и локальными переменными. Тип такого свойства должен быть non-null и не должен быть примитивным.
Доступ к lateinit свойству до того, как оно проинициализировано, выбрасывает специальное исключение, которое чётко обозначает свойство, к которому осуществляется доступ, и тот факт, что оно не было инициализировано.
Проверка инициализации lateinit var
Чтобы проверить, было ли проинициализировано lateinit var свойство, используйте .isInitialized метод ссылки на это свойство.
Эта проверка возможна только для лексически доступных свойств, то есть объявленных в том же типе, или в одном из внешних типов, или глобальных свойств, объявленных в том же файле.
Переопределение свойств
Делегированные свойства
Самый простой тип свойств просто считывает (или записывает) данные из теневого поля. Тем не менее с пользовательскими геттерами и сеттерами мы можем реализовать совершенно любое поведение свойства. Где-то между простотой первого вида и разнообразием второго существуют общепринятые шаблоны того, что могут делать свойства. Несколько примеров: вычисление значения свойства при первом доступе к нему (ленивые значения), чтение из ассоциативного списка с помощью заданного ключа, доступ к базе данных, оповещение listener’а в момент доступа.
Такие распространённые поведения свойств могут быть реализованы в виде библиотек с помощью делегированных свойств.
Initializing lazy and lateinit variables in Kotlin
Kotlin usually requires us to initialize properties as soon as we define them. Doing this seems odd when we don’t know the ideal initial value, especially in the case of lifecycle-driven Android properties.
Luckily, there is a way to get through this problem. The IntelliJ IDEA editor will warn you if you declare a class property without initializing it and recommend adding a lateinit keyword.
What if an initialized property or object doesn’t actually get used in the program? Well, these unused initializations will be liabilities to the program since object creation is a heavy process. This is another example of where lateinit can come to our rescue.
This article will explain how the lateinit modifier and lazy delegation can take care of unused or unnecessary early initializations. This will make your Kotlin development workflow more efficient altogether.
lateinit in Kotlin
The lateinit keyword stands for “late initialization.” When used with a class property, the lateinit modifier keeps the property from being initialized at the time of its class’ object construction.
Memory is allocated to lateinit variables only when they are initialized later in the program, rather than when they are declared. This is very convenient in terms of flexibility in initialization.
Let’s look at some important features that lateinit has to offer!
Key features
Firstly, memory is not allocated to a lateinit property at the time of declaration. The initialization takes place later when you see fit.
A lateinit property may change more than once throughout the program and is supposed to be mutable. That’s why you should always declare it as a var and not as a val or const .
The lateinit initialization can save you from repetitive null checks that you might need when initializing properties as nullable types. This feature of lateinit properties doesn’t support the nullable type.
Over 200k developers use LogRocket to create better digital experiences
Learn more →
Expanding on my last point, lateinit can be used well with non-primitive data types. It doesn’t work with primitive types like long or int . This is because whenever a lateinit property is accessed, Kotlin provides it a null value under the hood to indicate that the property has not been initialized yet.
Primitive types can’t be null , so there is no way to indicate an uninitialized property. In consequence, primitive types throw an exception when used with the lateinit keyword.
Lastly, a lateinit property must be initialized at some point before it is accessed or it will throw an UninitializedPropertyAccessException error, as seen below:
A lateinit property accessed before initialization leads to this exception.
Kotlin allows you to check if a lateinit property is initialized. This can be handy to deal with the uninitialization exception we just discussed.
Examples of the lateinit modifier in use
Let’s see the lateinit modifier in action with a simple example. The code below defines a class and initializes some of its properties with dummy and null values.
This isn’t the best way to initialize a variable, but in this case, it still does the job.
As you can see above, if you choose to make the property nullable, you’ll have to null check it whenever you modify or use it. This can be rather tedious and annoying.
Let’s tackle this issue with the lateinit modifier:
The lateinit implementation speaks for itself and demonstrates a neat way to deal with variables! Apart from the default behavior of lateinit , the main takeaway here is how easily we can avoid using the nullable type.
Lifecycle-driven properties and lateinit
Data binding is another example of using lateinit to initialize an activity later on. Developers often want to initialize the binding variable earlier to use it as a reference in other methods for accessing different views.
In the MainActivity class below, we declared the binding with the lateinit modifier to achieve the same thing.
The binding for MainActivity can only get initialized once the activity lifecycle function, onCreate() , gets fired. Therefore, declaring the binding with the lateinit modifier makes complete sense here.
When to use lateinit
With regular variable initialization, you have to add a dummy and, most likely, a null value. This will add a lot of null checks whenever they’re accessed.
We can use the lateinit modifier to avoid these repeated null checks, particularly when a property is likely to fluctuate frequently.
Things to remember when using lateinit
It’s good to remember to always initialize a lateinit property before accessing it, otherwise, you’ll see a big exception thrown at the time of compilation.
Make sure to also keep the property mutable by using a var declaration. Using val and const won’t make any sense, as they indicate immutable properties with which lateinit will not work.
Finally, avoid using lateinit when the given property’s data type is primitive or the chances of a null value are high. It’s not made for these cases and doesn’t support primitive or nullable types.
Lazy delegation in Kotlin
As the name suggests, lazy in Kotlin initializes a property in a lazy manner. Essentially, it creates a reference but only goes for the initialization when the property is used or called for the first time.
Now, you may be asking how this is different from regular initialization. Well, at the time of a class object construction, all of its public and private properties get initialized within its constructor. There’s some overhead associated with initializing variables in a class; the more variables, the greater the overhead will be.
Let’s understand it with an example:
Despite not using it, class Y in the above code still has an object created of class X . Class X will also slow down Y if it’s a heavily built class.
Unnecessary object creation is inefficient and may slow down the current class. It could be that some properties or objects are not required under certain conditions, depending on the program flow.
It could also be that properties or objects rely on other properties or objects for creation. Lazy delegation deals with these two possibilities efficiently.
Key features
A variable with lazy initialization will not be initialized until it’s called or used. This way, the variable is initialized only once and then its value is cached for further use in the program.
Since a property initialized with lazy delegation is supposed to use the same value throughout, it is immutable in nature and is generally used for read-only properties. You must mark it with a val declaration.
It is thread-safe, i.e. computed only once and shared by all threads by default. Once initialized, it remembers or caches the initialized value throughout the program.
In contrast to lateinit , lazy delegation supports a custom setter and getter that allows it to perform intermediate operations while reading and writing the value.
Example of lazy delegation in use
The code below implements simple math to calculate the areas of certain shapes. In the case of a circle, the calculation would require a constant value for pi .
As you can see above, no calculation of the area of any circle was completed, making our definition of pi useless. The property pi still gets initialized and allocated memory.
Let’s rectify this issue with the lazy delegation:
The above implementation of lazy delegation makes use of pi only when it is accessed. Once accessed, its value is cached and reserved to use throughout the program. We’ll see it in action with objects in the next examples.
Intermediate actions
Here’s how you can add some intermediate actions while writing values via lazy delegation. The below code lazy initializes a TextView in an Android activity.
Whenever this TextView gets called for the first time within the MainActivity , a debug message with a LazyInit tag will be logged, as shown below in the lambda function of the delegate:
Lazy delegation in Android apps
Now let’s move on to the application of lazy delegation in Android apps. The simplest use case can be our previous example of an Android activity that uses and manipulates a view conditionally.
Above, we initialized the SharedPreferences and a Button with lazy delegation. The logic entails the implementation of an onboarding screen based on a boolean value fetched from shared preferences.
The difference between by lazy and = lazy
The by lazy statement adds an enhancement by the lazy delegate directly to a given property. Its initialization will happen only once upon its first access.
On the other hand, the = lazy statement holds a reference to the delegate object instead, by which you may use the isInitialized() delegation method or access it with the value property.
When to use lazy
Consider using lazy delegates to lighten a class that involves multiple and/or conditional creations of other class objects. If the object creation depends on an internal property of the class, lazy delegation is the way to go.
Things to remember when using lazy
Lazy initialization is a delegation that initializes something only once and only when it’s called. It’s meant to avoid unnecessary object creation.
The delegate object caches the value returned on first access. This cached value is used further in the program when required.
You may take advantage of its custom getter and setter for intermediate actions when reading and writing values. I also prefer using it with immutable types, as I feel it works best with values that stay unchanged throughout the program.
Conclusion
In this article, we discussed Kotlin’s lateinit modifier and lazy delegation. We showed some basic examples demonstrating their uses and also talked about some practical use cases in Android development.
Thank you for taking the time to read this starter through the end! I hope you’ll be able to use this guide to implement these two features in your app development journey.
LogRocket: Instantly recreate issues in your Android apps.
LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket’s product analytics features surface the reasons why users don’t complete a particular flow or don’t adopt a new feature.
Kotlin. Отложенная и ленивая инициализация свойств
Разработчики Kotlin крайне серьёзно относятся к проверкам на null. Поэтому, как правило, свойства, которые по логике программы должны хранить ненулевые значения инициализируются в конструкторе.
Тем не менее бывают ситуации, когда такой подход не особо удобен. Например, если вы хотите инициализировать свойства через внедрение зависимостей. Kotlin предусматривает такую возможность и предлагает использовать отложенную (позднюю) инициализацию. Осуществляется это с помощью модификатора lateinit.
Модификатор lateinit говорит о том, что данная переменная будет инициализирована позже. При этом инициализировать свойство можно из любого места, откуда она видна.
Правила использования модификатора lateinit:
- используется только совместно с ключевым словом var;
- свойство может быть объявлено только внутри тела класса (не в основном конструкторе);
- тип свойства не может быть нулевым и примитивным;
- у свойства не должно быть пользовательских геттеров и сеттеров;
- с версии Kotlin 1.2 можно применять к свойствам верхнего уровня и локальным переменным.
Если обратиться к свойству с модификатором lateinit до того, как оно будет проинициализировано, то получите ошибку, которая явно указывает, что свойство не было определено:
В версии Kotlin 1.2 модификатор был улучшен. Теперь перед обращением к переменной можно проверить была ли она инициализирована. Осуществляется это с помощью метода .isInitialized . Данная функция вернет true, если переменная инициализирована и false, если нет.
Когда стоит использовать?
Для того чтобы ответить на этот вопрос, нужно сначала понять, почему и откуда взялось это lateinit .
По факту lateinit появился с целью облегчить инъекцию зависимостей через Dagger. До его появления приходилось свойства, которые будут инъектиться, объявлять как nullable — ведь мы не можем такому свойству задать какое-либо значение кроме null . Это приводило к тому, что все вызовы этого свойства должны были сопровождаться проверкой на null . Так и появился lateinit .
Соответственно из этого можно сделать вывод: по возможности избегайте использования lateinit . По факту только в одном случае никак не избежать его использования — при инъекции зависимостей. В остальных случаях постарайтесь найти другой выход, например, используйте “ленивую” инициализацию (о ней ниже) или инициализируйте поле с начальным значением null . В этом случае по крайней мере вам компилятор будет подсказывать о необходимости проверки на null .
Но почему стоит избегать? В основном из-за того, что lateinit часто используют неправильно.
Пример из андроида: у вас во фрагменте есть lateinit -переменная, которая инициализируется в onCreateView . А теперь по шагам:
- Фрагмент создался.
- Создалась view для фрагмента. В lateinit -переменную было сохранено значение из view .
- Фрагмент ушел в backstack (например, был заменён на другой фрагмент). Вызывается метод onDestroyView (но не onDestroy ), который уничтожит view , но не ссылку на него в lateinit -переменной.
- При возвращении к фрагменту в lateinit -переменную присвоится новая ссылка на view , тогда как старый объект будет ещё какое-то время висеть в памяти.
Если эти шаги повторить, скажем, 10 раз подряд, то у вас в памяти будет висеть уже 10 бесполезных объектов, которые уничтожатся только с уничтожением самого фрагмента.
Поэтому используйте lateinit с осторожностью, чтобы потом не удивляться от возникновения неожиданных последствий.
Для более подробной информации рекомендую ознакомиться с видео — lateinit — это зло и «костыль» Kotlin. Dagger 2 всему виной.
Помимо отложенной инициализации в Kotlin существует ленивая инициализация свойств. Такая инициализация осуществляется с помощью функции lazy() , которая принимает лямбду, а возвращает экземпляр класса Lazy<T> . Данный объект реализует ленивое вычисление значения свойства: при первом обращении к свойству метод get() запускает лямбда-выражение (переданное lazy() в качестве аргумента) и запоминает полученное значение, а последующие вызовы просто возвращают запомненное значение.
Ленивая инициализация может быть использована только совместно с ключевым словом val.
Свойство, инициализированное подобным образом, называется делегированным свойством. Потому что мы делегировали вычисление значения классу-делегату Lazy<T> . Данный класс является частью стандартной библиотеки Kotlin и именно в нем реализован get-метод вычисляющий и возвращающий значение.
По умолчанию вычисление ленивых свойств синхронизировано: значение вычисляется только в одном потоке, а все остальные потоки могут видеть одно и то же значение. Однако способом вычисления можно управлять. Для этого функции lazy() нужно передать один из параметров:
Свойства
Свойства в классах Kotlin могут быть объявлены либо как изменяемые (mutable) и неизменяемые (read-only) — var и val соответственно.
Для того чтобы воспользоваться свойством, просто обратитесь к нему по имени.
Геттеры и сеттеры
Полный синтаксис объявления свойства выглядит так:
Инициализатор property_initializer , геттер и сеттер можно не указывать. Также необязательно указывать тип свойства, если он может быть выведен из инициализатора или из возвращаемого типа геттера.
Синтаксис объявления констант имеет два отличия от синтаксиса объявления изменяемых переменных: во-первых, объявление константы начинается с ключевого слова val вместо var , а во-вторых, объявление сеттера запрещено.
Вы можете самостоятельно определить методы доступа для свойства. Если вы определяете пользовательский геттер, он будет вызываться каждый раз, когда вы обращаетесь к свойству (таким образом, вы можете реализовать вычисляемое свойство). Вот пример пользовательского геттера:
Вы можете опустить тип свойства, если его можно определить с помощью геттера.
Если вы определяете пользовательский сеттер, он будет вызываться каждый раз, когда вы присваиваете значение свойству, за исключением его инициализации. Пользовательский сеттер выглядит так:
По договорённости, имя параметра сеттера — value , но вы можете использовать любое другое.
Если вам нужно изменить область видимости метода доступа или пометить его аннотацией, при этом не внося изменения в реализацию по умолчанию, вы можете объявить метод доступа без объявления его тела.
Теневые поля
В Kotlin поле используется только как часть свойства для хранения его значения в памяти. Поля не могут быть объявлены напрямую. Однако, когда свойству требуется теневое поле (backing field), Kotlin предоставляет его автоматически. На это теневое поле можно обратиться в методах доступа, используя идентификатор field :
Идентификатор field может быть использован только в методах доступа к свойству.
Теневое поле будет сгенерировано для свойства, если оно использует стандартную реализацию как минимум одного из методов доступа, либо если пользовательский метод доступа ссылается на него через идентификатор field .
Например, в примере ниже не будет никакого теневого поля:
Теневые свойства
Если вы хотите предпринять что-то такое, что выходит за рамки вышеуказанной схемы неявного теневого поля, вы всегда можете использовать теневое свойство (backing property).
On the JVM: Access to private properties with default getters and setters is optimized to avoid function call overhead. —>
В JVM: доступ к приватным свойствам со стандартными геттерами и сеттерами оптимизируется таким образом, что вызов функции не происходит.
Константы времени компиляции
Если значение константного (read-only) свойства известно во время компиляции, пометьте его как константы времени компиляции, используя модификатор const . Такие свойства должны соответствовать следующим требованиям:
- Находиться на самом высоком уровне или быть членами объявления object или вспомогательного объекта;
- Быть проинициализированными значением типа String или значением примитивного типа;
- Не иметь переопределённого геттера.
Такие свойства могут быть использованы в аннотациях.
Свойства и переменные с поздней инициализацией
Обычно, свойства, объявленные non-null типом, должны быть проинициализированы в конструкторе. Однако часто бывает так, что делать это неудобно. К примеру, свойства могут быть инициализированы через внедрение зависимостей или в установочном методе (ориг.: setup method) юнит-теста. В таком случае вы не можете обеспечить non-null инициализацию в конструкторе, но всё равно хотите избежать проверок на null при обращении внутри тела класса к такому свойству.
Для того чтобы справиться с такой задачей, вы можете пометить свойство модификатором lateinit .
Такой модификатор может быть использован только с var свойствами, объявленными внутри тела класса (не в основном конструкторе, и только тогда, когда свойство не имеет пользовательских геттеров и сеттеров), со свойствами верхнего уровня и локальными переменными. Тип такого свойства должен быть non-null и не должен быть примитивным.
Доступ к lateinit свойству до того, как оно проинициализировано, выбрасывает специальное исключение, которое чётко обозначает свойство, к которому осуществляется доступ, и тот факт, что оно не было инициализировано.
Проверка инициализации lateinit var
Чтобы проверить, было ли проинициализировано lateinit var свойство, используйте .isInitialized метод ссылки на это свойство.
Эта проверка возможна только для лексически доступных свойств, то есть объявленных в том же типе, или в одном из внешних типов, или глобальных свойств, объявленных в том же файле.
Переопределение свойств
Делегированные свойства
Самый простой тип свойств просто считывает (или записывает) данные из теневого поля. Тем не менее с пользовательскими геттерами и сеттерами мы можем реализовать совершенно любое поведение свойства. Где-то между простотой первого вида и разнообразием второго существуют общепринятые шаблоны того, что могут делать свойства. Несколько примеров: вычисление значения свойства при первом доступе к нему (ленивые значения), чтение из ассоциативного списка с помощью заданного ключа, доступ к базе данных, оповещение listener’а в момент доступа.
Такие распространённые поведения свойств могут быть реализованы в виде библиотек с помощью делегированных свойств.
Отложенная инициализация(by lazy, lateinit)
Ключевое слово by lazy служит для отложенной инициализации через механизм делегатов. Делегат lazy принимает лямбда-выражение с кодом, который вы бы хотели выполнить для инициализации свойства.
Иногда требуется объявить переменную, но отложить инициализацию. Причины задержки инициализации могут быть самыми разными. Например, для инициализации требуются предварительные вычисления, которые желательно не выполнять, если их результат никогда не будет использован. Обычно в таких случаях переменную объявляют через var, которая получает значение null, пока не будет инициализирована нужным значением, которое потом никогда не меняется.
Это неудобно, если вы знаете, что у переменной будет конкретное значение и вы хотите избегать значения null. В нашем примере переменная имеет тип String?, который поддерживает значение null, хотя могла бы иметь тип String. Как вариант, можно было использовать заданное значение, например:
В любом случае приходится использовать var, даже зная, что после инициализации значение переменной никогда не изменится. Kotlin предлагает для подобных случаев использовать by lazy:
В этом случае функция getName() будет вызвана только один раз, при первом обращении к catName. Вместо лямбда-выражения можно также использовать ссылку на функцию:
Модификатор lateinit
Иногда переменную нельзя сразу инициализировать, сделать это можно чуть позже. Для таких случаев придумали новый модификатор lateinit (отложенная инициализация). Это относится только к изменяемым переменным.
Переменная обязательно должна быть изменяемой (var). Не должна относиться к примитивным типам (Int, Double, Float и т.д). Не должна иметь собственных геттеров/сеттеров.
Подобный подход удобен во многих случаях, избегая проверки на null. В противном случае пришлось бы постоянно использовать проверку или утверждение !!, что засоряет код.
Если вы обратитесь к переменной до её инициализации, то получите исключение «lateinit property . hos not been initialized» вместо NullPointerException.
В любое объявление var-свойства можно добавить ключевое слово lateinit. Тогда Kotlin позволит отложить инициализацию свойства до того момента, когда такая возможность появится. Это полезная возможность, но её следует применять осторожно. Если переменная с поздней инициализацией получит начальное значение до первого обращения к ней, проблем не будет. Но если сослаться на такое свойство до его инициализации, то получим исключение UninitializedPropertyAccessException. Как вариант, можно использовать тип с поддержкой null, но тогда придётся обрабатывать возможное значение null по всему коду. Получив начальное значение, переменные с поздней инициализацией будут работать так же, как другие переменные.
Для проверки факта инициализации переменной вызывайте метод isInitialized(). Функцию следует использовать экономно — не следует добавлять эту проверку к каждой переменной с поздней инициализацией. Если вы используете isInitialized() слишком часто, то скорее всего вам лучше использовать тип с поддержкой null.
“lateinit” Variable in Kotlin
In Kotlin, there are some tokens that cannot be used as identifiers for naming variables, etc. Such tokens are known as keywords. In simple words, keywords are reserved and have special meaning to the compiler, hence they can’t be used as identifiers. For example,
- as
- break
- class
- continue
- lateinit
“lateinit” keyword:
The “lateinit” keyword in Kotlin as the name suggests is used to declare those variables that are guaranteed to be initialized in the future.
Properties of primitive data types (e.g., Int, Double), as well as nullable properties, can’t be declared using “lateinit”.
“lateinit” variable:
A variable that is declared using “lateinit” keyword is known as “lateinit” variable.
Syntax:
This article focuses on how to check whether “lateinit” variable is initialized.
How to check if a “lateinit” variable has been initialized?
In Kotlin 1.2 version some changes were made using which we can check whether “lateinit” variable is initialized with the help of isInitialized method.
Syntax:
Return value:
false: If myVariable is not initialized yet
true: If myVariable is initialized
Example 1:
In the below program, we have declared “myVariable” using “lateinit” keyword. Before initialization, we checked whether this variable is initialized using isInitialized method. Later we have initialized it as “GFG”. Now we are checking again whether this variable is initialized.