Что такое sealed class
Запечатанные классы (sealed)
Добавление модификатора sealed к суперклассу ограничивает возможность создания подклассов. Все прямые подклассы должны быть вложены в суперкласс. Запечатанный класс не может иметь наследников, объявленных вне класса.
В методе eval() при использовании when не пришлось использовать ветку else, так как sealed позволяет указать все доступные варианты и значение по умолчанию не требуется.
Если позже вы добавите новый подкласс, то выражение when будет ругаться и вы быстро вспомните, что нужно добавить новый код в программу.
По умолчанию запечатанный класс открыт и модификатор open не требуется. Запечатанные классы немного напоминают enum.
Пример: Продам кота дёшево
Создадим запечатанный класс AcceptedCurrency и три подкласса на его основе. Обратите внимание, что сейчас Kotlin разрешает объявлять подклассы не внутри запечатанного класса, а на одном уровне (для сравнения смотри старые примеры выше).
В классе активности создадим список принимаемых валют для покупки котят и применим его к адаптеру выпадающего списка.
Если запустить пример прямо сейчас, то в выпадающем списке увидим служебную информацию о классах. Не совсем то, что мы хотели увидеть.
Внесём изменения в запечатанный класс, чтобы у него появилось новое свойство.
Если вы пропустите какой-то подкласс в выражении when, то компилятор будет ругаться. Это удобно, когда вы будете вносить изменения в код.
Поменяем код для адаптера.
Теперь названия выводятся нормально.
Установим зависимость валют от рубля. Создадим в запечатанном классе абстрактное свойство valueInRubels. После этого студия потребует дополнить код у всех подклассов.
Добавим в класс ещё одну переменную ammount и функцию для подсчёта общей суммы.
Напишем код для щелчка кнопки. Вам нужно ввести минимальную и максимальную цену в любой валюте для одного котёнка, а кнопка покажет цену в рублях. Если вы увидите, что покупатель из Америки, то выставляете ценник в долларах. Если покупатель из непонятной страны, то ставьте тугрики (какая вам разница?).
В примере мы выставили цену от 2 до 3 долларов за котёнка (что-то мы продешевили) и сразу видим, сколько заработаем в рублях.
Sealed classes. Semantics vs performance
Наверное, не я один после прочтения документации о sealed классах подумал: «Ладно. Может быть это когда-нибудь пригодится». Позже, когда в работе я встретил пару задач, где удалось успешно применить этот инструмент, я подумал: «Недурно. Стоит чаще задумываться о применении». И, наконец, я наткнулся на описание класса задач в книге Effective Java (Joshua Bloch, 3rd) (да-да, в книге о Java).
Давайте рассмотрим один из вариантов применения и оценим его с точки зрения семантики и производительности.
Думаю, все, кто работал с UI, когда-то встречали реализации взаимодействия UI с сервисом через некие состояния, где одним из атрибутов был какой-то маркер типа. Механика обработки очередного состояния в таких реализациях, обычно, напрямую зависит от указанного маркера. Например такая реализация класса State:
Замечания из главы 23 «Prefer class hierarchies to tagged classes» книги. Предлагаю ознакомиться и с ней.
Обработка нового состояния может выглядеть так:
Обратите внимание, для состояний типа ERROR и DATA компилятор не в состоянии определить безопасность использования атрибутов, поэтому пользователю приходятся писать избыточный код. Изменения в семантике можно будет выявить только во время исполнения.
Sealed class
Несложным рефакторингом, мы можем разбить наш State на группу классов:
На стороне пользователя, мы получим обработку состояний, где доступность атрибутов будет определяться на уровне языка, а неверное использование будет порождать ошибки на этапе компиляции:
Так как в экземплярах присутствуют только значимые атрибуты можно говорить об экономии памяти и, что немаловажно, об улучшении семантики. Пользователям sealed классов не нужно вручную реализовывать правила работы с атрибутами в зависимости от маркера типа, доступность атрибутов обеспечивается разделением на типы.
Бесплатно ли всё это?
Java разработчики, кто пробовал Kotlin, наверняка заглядывали в декомпилированный код, чтобы посмотреть, на что похожи Kotlin выражения в терминах Java. Выражение с when будет выглядеть примерно так:
Ветвления с изобилием instanceof могут насторожить из-за стереотипов о «признаке плохого кода» и «влиянии на производительность», но нам ни к чему догадки. Нужно каким-то образом сравнить скорость выполнения, например, с помощью jmh.
На основе статьи «Измеряем скорость кода Java правильно» был подготовлен тест обработки четырёх состояний (LOADING, ERROR, EMPTY, DATA), вот его результаты:
Видно, что sealed реализация работает ≈25% медленнее (было предположение, что отставание не превысит 10-15%).
Если на четырёх типах мы имеем отставание на четверть, с увеличением типов (количество проверок instanceof) отставание должно только расти. Для проверки увеличим количество типов до 16 (предположим, что нас угораздило обзавестись настолько широкой иерархией):
Вместе со снижением производительности возросло отставание sealed реализации до ≈35% — чуда не произошло.
Заключение
В этой статье мы не открыли Америку и sealed реализации в ветвлениях на instanceof действительно работают медленнее сравнения ссылок.
Тем не менее нужно озвучить пару мыслей:
Как использовать (в чем смысл) sealed?
Вот пример из документации
То есть от класса B наследование не возможно. Ясно.
И уже тут возникает вопрос, почему не написать так
То есть в этом примере я конкретно показал, что наследование не возможно.
В первом примере это показано не настолько явно. Там модификатор sealed был применен к наследуемому классу B и говорилось, что уже от класса B наследование не возможно. То есть какой то третий класс не сможет наследоваться.
И далее идет ещё один пример. (Вот он третий класс Z.)
Обратите внимание, тут sealed идет в связке с override.
Но вот этот код не верен
Свойство Field_1 не может быть запечатанным т.к. не содержит модификатора override.
Почему? Я не хочу чтобы моё свойство было замещено или переопределено.
Вот ещё мой пример
Почему для запечатывания требуется override? Почему запечатывание свойства я могу применить только ко второму классу?
Почему для запечатывания требуется override?
Sealed переводится как «запечатанный, герметизированный». Запечатывается что именно?
1) Применение к объявлению класса. Первый пример показывает контекст применения ключевого слова. Функционал наследования запечатывается на первом же уровне наследования. В вашем контрпример не предоставляется новой информации для компилятора, поскольку на моменте компиляции компилятор сам может прийти к выводу, нужно ли при хранении объектов класса использовать указатель на таблицу виртуальных функций. Поэтому там слово sealed синтаксически верно, но лексически бесполезно.
уровне базового класса, то это в терминологии C# это невыражаемая мысль. Вы не должны использовать в таком случае наследование вообще (не объявлять virtual). Такую мысль могло бы выразить ключевое слово final (С++), но в C# его нет, в нём ООП используется немного в другом в контексте (где, грубо говоря, типы могут быть manged и unmanaged, и у программиста нет полного контроля над sizeof объектов типа).
Именно поэтому поведение реализовано как реализовано и
When applied to a method or property, the sealed modifier must always be used with override.
Почему запечатывание свойства я могу применить только ко второму классу?
Android RecyclerView с использованием котлиновских sealed классов
RecyclerView — это один из самых лучших инструментов для отображения больших списков на Android. Как разработчики, вы, скорее всего понимаете о чем я говорю. У нас есть много дополнительных фич, таких как шаблоны вью холдеров, сложная анимация, Diff-Utils колбек для повышения производительности и т. д. Такие приложения, как WhatsApp и Gmail, используют RecyclerView для отображения бесконечного количества сообщений.
Почему sealed классы Kotlin?
После появления Kotlin для разработки приложений под Android наши подходы к реализации кода кардинально изменились. То есть такие фичи, как расширения, почти заменили потребность в поддержании базовых классов для компонентов Android. Делегаты Kotlin внесли изменения в нашу работу с сеттерами и геттерами.
Вдохновившись этой статьей, я хочу показать вам реализацию типов представлений в RecyclerView с использованием sealed классов. Мы постараемся развить сравнение случайных чисел или лейаутов до типов классов. Если вы фанат Kotlin, я уверен, что вам понравится эта реализация.
Создание sealed классов в Kotlin
Первое, что нам нужно сделать при этом подходе — это создать все классы данных, которые мы намерены использовать в адаптере, а затем необходимо связать их в sealed классе. Давайте создадим группу классов данных:
Это несколько классов данных, которые я хотел отобразить в списке, основываясь на данных с серверов. Вы можете создать столько классов данных, сколько захотите. Этот подход хорошо масштабируется.
То, что мы можем работать с состояниями загрузки, хедерами, футерами и многим другое без написания дополнительных классов — это одно из крутых преимуществ данного метода. Вы скоро узнаете, как это сделать. Следующим шагом является создание sealed классов, содержащих все необходимые классы данных:
Sealed класс с пользовательскими моделями
Sealed класс с хидером и футером
На этом этапе заканчивается реализация нашего sealed класса.
Создание адаптера RecyclerView
В приведенном выше коде показана базовая реализация адаптера RecyclerView без какой-либо логики sealed классов. Как можно заметить, мы объявили sealed классы arraylist (UIModel). Следующим шагом является возврат соответствующего типа представления на основе позиции:
Сравнение модели sealed класса для получения типа представления
Теперь, когда мы успешно вернули правильный лейаут на основе модели sealed класса, нам нужно создать соответствующий ViewHolder в функции onCreateViewHolder на основе viewtype :
Создание вью холдера с учетом типа представления из sealed классов
Последний шаг — обновить вью холдер на основе текущих данных элемента, чтобы адаптер мог отображать данные в пользовательском интерфейсе. Поскольку у адаптера есть несколько представлений, мы должны классифицировать тип, а затем вызвать соответствующий ViewHolder :
После объединения всех частей кода, он выглядит так:
Финальная версия адаптера
Публикация данных в адаптер RecyclerView
DiffCallback
«DiffUtil — это вспомогательный класс, который может вычислять разницу между двумя списками и выводить список операций обновления, который преобразует первый список во второй». — Android Developer
Реализация diffcallback не является обязательной, но она повысит производительность, если вы работаете с большими наборами данных. Итак, чтобы реализовать difCallback в нашем адаптере, нам нужно различать модели и сравнивать нужные переменные:
Это все. Надеюсь, эта статья была для вас полезной. Спасибо за внимание!
Всех желающих приглашаем на двухдневный онлайн-интенсив «Делаем мобильную мини-игру за 2 дня». За 2 дня вы сделаете мобильную версию PopIt на языке Kotlin. В приложении будет простая анимация, звук хлопка, вибрация, таймер как соревновательный элемент. Интенсив подойдет для тех, кто хочет попробовать себя в роли Android-разработчика.
>> РЕГИСТРАЦИЯ
Исследуем sealed классы в Java 15
Продолжаем исследовать новые возможности, которые появляются в Java. В прошлые разы мы подробно рассматривали улучшенный оператор instanceof и записи, а сегодня объектом исследования будут sealed классы, которые запланированы к выходу в пятнадцатой версии Java.
Давайте же наконец играться с sealed классами. Но для начала проверим нашу версию Java:
Как видите, я запустил раннюю сборку JDK 15. В ней sealed классы уже присутствуют.
Напишем и запустим какой-нибудь простой код:
Кстати, у нас же есть записи. Может, перепишем код, используя их, чтобы стало короче?
Так гораздо лучше. Но пришлось сделать Shape интерфейсом, потому что записи не могут наследоваться от классов.
Однако мы так и не проверили, что модификатор sealed действительно работает. Давайте перенесём один из классов в другой файл:
Давайте попробуем сделать что-нибудь нелегальное. Например, укажем в permits класс, но «забудем» его отнаследовать:
Это явно ошибочная ситуация, и компилятор надёжно её перехватывает, сообщая нам об ошибке.
А если попробовать вписать в permits класс, который не является прямым наследником?
Получили в общем-то ту же самую ошибку, что и выше: если указываешь в permits класс, то это класс должен быть отнаследован напрямую.
Интересно, что будет, если указать в permits какой-нибудь совершенно левый класс?
Что если сделать sealed без единого наследника?
Было бы ещё интересно проверить вот что. Java на этапе компиляции умеет делать проверки совместимости типов. Если она сможет доказать, что два типа не могут быть приведены друг к другу, то будет ошибка компиляции. Например:
А теперь вопрос: будет ли ошибка компиляции, если усложнить наш пример, введя промежуточный sealed класс? Давайте проверим:
К сожалению, не хватило. Вообще это довольно странно, потому что это было бы совершенно логичным поведением компилятора. Я решил задать про это вопрос в рассылке OpenJDK, и мне ответили, что решили эту возможность пока не реализовывать и отложить её до следующего релиза. Ну что ж, тогда будем ждать.
Что мы ещё не попробовали? Что насчёт анонимных классов? Могут ли они наследоваться от sealed классов?
Ага, значит наследоваться не могут не только анонимные, но и вообще любые локальные классы. Ну и, конечно же, логично было бы запретить лямбды:
Заключение
Далеко не всегда иерархии классов должны быть открыты для расширения неограниченным кругом лиц. Часто встречается необходимость смоделировать такую иерархию, которая будет открыта для использования, но закрыта для расширения. «Запечатанные» классы и интерфейсы в Java 15, наконец, сделают такое возможным. Особенно хорошо sealed классы будут взаимодействовать с записями, позволив легко моделировать алгебраические типы данных.
Сейчас sealed классы некоторое время будут находиться в режиме preview, но к следующему LTS релизу Java 17 они, скорее всего, станут стабильными. Это даст неплохую мотивацию перейти на последнюю версию Java.
Если вам интересно читать про новости Java, то рекомендую вам подписаться на мой канал в Telegram.