Spec-Zone.ru › Kotlin 2

Классы и интерфейсы

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

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

В предыдущей главе мы рассмотрели, как можно использовать функции-расширения, чтобы расширять классы, не изменяя исходный код. Но что, если вы работаете над чем-то сложным и было бы полезно совместно использовать код между классами? В таких случаях можно использовать наследование классов.

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

В Kotlin поддерживается только одиночное наследование: наследоваться можно только от одного класса за раз. Этот класс называется родительским.

Родительский класс может наследоваться от другого класса (класса-прародителя), образуя иерархию. На вершине иерархии классов Kotlin находится общий родительский класс: Any. В конечном итоге все классы наследуются от класса Any:

An example of the class hierarchy with Any type

Класс 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.
}
abstract class SmartDevice(val name: String) { abstract fun turnOn() abstract fun turnOff() } 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(name: String) : SmartDevice(name) { override fun turnOn() { println("$name thermostat is now heating.") } override fun turnOff() { println("$name thermostat is now off.") } fun adjustTemperature(temperature: Int) { println("$name thermostat set to $temperature°C.") } } 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
}
interface Media { val title: String fun play() } class Audio(override val title: String, val composer: String) : Media { override fun play() { println("Playing audio: $title, composed by $composer") } } fun main() { val audio = Audio("Symphony No. 5", "Beethoven") audio.play() // Playing audio: Symphony No. 5, composed by Beethoven }

Упражнение 3

Вы разрабатываете систему обработки платежей для приложения электронной коммерции. Каждый способ оплаты должен поддерживать авторизацию платежа и обработку транзакции. Для некоторых способов оплаты также необходимо поддерживать возврат средств.

  1. Добавьте в интерфейс Refundable функцию refund() для обработки возвратов средств.

  2. В абстрактном классе PaymentMethod:

    • Добавьте функцию authorize(), которая принимает сумму и выводит сообщение с этой суммой.

    • Добавьте абстрактную функцию processPayment(), которая также принимает сумму.

  3. Создайте класс 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.
}
interface Refundable { fun refund(amount: Double) } abstract class PaymentMethod(val name: String) { fun authorize(amount: Double) { println("Authorizing payment of $$amount.") } abstract fun processPayment(amount: Double) } class CreditCard(name: String) : PaymentMethod(name), Refundable { override fun processPayment(amount: Double) { println("Processing credit card payment of $$amount.") } override fun refund(amount: Double) { println("Refunding $$amount to the credit card.") } } 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].

Переписывать функцию receiveMessage() в классе SmartMessenger не нужно.

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!
}
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(val basicMessenger: BasicMessenger) : Messenger by basicMessenger { override fun sendMessage(message: String) { println("Sending a smart message: $message") basicMessenger.sendMessage("[smart] $message") } } 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! }
  • Предыдущий шаг

  • Следующий шаг

1 июля 2026 г.
Лямбда-выражения с получателемОбъекты

© 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

Spec-Zone.ru

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