Sealed-иерархии и объекты
Закрытые иерархии типов и семейство object — sealed-классы и интерфейсы, исчерпывающий when, object-объявления как синглтоны, companion object вместо статики и анонимные объекты.
9 вопросов
JuniorТеорияОчень частоЧто даёт sealed-класс такого, чего не даёт обычный абстрактный класс?
Что даёт sealed-класс такого, чего не даёт обычный абстрактный класс?
sealed-класс фиксирует набор своих прямых наследников на этапе компиляции: они обязаны лежать в том же пакете и модуле. Поэтому компилятор знает все варианты, и when по такому типу не требует else и проверяется на полноту.
Типичные ошибки
- ✗Думать, что
sealedозначает лишь запрет на инстанцирование - ✗Объявлять наследника в другом модуле и ждать, что это соберётся
- ✗Считать, что
whenпо sealed-типу всё равно требуетelse
Уточняющие вопросы
- →Что происходит с существующим
when, когда вы добавляете нового наследника? - →Где именно должны быть объявлены прямые наследники
sealed-класса?
JuniorТеорияОчень частоВо что компилируется object-объявление и почему оно потокобезопасно?
Во что компилируется object-объявление и почему оно потокобезопасно?
Оно компилируется в класс с приватным конструктором и статическим полем INSTANCE, которое присваивается в статическом инициализаторе. JVM выполняет его один раз, при первом обращении, по гарантиям загрузки классов, поэтому синглтон ленив и не требует блокировок.
Типичные ошибки
- ✗Считать
object-объявление просто пространством имён для функций - ✗Добавлять ручную блокировку, чтобы сделать
objectпотокобезопасным - ✗Ждать, что экземпляр создаётся при старте процесса, а не при первом обращении
Уточняющие вопросы
- →Когда
object-объявление плохо подходит для зависимости, которую хочется подменить в тестах? - →Чем ленивый делегат
by lazyотличается отobject-объявления?
JuniorТеорияЧастоЧем companion object отличается от статических членов в Java?
Чем companion object отличается от статических членов в Java?
companion object — это не статика: это настоящий экземпляр объекта в статическом поле Companion своего класса, и он может хранить состояние или реализовать интерфейс. Из Java пишут Foo.Companion.bar(), если @JvmStatic не создаст настоящий статический член.
Типичные ошибки
- ✗Называть член companion-объекта статическим членом класса
- ✗Ждать, что Java увидит
Foo.bar()без@JvmStatic - ✗Думать, что класс может объявить несколько companion-объектов
Уточняющие вопросы
- →Зачем companion-объекту реализовывать интерфейс?
- →Что именно
@JvmStaticдобавляет в сгенерированный байт-код?
MiddleДебаггингЧастоНовый sealed-подтип молча уходит не в ту ветку when — найдите и исправьте
Новый sealed-подтип молча уходит не в ту ветку when — найдите и исправьте
Ветка else уничтожает исчерпывающность: пока она есть, компилятор перестаёт проверять when, и Pending молча попадает в else уже во время выполнения. Уберите else и обработайте каждый подтип — тогда любой будущий вариант сломает сборку.
Типичные ошибки
- ✗Добавлять
elseвwhenпо sealed-типу, лишь бы замолчал компилятор - ✗Считать, что новый подтип не проскочит, раз код всё ещё компилируется
- ✗Думать, что
elseи полный набор веток взаимозаменяемы
Уточняющие вопросы
- →Где ещё в кодовой базе спрячется эта ошибка после добавления нового подтипа?
- →Когда ветка
elseпо sealed-типу всё-таки оправдана?
MiddleДизайнЧастоВы моделируете результат платёжного запроса в библиотеке, от которой зависят другие команды компании. Сегодня результат — один из трёх вариантов: одобрен, отклонён, в ожидании, и каждый вызывающий код разбирает их все, чтобы показать сообщение. На следующий квартал уже запланированы ещё два варианта: возвратный платёж и ручная проверка. Код вызывающих вам не подконтролен, и главное, чего нельзя допустить, — чтобы вызывающий молча неверно обработал вариант, добавленный уже после написания его кода. Один коллега предлагает обычный абстрактный класс PaymentOutcome с тремя наследниками, другой — sealed-класс. Что вы выберете, что этот выбор даст в день выхода четвёртого варианта и в какой ситуации абстрактный класс всё-таки был бы верным решением?
Вы моделируете результат платёжного запроса в библиотеке, от которой зависят другие команды компании. Сегодня результат — один из трёх вариантов: одобрен, отклонён, в ожидании, и каждый вызывающий код разбирает их все, чтобы показать сообщение. На следующий квартал уже запланированы ещё два варианта: возвратный платёж и ручная проверка. Код вызывающих вам не подконтролен, и главное, чего нельзя допустить, — чтобы вызывающий молча неверно обработал вариант, добавленный уже после написания его кода. Один коллега предлагает обычный абстрактный класс PaymentOutcome с тремя наследниками, другой — sealed-класс. Что вы выберете, что этот выбор даст в день выхода четвёртого варианта и в какой ситуации абстрактный класс всё-таки был бы верным решением?
Выбирайте sealed: набор вариантов закрыт, поэтому у вызывающих when исчерпывающий, и новый вариант ломает им сборку, а не отображается молча неверно. Абстрактный класс верен лишь тогда, когда чужой код обязан добавлять подтипы, которых вы не предвидите.
Типичные ошибки
- ✗Считать, что абстрактный класс тоже даёт вызывающим исчерпывающий
when - ✗Думать, что
sealedзапрещает автору библиотеки добавлять новые варианты - ✗Оставлять ветку
elseу вызывающих и называть такой дизайн безопасным
Уточняющие вопросы
- →Как вызывающий в другом модуле добавил бы свой вариант, выбери вы абстрактный класс?
- →Какая миграция ждёт вызывающих в день выхода четвёртого sealed-варианта?
MiddleТеорияИногдаЧем анонимный object : Listener { ... } отличается от object-объявления?
Чем анонимный object : Listener { ... } отличается от object-объявления?
Анонимный объект — это выражение: он даёт новый экземпляр при каждом вычислении, тогда как object-объявление — один именованный синглтон. Он может реализовать сразу несколько интерфейсов и способен захватывать и даже изменять локальные переменные.
Типичные ошибки
- ✗Считать, что выражение возвращает один и тот же экземпляр при каждом вычислении
- ✗Думать, что анонимный объект может реализовать только один интерфейс
- ✗Переносить на Kotlin правило Java о фактически финальных захваченных переменных
Уточняющие вопросы
- →Сколько объектов создастся, если такое выражение стоит внутри цикла?
- →Каков объявленный тип функции, которая возвращает анонимный объект?
MiddleТеорияИногдаКогда выбрать sealed interface вместо sealed-класса?
Когда выбрать sealed interface вместо sealed-класса?
Берите интерфейс, когда подтип должен ещё и наследовать другой класс или принадлежать сразу двум закрытым иерархиям: единственное наследование не даёт sealed-классу ни того, ни другого. sealed-класс выигрывает, когда варианты делят состояние или конструктор.
Типичные ошибки
- ✗Думать, что
sealed interfaceоставляет иерархию открытой для любых реализаций - ✗Считать, что подтип может наследовать
sealed-класс и ещё один класс сразу - ✗Ждать, что
sealed interfaceпонесёт состояние конструктора
Уточняющие вопросы
- →Может ли один класс принадлежать сразу двум разным
sealed-интерфейсам? - →Где должны быть объявлены реализации
sealed-интерфейса?
SeniorТеорияИногдаЧем модификатор sealed в Kotlin отличается от sealed с permits в Java 17?
Чем модификатор sealed в Kotlin отличается от sealed с permits в Java 17?
Kotlin выводит набор разрешённых подтипов из наследников в том же пакете и модуле, поэтому в нём нет ни permits, ни лазейки non-sealed. Java 17 требует явного permits, если подтипы не лежат в одном файле, и каждый подтип обязан быть final, sealed или non-sealed.
Типичные ошибки
- ✗Искать в Kotlin конструкцию
permits - ✗Ждать, что подтип
non-sealedснова откроет иерархию в Kotlin - ✗Считать, что подтип в Java может остаться непомеченным под sealed-родителем
Уточняющие вопросы
- →Почему Java 17 требует
permits, как только подтипы лежат в другом файле? - →Что позволяет
non-sealedи почему в Kotlin намеренно нет его аналога?
SeniorТеорияИногдаПочему рукописная идиома ленивой инициализации double-checked locking в Kotlin не нужна?
Почему рукописная идиома ленивой инициализации double-checked locking в Kotlin не нужна?
object-объявление и так ленивое и потокобезопасное: JVM присваивает INSTANCE в статическом инициализаторе, который выполняется ровно один раз по гарантиям загрузки классов. Double-checked locking воспроизводит ту же гарантию через @Volatile и блокировку.
Типичные ошибки
- ✗Думать, что
objectв Kotlin нуждается в@Volatile-поле ради безопасности - ✗Считать, что экземпляр создаётся жадно при старте процесса
- ✗Полагать, что каждое обращение к
objectберёт блокировку
Уточняющие вопросы
- →Что JVM гарантирует о выполнении статического инициализатора класса?
- →Когда ленивый делегат
by lazyлучше подходит, чемobject-объявление?