Классы и интерфейсы
В вводном туре вы узнали, как использовать классы и классы данных для хранения данных и поддержания набора характеристик, которыми можно делиться в коде. Со временем вам может понадобиться создать иерархию, чтобы эффективно использовать общий код в рамках проектов. В этой главе рассказывается о возможностях, которые Kotlin предоставляет для совместного использования кода, и о том, как они могут сделать ваш код более безопасным и простым в обслуживании.
Наследование классов
В предыдущей главе мы рассмотрели, как можно использовать функции-расширения, чтобы расширять классы, не изменяя исходный код. Но что, если вы работаете над чем-то сложным и было бы полезно совместно использовать код между классами? В таких случаях можно использовать наследование классов.
По умолчанию от классов в Kotlin нельзя наследоваться. Kotlin устроен таким образом, чтобы предотвратить непреднамеренное наследование и упростить обслуживание ваших классов.
В Kotlin поддерживается только одиночное наследование: наследоваться можно только от одного класса за раз. Этот класс называется родительским.
Родительский класс может наследоваться от другого класса (класса-прародителя), образуя иерархию. На вершине иерархии классов Kotlin находится общий родительский класс: Any. В конечном итоге все классы наследуются от класса Any:
Класс Any автоматически предоставляет функцию toString() в качестве функции-члена. Поэтому вы можете использовать эту унаследованную функцию в любом из своих классов. Например:
class Car(val make: String, val model: String, val numberOfDoors: Int)
fun main() {
//sampleStart
val car1 = Car("Toyota", "Corolla", 4)
// Uses the .toString() function via string templates to print class properties
println("Car1: make=${car1.make}, model=${car1.model}, numberOfDoors=${car1.numberOfDoors}")
// Car1: make=Toyota, model=Corolla, numberOfDoors=4
//sampleEnd
}
Если вы хотите использовать наследование для совместного использования кода между классами, сначала рассмотрите возможность применения абстрактных классов.
Абстрактные классы
Абстрактные классы доступны для наследования по умолчанию. Они предназначены для предоставления членов, которые другие классы наследуют или реализуют. Поэтому у них есть конструктор, но создавать их экземпляры нельзя. В дочернем классе вы определяете поведение свойств и функций родительского класса с помощью ключевого слова override. Таким образом, можно сказать, что дочерний класс «переопределяет» члены родительского класса.
Абстрактные классы могут содержать как функции и свойства с реализацией, так и функции и свойства без реализации, которые называются абстрактными функциями и свойствами.
Чтобы создать абстрактный класс, используйте ключевое слово abstract:
abstract class Animal
Чтобы объявить функцию или свойство без реализации, также используйте ключевое слово abstract:
abstract fun makeSound() abstract val sound: String
Например, предположим, что вы хотите создать абстрактный класс с именем Product, от которого можно будет создавать дочерние классы для определения различных категорий товаров:
abstract class Product(val name: String, var price: Double) {
// Abstract property for the product category
abstract val category: String
// A function that can be shared by all products
fun productInfo(): String {
return "Product: $name, Category: $category, Price: $price"
}
}
В абстрактном классе:
В конструкторе есть два параметра:
nameиpriceтовара.Есть абстрактное свойство, содержащее категорию товара в виде строки.
Есть функция, которая выводит информацию о товаре.
Создадим дочерний класс для электроники. Прежде чем определить реализацию свойства category в дочернем классе, необходимо использовать ключевое слово override:
class Electronic(name: String, price: Double, val warranty: Int) : Product(name, price) {
override val category = "Electronic"
}
Класс Electronic:
Наследуется от абстрактного класса
Product.Содержит дополнительный параметр конструктора:
warranty, характерный для электроники.Переопределяет свойство
category, присваивая ему строку"Electronic".
Теперь эти классы можно использовать следующим образом:
abstract class Product(val name: String, var price: Double) {
// Abstract property for the product category
abstract val category: String
// A function that can be shared by all products
fun productInfo(): String {
return "Product: $name, Category: $category, Price: $price"
}
}
class Electronic(name: String, price: Double, val warranty: Int) : Product(name, price) {
override val category = "Electronic"
}
//sampleStart
fun main() {
// Creates an instance of the Electronic class
val laptop = Electronic(name = "Laptop", price = 1000.0, warranty = 2)
println(laptop.productInfo())
// Product: Laptop, Category: Electronic, Price: 1000.0
}
//sampleEnd
Хотя абстрактные классы отлично подходят для такого совместного использования кода, их возможности ограничены, поскольку в Kotlin поддерживается только одиночное наследование. Если вам нужно наследоваться от нескольких источников, рассмотрите возможность использования интерфейсов.
Интерфейсы
Интерфейсы похожи на классы, но имеют несколько отличий:
Нельзя создать экземпляр интерфейса. У интерфейсов нет конструктора или заголовка.
Их функции и свойства по умолчанию неявно доступны для наследования. В Kotlin говорят, что они «открытые».
Не нужно помечать функции ключевым словом
abstract, если вы не предоставляете для них реализацию.
Как и абстрактные классы, интерфейсы используются для определения набора функций и свойств, которые классы могут унаследовать и реализовать позднее. Такой подход помогает сосредоточиться на абстракции, описанной интерфейсом, а не на деталях конкретной реализации. Использование интерфейсов делает код:
Более модульным, поскольку разные части изолированы и могут развиваться независимо друг от друга.
Более понятным благодаря объединению связанных функций в целостный набор.
Более простым для тестирования, поскольку реализацию можно быстро заменить заглушкой.
Чтобы объявить интерфейс, используйте ключевое слово interface:
interface PaymentMethod
Реализация интерфейса
Интерфейсы поддерживают множественное наследование, поэтому класс может реализовывать сразу несколько интерфейсов. Сначала рассмотрим случай, когда класс реализует один интерфейс.
Чтобы создать класс, реализующий интерфейс, добавьте двоеточие после заголовка класса, а затем укажите имя интерфейса, который хотите реализовать. После имени интерфейса не используются круглые скобки (), поскольку у интерфейсов нет конструктора:
class CreditCardPayment : PaymentMethod
Например:
interface PaymentMethod {
// Functions are inheritable by default
fun initiatePayment(amount: Double): String
}
class CreditCardPayment(val cardNumber: String, val cardHolderName: String, val expiryDate: String) : PaymentMethod {
override fun initiatePayment(amount: Double): String {
// Simulate processing payment with credit card
return "Payment of $$amount initiated using Credit Card ending in ${cardNumber.takeLast(4)}."
}
}
fun main() {
val paymentMethod = CreditCardPayment("1234 5678 9012 3456", "John Doe", "12/25")
println(paymentMethod.initiatePayment(100.0))
// Payment of $100.0 initiated using Credit Card ending in 3456.
}
В этом примере:
PaymentMethod— это интерфейс с функциейinitiatePayment()без реализации.CreditCardPayment— это класс, реализующий интерфейсPaymentMethod.Класс
CreditCardPaymentпереопределяет унаследованную функциюinitiatePayment().paymentMethod— это экземпляр классаCreditCardPayment.Переопределённая функция
initiatePayment()вызывается для экземпляраpaymentMethodс параметром100.0.
Чтобы создать класс, реализующий несколько интерфейсов, добавьте двоеточие после заголовка класса, а затем укажите через запятую имена интерфейсов, которые хотите реализовать:
class CreditCardPayment : PaymentMethod, PaymentType
Например:
interface PaymentMethod {
fun initiatePayment(amount: Double): String
}
interface PaymentType {
val paymentType: String
}
class CreditCardPayment(val cardNumber: String, val cardHolderName: String, val expiryDate: String) : PaymentMethod,
PaymentType {
override fun initiatePayment(amount: Double): String {
// Simulate processing payment with credit card
return "Payment of $$amount initiated using Credit Card ending in ${cardNumber.takeLast(4)}."
}
override val paymentType: String = "Credit Card"
}
fun main() {
val paymentMethod = CreditCardPayment("1234 5678 9012 3456", "John Doe", "12/25")
println(paymentMethod.initiatePayment(100.0))
// Payment of $100.0 initiated using Credit Card ending in 3456.
println("Payment is by ${paymentMethod.paymentType}")
// Payment is by Credit Card
}
В этом примере:
PaymentMethod— это интерфейс с функциейinitiatePayment()без реализации.PaymentType— это интерфейс со свойствомpaymentType, которому не присвоено начальное значение.CreditCardPayment— это класс, реализующий интерфейсыPaymentMethodиPaymentType.Класс
CreditCardPaymentпереопределяет унаследованную функциюinitiatePayment()и свойствоpaymentType.paymentMethod— это экземпляр классаCreditCardPayment.Переопределённая функция
initiatePayment()вызывается для экземпляраpaymentMethodс параметром100.0.К экземпляру
paymentMethodобращаются для получения переопределённого свойстваpaymentType.
Подробнее об интерфейсах и наследовании интерфейсов см. в разделе Интерфейсы.
Делегирование
Интерфейсы полезны, но если интерфейс содержит много функций, в дочерних классах может оказаться много шаблонного кода. Если вы хотите переопределить лишь небольшую часть поведения класса, вам придётся многократно повторять один и тот же код.
Например, предположим, что у вас есть интерфейс DrawingTool с несколькими функциями и одним свойством color:
interface DrawingTool {
val color: String
fun draw(shape: String)
fun erase(area: String)
fun getToolInfo(): String
}
Вы создаёте класс PenTool, реализующий интерфейс DrawingTool и предоставляющий реализации для всех его членов:
class PenTool : DrawingTool {
override val color: String = "black"
override fun draw(shape: String) {
println("Drawing $shape using a pen in $color")
}
override fun erase(area: String) {
println("Erasing $area with pen tool")
}
override fun getToolInfo(): String {
return "PenTool(color=$color)"
}
}
Вы хотите создать класс, похожий на PenTool, с таким же поведением, но другим значением color. Один из вариантов — создать новый класс, который принимает в качестве параметра объект, реализующий интерфейс DrawingTool, например экземпляр класса PenTool. Затем внутри класса можно переопределить свойство color.
Однако в этом случае вам придётся добавить реализации для каждого члена интерфейса DrawingTool:
interface DrawingTool {
val color: String
fun draw(shape: String)
fun erase(area: String)
fun getToolInfo(): String
}
class PenTool : DrawingTool {
override val color: String = "black"
override fun draw(shape: String) {
println("Drawing $shape using a pen in $color")
}
override fun erase(area: String) {
println("Erasing $area with pen tool")
}
override fun getToolInfo(): String {
return "PenTool(color=$color)"
}
}
//sampleStart
class CanvasSession(val tool: DrawingTool) : DrawingTool {
override val color: String = "blue"
override fun draw(shape: String) {
tool.draw(shape)
}
override fun erase(area: String) {
tool.erase(area)
}
override fun getToolInfo(): String {
return tool.getToolInfo()
}
}
//sampleEnd
fun main() {
val pen = PenTool()
val session = CanvasSession(pen)
println("Pen color: ${pen.color}")
// Pen color: black
println("Session color: ${session.color}")
// Session color: blue
session.draw("circle")
// Drawing circle with pen in black
session.erase("top-left corner")
// Erasing top-left corner with pen tool
println(session.getToolInfo())
// PenTool(color=black)
}
Как видите, если интерфейс DrawingTool содержит большое количество функций-членов, объём шаблонного кода в классе CanvasSession может быть значительным. Но есть и другой вариант.
В Kotlin можно делегировать реализацию интерфейса экземпляру класса с помощью ключевого слова by. Например:
class CanvasSession(val tool: DrawingTool) : DrawingTool by tool
Здесь tool — это имя экземпляра класса PenTool, которому делегируется реализация функций-членов.
Теперь вам не нужно добавлять реализации функций-членов в класс CanvasSession. Компилятор автоматически делает это за вас, используя класс PenTool. Это избавляет от необходимости писать много шаблонного кода. Вместо этого вы добавляете код только для того поведения дочернего класса, которое хотите изменить.
Например, если вы хотите изменить значение свойства color:
interface DrawingTool {
val color: String
fun draw(shape: String)
fun erase(area: String)
fun getToolInfo(): String
}
class PenTool : DrawingTool {
override val color: String = "black"
override fun draw(shape: String) {
println("Drawing $shape using a pen in $color")
}
override fun erase(area: String) {
println("Erasing $area with pen tool")
}
override fun getToolInfo(): String {
return "PenTool(color=$color)"
}
}
//sampleStart
class CanvasSession(val tool: DrawingTool) : DrawingTool by tool {
// No boilerplate code!
override val color: String = "blue"
}
//sampleEnd
fun main() {
val pen = PenTool()
val session = CanvasSession(pen)
println("Pen color: ${pen.color}")
// Pen color: black
println("Session color: ${session.color}")
// Session color: blue
session.draw("circle")
// Drawing circle with pen in black
session.erase("top-left corner")
// Erasing top-left corner with pen tool
println(session.getToolInfo())
// PenTool(color=black)
}
При желании можно также переопределить поведение унаследованной функции-члена в классе CanvasSession, но теперь не нужно добавлять новые строки кода для каждой унаследованной функции-члена.
Подробнее см. в разделе Делегирование.
Практика
Упражнение 1
Представьте, что вы работаете над системой умного дома. Обычно в умном доме есть устройства разных типов, у которых есть общие базовые функции, а также уникальные особенности. В приведённом ниже примере кода дополните класс SmartDevice, объявленный как abstract, чтобы дочерний класс SmartLight успешно компилировался.
Затем создайте ещё один дочерний класс SmartThermostat, который наследуется от класса SmartDevice и реализует функции turnOn() и turnOff(), выводящие сообщения о том, какой термостат нагревает помещение или выключен. Наконец, добавьте ещё одну функцию adjustTemperature(), которая принимает на вход измеренное значение температуры и выводит: $name thermostat set to $temperature°C.
- Подсказка
В классе
SmartDeviceдобавьте функцииturnOn()иturnOff(), чтобы позднее переопределить их поведение в классеSmartThermostat.
abstract class // Write your code here
class SmartLight(name: String) : SmartDevice(name) {
override fun turnOn() {
println("$name is now ON.")
}
override fun turnOff() {
println("$name is now OFF.")
}
fun adjustBrightness(level: Int) {
println("Adjusting $name brightness to $level%.")
}
}
class SmartThermostat // Write your code here
fun main() {
val livingRoomLight = SmartLight("Living Room Light")
val bedroomThermostat = SmartThermostat("Bedroom Thermostat")
livingRoomLight.turnOn()
// Living Room Light is now ON.
livingRoomLight.adjustBrightness(10)
// Adjusting Living Room Light brightness to 10%.
livingRoomLight.turnOff()
// Living Room Light is now OFF.
bedroomThermostat.turnOn()
// Bedroom Thermostat thermostat is now heating.
bedroomThermostat.adjustTemperature(5)
// Bedroom Thermostat thermostat set to 5°C.
bedroomThermostat.turnOff()
// Bedroom Thermostat thermostat is now off.
}
Упражнение 2
Создайте интерфейс Media для реализации конкретных классов мультимедиа, таких как Audio, Video или Podcast. Интерфейс должен содержать:
Свойство
title, представляющее название мультимедиа.Функцию
play()для воспроизведения мультимедиа.
Затем создайте класс Audio, реализующий интерфейс Media. В конструкторе класса Audio должно использоваться свойство title, а также должно быть дополнительное свойство composer типа String. Реализуйте в классе функцию play(), которая выводит следующее: "Playing audio: $title, composed by $composer".
- Подсказка
Чтобы реализовать свойство интерфейса в конструкторе, можно использовать ключевое слово
overrideв заголовке класса.
interface // Write your code here
class // Write your code here
fun main() {
val audio = Audio("Symphony No. 5", "Beethoven")
audio.play()
// Playing audio: Symphony No. 5, composed by Beethoven
}
Упражнение 3
Вы разрабатываете систему обработки платежей для приложения электронной коммерции. Каждый способ оплаты должен поддерживать авторизацию платежа и обработку транзакции. Для некоторых способов оплаты также необходимо поддерживать возврат средств.
Добавьте в интерфейс
Refundableфункциюrefund()для обработки возвратов средств.-
В абстрактном классе
PaymentMethod:Добавьте функцию
authorize(), которая принимает сумму и выводит сообщение с этой суммой.Добавьте абстрактную функцию
processPayment(), которая также принимает сумму.
-
Создайте класс
CreditCard, реализующий интерфейсRefundableи наследующий абстрактный классPaymentMethod. Добавьте в этот класс реализации функцийrefund()иprocessPayment(), чтобы они выводили следующие сообщения:"Refunding $amount to the credit card.""Processing credit card payment of $amount."
interface Refundable {
// Write your code here
}
abstract class PaymentMethod(val name: String) {
// Write your code here
}
class CreditCard // Write your code here
fun main() {
val visa = CreditCard("Visa")
visa.authorize(100.0)
// Authorizing payment of $100.0.
visa.processPayment(100.0)
// Processing credit card payment of $100.0.
visa.refund(50.0)
// Refunding $50.0 to the credit card.
}
Упражнение 4
У вас есть простое приложение для обмена сообщениями с базовыми функциями, и вы хотите добавить возможности для умных сообщений, не дублируя при этом значительную часть кода.
В приведённом ниже коде объявите класс SmartMessenger, который наследуется от интерфейса Messenger, делегируя реализацию экземпляру класса BasicMessenger.
В классе SmartMessenger переопределите функцию sendMessage() для отправки умных сообщений. Функция должна принимать на вход message и выводить сообщение: "Sending a smart message: $message". Кроме того, вызовите функцию sendMessage() из класса BasicMessenger и добавьте к сообщению префикс [smart].
interface Messenger {
fun sendMessage(message: String)
fun receiveMessage(): String
}
class BasicMessenger : Messenger {
override fun sendMessage(message: String) {
println("Sending message: $message")
}
override fun receiveMessage(): String {
return "You've got a new message!"
}
}
class SmartMessenger // Write your code here
fun main() {
val basicMessenger = BasicMessenger()
val smartMessenger = SmartMessenger(basicMessenger)
basicMessenger.sendMessage("Hello!")
// Sending message: Hello!
println(smartMessenger.receiveMessage())
// You've got a new message!
smartMessenger.sendMessage("Hello from SmartMessenger!")
// Sending a smart message: Hello from SmartMessenger!
// Sending message: [smart] Hello from SmartMessenger!
}
© 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-classes-interfaces.html