Открытые и специальные классы
В этой главе вы узнаете об открытых классах, их взаимодействии с интерфейсами и других специальных типах классов, доступных в Kotlin.
Открытые классы
Если вы не можете использовать интерфейсы или абстрактные классы, можно явно разрешить наследование от класса, объявив его открытым. Для этого используйте ключевое слово open перед объявлением класса:
open class Vehicle(val make: String, val model: String)
Чтобы создать класс, наследующий другой класс, поставьте двоеточие после заголовка класса, а затем вызовите конструктор родительского класса, от которого хотите наследоваться. В этом примере класс Car наследуется от класса Vehicle:
open class Vehicle(val make: String, val model: String)
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model)
fun main() {
// Creates an instance of the Car class
val car = Car("Toyota", "Corolla", 4)
// Prints the details of the car
println("Car Info: Make - ${car.make}, Model - ${car.model}, Number of doors - ${car.numberOfDoors}")
// Car Info: Make - Toyota, Model - Corolla, Number of doors - 4
}
Как и при создании обычного экземпляра класса, если ваш класс наследуется от родительского, он должен инициализировать все параметры, объявленные в заголовке родительского класса. Поэтому в примере экземпляр класса Car — car — инициализирует параметры родительского класса: make и model.
Переопределение унаследованного поведения
Если вы хотите наследоваться от класса, но изменить некоторые аспекты его поведения, можно переопределить унаследованное поведение.
По умолчанию переопределить функцию-член или свойство родительского класса нельзя. Как и в случае с абстрактными классами, для этого нужно добавить специальные ключевые слова.
Функции-члены
Чтобы разрешить переопределение функции в родительском классе, используйте ключевое слово open перед её объявлением в родительском классе:
open fun displayInfo() {}
Чтобы переопределить унаследованную функцию-член, используйте ключевое слово override перед объявлением функции в дочернем классе:
override fun displayInfo() {}
Например:
open class Vehicle(val make: String, val model: String) {
open fun displayInfo() {
println("Vehicle Info: Make - $make, Model - $model")
}
}
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model) {
override fun displayInfo() {
println("Car Info: Make - $make, Model - $model, Number of Doors - $numberOfDoors")
}
}
fun main() {
val car1 = Car("Toyota", "Corolla", 4)
val car2 = Car("Honda", "Civic", 2)
// Uses the overridden displayInfo() function
car1.displayInfo()
// Car Info: Make - Toyota, Model - Corolla, Number of Doors - 4
car2.displayInfo()
// Car Info: Make - Honda, Model - Civic, Number of Doors - 2
}
В этом примере:
Создаются два экземпляра класса
Car, наследующего классVehicle:car1иcar2.В классе
Carпереопределяется функцияdisplayInfo(), чтобы также выводить количество дверей.Для экземпляров
car1иcar2вызывается переопределённая функцияdisplayInfo().
Свойства
В Kotlin обычно не делают свойство наследуемым с помощью ключевого слова open, чтобы затем переопределить его. В большинстве случаев для этого используют абстрактный класс или интерфейс, в которых свойства наследуются по умолчанию.
Свойства открытых классов доступны их дочерним классам. Как правило, лучше обращаться к ним напрямую, а не переопределять новым свойством.
Например, допустим, у вас есть свойство transmissionType, которое вы хотите переопределить позднее. Синтаксис переопределения свойств точно такой же, как и для функций-членов. Это можно сделать так:
open class Vehicle(val make: String, val model: String) {
open val transmissionType: String = "Manual"
}
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model) {
override val transmissionType: String = "Automatic"
}
Однако это считается плохой практикой. Вместо этого можно добавить свойство в конструктор класса, от которого разрешено наследоваться, и объявить его значение при создании дочернего класса Car:
open class Vehicle(val make: String, val model: String, val transmissionType: String = "Manual") class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model, "Automatic")
Прямой доступ к свойствам вместо их переопределения делает код проще и понятнее. Объявляя свойства один раз в родительском классе и передавая их значения через конструктор, вы избавляетесь от необходимости переопределять их в дочерних классах.
Подробнее о наследовании классов и переопределении поведения классов см. в разделе Наследование.
Открытые классы и интерфейсы
Можно создать класс, который наследуется от другого класса и реализует несколько интерфейсов. В этом случае после двоеточия сначала нужно объявить родительский класс, а затем перечислить интерфейсы:
// Define interfaces
interface EcoFriendly {
val emissionLevel: String
}
interface ElectricVehicle {
val batteryCapacity: Double
}
// Parent class
open class Vehicle(val make: String, val model: String)
// Child class
open class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model)
// New class that inherits from Car and implements two interfaces
class ElectricCar(
make: String,
model: String,
numberOfDoors: Int,
val capacity: Double,
val emission: String
) : Car(make, model, numberOfDoors), EcoFriendly, ElectricVehicle {
override val batteryCapacity: Double = capacity
override val emissionLevel: String = emission
}
Специальные классы
Помимо абстрактных, открытых классов и классов данных, в Kotlin есть специальные типы классов для различных целей, например для ограничения определённого поведения или снижения влияния на производительность при создании небольших объектов.
Запечатанные классы
Иногда требуется ограничить наследование. Для этого можно использовать запечатанные классы. Запечатанные классы — это особый тип абстрактного класса. После объявления класса запечатанным создавать его дочерние классы можно только в пределах того же пакета. Наследоваться от запечатанного класса вне этой области нельзя.
Чтобы создать запечатанный класс, используйте ключевое слово sealed:
sealed class Mammal
Запечатанные классы особенно полезны в сочетании с выражением when. С помощью выражения when можно определить поведение для всех возможных дочерних классов. Например:
sealed class Mammal(val name: String)
class Cat(val catName: String) : Mammal(catName)
class Human(val humanName: String, val job: String) : Mammal(humanName)
fun greetMammal(mammal: Mammal): String {
when (mammal) {
is Human -> return "Hello ${mammal.name}; You're working as a ${mammal.job}"
is Cat -> return "Hello ${mammal.name}"
}
}
fun main() {
println(greetMammal(Cat("Snowy")))
// Hello Snowy
}
В этом примере:
Запечатанный класс
Mammalимеет параметрnameв конструкторе.Класс
Catнаследуется от запечатанного классаMammalи использует параметрcatNameв своём конструкторе в качестве параметраnameклассаMammal.Класс
Humanнаследуется от запечатанного классаMammalи использует параметрhumanNameв своём конструкторе в качестве параметраnameклассаMammal. В его конструкторе также есть параметрjob.Функция
greetMammal()принимает аргумент типаMammalи возвращает строку.В теле функции
greetMammal()находится выражениеwhen, которое использует операторis, чтобы проверить типmammalи определить, какое действие выполнить.Функция
main()вызывает функциюgreetMammal()с экземпляром классаCatи параметромnameпод названиемSnowy.
Подробнее о запечатанных классах и рекомендуемых сценариях их использования см. в разделе Запечатанные классы и интерфейсы.
Классы перечислений
Классы перечислений полезны, когда нужно представить в классе конечный набор различных значений. Класс перечисления содержит константы перечисления, каждая из которых сама является экземпляром этого класса.
Чтобы создать класс перечисления, используйте ключевое слово enum:
enum class State
Допустим, вы хотите создать класс перечисления, содержащий различные состояния процесса. Каждая константа перечисления должна отделяться запятой ,:
enum class State {
IDLE, RUNNING, FINISHED
}
Класс перечисления State содержит константы перечисления: IDLE, RUNNING и FINISHED. Чтобы обратиться к константе перечисления, укажите имя класса, затем . и имя константы перечисления:
val state = State.RUNNING
Этот класс перечисления можно использовать с выражением when, чтобы определить действие в зависимости от значения константы перечисления:
enum class State {
IDLE, RUNNING, FINISHED
}
fun main() {
val state = State.RUNNING
val message = when (state) {
State.IDLE -> "It's idle"
State.RUNNING -> "It's running"
State.FINISHED -> "It's finished"
}
println(message)
// It's running
}
Классы перечислений, как и обычные классы, могут иметь свойства и функции-члены.
Например, допустим, вы работаете с HTML и хотите создать класс перечисления с несколькими цветами. Вы хотите, чтобы у каждого цвета было свойство rgb со значением RGB в шестнадцатеричном формате. При создании констант перечисления необходимо инициализировать это свойство:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF),
YELLOW(0xFFFF00)
}
Чтобы добавить в этот класс функцию-член, отделите её от констант перечисления точкой с запятой ;:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF),
YELLOW(0xFFFF00);
fun containsRed() = (this.rgb and 0xFF0000 != 0)
}
fun main() {
val red = Color.RED
// Calls containsRed() function on enum constant
println(red.containsRed())
// true
// Calls containsRed() function on enum constants via class names
println(Color.BLUE.containsRed())
// false
println(Color.YELLOW.containsRed())
// true
}
В этом примере функция-член containsRed() с помощью ключевого слова this обращается к значению свойства rgb константы перечисления и проверяет, содержит ли шестнадцатеричное значение FF в качестве первых битов, чтобы вернуть логическое значение.
Подробнее см. в разделе Классы перечислений.
Встраиваемые классы-значения
Иногда в коде требуется создавать небольшие объекты классов и использовать их лишь непродолжительное время. Такой подход может повлиять на производительность. Встраиваемые классы-значения — это особый тип классов, позволяющий избежать такого влияния на производительность. Однако они могут содержать только значения.
Чтобы создать встраиваемый класс-значение, используйте ключевое слово value и аннотацию @JvmInline:
@JvmInline value class Email
У встраиваемого класса-значения должно быть одно свойство, инициализированное в заголовке класса.
Допустим, вы хотите создать класс для хранения адреса электронной почты:
// The address property is initialized in the class header.
@JvmInline
value class Email(val address: String)
fun sendEmail(email: Email) {
println("Sending email to ${email.address}")
}
fun main() {
val myEmail = Email("example@example.com")
sendEmail(myEmail)
// Sending email to example@example.com
}
В этом примере:
Email— это встраиваемый класс-значение с одним свойством в заголовке класса:address.Функция
sendEmail()принимает объекты типаEmailи выводит строку в стандартный поток вывода.-
Функция
main():Создаёт экземпляр класса
Emailс именемmyEmail.Вызывает функцию
sendEmail()для объектаmyEmail.
Используя встраиваемый класс-значение, вы встраиваете класс и можете использовать его непосредственно в коде, не создавая объект. Это позволяет значительно уменьшить объём занимаемой памяти и повысить производительность кода во время выполнения.
Подробнее о встраиваемых классах-значениях см. в разделе Встраиваемые классы-значения.
Практика
Упражнение 1
Вы управляете службой доставки и хотите отслеживать статусы посылок. Создайте запечатанный класс DeliveryStatus с классами данных, представляющими следующие статусы: Pending, InTransit, Delivered, Canceled. Дополните объявление класса DeliveryStatus так, чтобы код функции main() выполнялся без ошибок:
sealed class // Write your code here
fun printDeliveryStatus(status: DeliveryStatus) {
when (status) {
is DeliveryStatus.Pending -> {
println("The package is pending pickup from ${status.sender}.")
}
is DeliveryStatus.InTransit -> {
println("The package is in transit and expected to arrive by ${status.estimatedDeliveryDate}.")
}
is DeliveryStatus.Delivered -> {
println("The package was delivered to ${status.recipient} on ${status.deliveryDate}.")
}
is DeliveryStatus.Canceled -> {
println("The delivery was canceled due to: ${status.reason}.")
}
}
}
fun main() {
val status1: DeliveryStatus = DeliveryStatus.Pending("Alice")
val status2: DeliveryStatus = DeliveryStatus.InTransit("2024-11-20")
val status3: DeliveryStatus = DeliveryStatus.Delivered("2024-11-18", "Bob")
val status4: DeliveryStatus = DeliveryStatus.Canceled("Address not found")
printDeliveryStatus(status1)
// The package is pending pickup from Alice.
printDeliveryStatus(status2)
// The package is in transit and expected to arrive by 2024-11-20.
printDeliveryStatus(status3)
// The package was delivered to Bob on 2024-11-18.
printDeliveryStatus(status4)
// The delivery was canceled due to: Address not found.
}
Упражнение 2
В программе нужно обрабатывать различные статусы и типы ошибок. У вас есть запечатанный класс для представления разных статусов, объявленных в классах данных или объектах. Дополните приведённый ниже код, создав класс перечисления Problem, представляющий разные типы проблем: NETWORK, TIMEOUT и UNKNOWN.
sealed class Status {
data object Loading : Status()
data class Error(val problem: Problem) : Status() {
// Write your code here
}
data class OK(val data: List<String>) : Status()
}
fun handleStatus(status: Status) {
when (status) {
is Status.Loading -> println("Loading...")
is Status.OK -> println("Data received: ${status.data}")
is Status.Error -> when (status.problem) {
Status.Error.Problem.NETWORK -> println("Network issue")
Status.Error.Problem.TIMEOUT -> println("Request timed out")
Status.Error.Problem.UNKNOWN -> println("Unknown error occurred")
}
}
}
fun main() {
val status1: Status = Status.Error(Status.Error.Problem.NETWORK)
val status2: Status = Status.OK(listOf("Data1", "Data2"))
handleStatus(status1)
// Network issue
handleStatus(status2)
// Data received: [Data1, Data2]
}
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/kotlin-tour-intermediate-open-special-classes.html