Spec-Zone.ru › Kotlin 2

Запечатанные классы и интерфейсы

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

Прямые подклассы — это классы, которые непосредственно наследуются от своего суперкласса.

Косвенные подклассы — это классы, которые наследуются от суперкласса через один или несколько уровней.

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

Запечатанные классы лучше всего подходят для следующих сценариев:

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

  • Требуется типобезопасный дизайн: безопасность и сопоставление с образцом имеют решающее значение для вашего проекта. Особенно это важно при управлении состоянием или обработке сложной условной логики. Пример см. в разделе Использование запечатанных классов с выражениями when.

  • Работа с закрытыми API: вы хотите создавать надёжные и удобные в сопровождении публичные API библиотек, которые гарантируют, что сторонние клиенты используют API предусмотренным образом.

Более подробные практические примеры приведены в разделе Сценарии использования.

В Java 15 была введена похожая концепция, в которой для определения ограниченных иерархий в запечатанных классах используется ключевое слово sealed вместе с предложением permits.

Объявление запечатанного класса или интерфейса

Чтобы объявить запечатанный класс или интерфейс, используйте модификатор sealed:

// Create a sealed interface
sealed interface Error

// Create a sealed class that implements sealed interface Error
sealed class IOError(): Error

// Define subclasses that extend sealed class 'IOError'
class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()

// Create a singleton object implementing the 'Error' sealed interface 
object RuntimeError : Error

Этот пример может представлять API библиотеки, содержащий классы ошибок, чтобы пользователи библиотеки могли обрабатывать ошибки, которые она может генерировать. Если иерархия таких классов ошибок включает интерфейсы или абстрактные классы, доступные в публичном API, ничто не мешает другим разработчикам реализовывать их или расширять в клиентском коде. Поскольку библиотека не знает об ошибках, объявленных за её пределами, она не может обрабатывать их так же, как собственные классы. Однако благодаря запечатанной иерархии классов ошибок авторы библиотеки могут быть уверены, что знают все возможные типы ошибок и что впоследствии другие типы ошибок не появятся.

Иерархия в примере выглядит так:

Hierarchy illustration of sealed classes and interfaces

Конструкторы

Сам запечатанный класс всегда является абстрактным классом, поэтому его нельзя инстанцировать напрямую. Однако он может содержать конструкторы или наследовать их. Эти конструкторы предназначены не для создания экземпляров самого запечатанного класса, а для его подклассов. Рассмотрим следующий пример с запечатанным классом Error и несколькими его подклассами, экземпляры которых мы создаём:

sealed class Error(val message: String) {
    class NetworkError : Error("Network failure")
    class DatabaseError : Error("Database cannot be reached")
    class UnknownError : Error("An unknown error has occurred")
}

fun main() {
    val errors = listOf(Error.NetworkError(), Error.DatabaseError(), Error.UnknownError())
    errors.forEach { println(it.message) }
}
// Network failure 
// Database cannot be reached 
// An unknown error has occurred

В запечатанных классах можно использовать классы enum, чтобы представлять состояния константами перечисления и предоставлять дополнительные сведения. Каждая константа перечисления существует только в виде единственного экземпляра, тогда как у подклассов запечатанного класса может быть несколько экземпляров. В примере sealed class Error вместе с несколькими подклассами использует enum для обозначения степени серьёзности ошибки. Конструктор каждого подкласса инициализирует severity и может изменять его состояние:

enum class ErrorSeverity { MINOR, MAJOR, CRITICAL }

sealed class Error(val severity: ErrorSeverity) {
    class FileReadError(val file: File): Error(ErrorSeverity.MAJOR)
    class DatabaseError(val source: DataSource): Error(ErrorSeverity.CRITICAL)
    object RuntimeError : Error(ErrorSeverity.CRITICAL)
    // Additional error types can be added here
}

Конструкторы запечатанных классов могут иметь одну из двух областей видимости: protected (по умолчанию) или private:

sealed class IOError {
    // A sealed class constructor has protected visibility by default. It's visible inside this class and its subclasses 
    constructor() { /*...*/ }

    // Private constructor, visible inside this class only. 
    // Using a private constructor in a sealed class allows for even stricter control over instantiation, enabling specific initialization procedures within the class.
    private constructor(description: String): this() { /*...*/ }

    // This will raise an error because public and internal constructors are not allowed in sealed classes
    // public constructor(code: Int): this() {} 
}

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

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

У подклассов запечатанных классов должно быть полное имя. Они не могут быть локальными или анонимными объектами.

Классы enum не могут расширять запечатанный класс или любой другой класс. Однако они могут реализовывать запечатанные интерфейсы:

sealed interface Error

// enum class extending the sealed interface Error
enum class ErrorType : Error {
    FILE_ERROR, DATABASE_ERROR
}

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

// Sealed interface 'Error' has implementations only in the same package and module
sealed interface Error

// Sealed class 'IOError' extends 'Error' and is extendable only within the same package
sealed class IOError(): Error

// Open class 'CustomError' extends 'Error' and can be extended anywhere it's visible
open class CustomError(): Error

Наследование в мультиплатформенных проектах

В мультиплатформенных проектах действует ещё одно ограничение наследования: прямые подклассы запечатанных классов должны находиться в одном наборе исходного кода. Оно применяется к запечатанным классам без модификаторов expect и actual.

Если запечатанный класс объявлен как expect в общем наборе исходного кода и имеет реализации actual в наборах исходного кода для платформ, то и версии expect, и actual могут иметь подклассы в своих наборах исходного кода. Кроме того, при использовании иерархической структуры можно создавать подклассы в любом наборе исходного кода между объявлениями expect и actual.

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

Использование запечатанных классов с выражением when

Главное преимущество запечатанных классов проявляется при их использовании в выражении when. Выражение when с запечатанным классом позволяет компилятору Kotlin исчерпывающе проверить, что учтены все возможные случаи. В таких ситуациях добавлять предложение else не нужно:

// Sealed class and its subclasses
sealed class Error {
    class FileReadError(val file: String): Error()
    class DatabaseError(val source: String): Error()
    object RuntimeError : Error()
}

//sampleStart
// Function to log errors
fun log(e: Error) = when(e) {
    is Error.FileReadError -> println("Error while reading file ${e.file}")
    is Error.DatabaseError -> println("Error while reading from database ${e.source}")
    Error.RuntimeError -> println("Runtime error")
    // No `else` clause is required because all the cases are covered
}
//sampleEnd

// List all errors
fun main() {
    val errors = listOf(
        Error.FileReadError("example.txt"),
        Error.DatabaseError("usersDatabase"),
        Error.RuntimeError
    )

    errors.forEach { log(it) }
}

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

Подробнее см. в разделе Предварительная версия контекстно-зависимого разрешения или в соответствующем предложении KEEP.

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

В мультиплатформенных проектах, если у вас есть запечатанный класс с выражением when в качестве ожидаемого объявления в общем коде, всё равно потребуется ветвь else. Это связано с тем, что подклассы actual реализаций для платформ могут расширять запечатанные классы, неизвестные в общем коде.

Сценарии использования

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

Управление состоянием в приложениях с пользовательским интерфейсом

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

sealed class UIState { 
    data object Loading : UIState()
    data class Success(val data: String) : UIState()
    data class Error(val exception: Exception) : UIState()
}

fun updateUI(state: UIState) { 
    when (state) {
        is UIState.Loading -> showLoadingIndicator()
        is UIState.Success -> showData(state.data)
        is UIState.Error -> showError(state.exception) 
    }
}

Обработка способов оплаты

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

sealed class Payment {
    data class CreditCard(val number: String, val expiryDate: String) : Payment()
    data class PayPal(val email: String) : Payment()
    data object Cash : Payment()
}

fun processPayment(payment: Payment) { 
    when (payment) {
        is Payment.CreditCard -> processCreditCardPayment(payment.number, payment.expiryDate)
        is Payment.PayPal -> processPayPalPayment(payment.email)
        is Payment.Cash -> processCashPayment() 
    }
}

Payment — это запечатанный класс, представляющий различные способы оплаты в системе электронной коммерции: CreditCard, PayPal и Cash. Каждый подкласс может иметь собственные свойства, например number и expiryDate для CreditCard и email для PayPal.

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

Обработка запросов и ответов API

Запечатанные классы и интерфейсы можно использовать для реализации системы аутентификации пользователей, обрабатывающей запросы и ответы API. Система аутентификации пользователей поддерживает вход и выход из системы. Запечатанный интерфейс ApiRequest определяет конкретные типы запросов: LoginRequest для входа и LogoutRequest для выхода из системы. Запечатанный класс ApiResponse инкапсулирует различные сценарии ответа: UserSuccess с данными пользователя, UserNotFound для отсутствующих пользователей и Error для любых сбоев. Функция handleRequest обрабатывает эти запросы типобезопасным способом с помощью выражения when, а getUserById имитирует получение данных пользователя:

// Import necessary modules
import io.ktor.server.application.*
import io.ktor.server.resources.*

import kotlinx.serialization.*

// Define the sealed interface for API requests using Ktor resources
@Resource("api")
sealed interface ApiRequest

@Serializable
@Resource("login")
data class LoginRequest(val username: String, val password: String) : ApiRequest


@Serializable
@Resource("logout")
object LogoutRequest : ApiRequest

// Define the ApiResponse sealed class with detailed response types
sealed class ApiResponse {
    data class UserSuccess(val user: UserData) : ApiResponse()
    data object UserNotFound : ApiResponse()
    data class Error(val message: String) : ApiResponse()
}

// User data class to be used in the success response
data class UserData(val userId: String, val name: String, val email: String)

// Function to validate user credentials (for demonstration purposes)
fun isValidUser(username: String, password: String): Boolean {
    // Some validation logic (this is just a placeholder)
    return username == "validUser" && password == "validPass"
}

// Function to handle API requests with detailed responses
fun handleRequest(request: ApiRequest): ApiResponse {
    return when (request) {
        is LoginRequest -> {
            if (isValidUser(request.username, request.password)) {
                ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail"))
            } else {
                ApiResponse.Error("Invalid username or password")
            }
        }
        is LogoutRequest -> {
            // Assuming logout operation always succeeds for this example
            ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail")) // For demonstration
        }
    }
}

// Function to simulate a getUserById call
fun getUserById(userId: String): ApiResponse {
    return if (userId == "validUserId") {
        ApiResponse.UserSuccess(UserData("validUserId", "John Doe", "john@example.com"))
    } else {
        ApiResponse.UserNotFound
    }
    // Error handling would also result in an Error response.
}

// Main function to demonstrate the usage
fun main() {
    val loginResponse = handleRequest(LoginRequest("user", "pass"))
    println(loginResponse)

    val logoutResponse = handleRequest(LogoutRequest)
    println(logoutResponse)

    val userResponse = getUserById("validUserId")
    println(userResponse)

    val userNotFoundResponse = getUserById("invalidId")
    println(userNotFoundResponse)
}

Что дальше

Подробнее о наследовании в Kotlin.

29 июня 2026 г.
Выражения thisКлассы перечислений

© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/sealed-classes.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API