--- title: Полиморфизм description: Полиморфизм подтипов, перегрузка, параметрический полиморфизм и что разрешается статически tags: - kotlin - oop --- Полиморфизм - возможность работать с разными типами через один интерфейс. В Kotlin четыре формы: | Форма | Механизм | Когда выбирается реализация | |---|---|---| | подтипов | `override` | в рантайме, по фактическому типу | | ad-hoc (перегрузка) | одно имя, разные параметры | при компиляции | | параметрический | дженерики | при компиляции (типы стираются) | | через `when` по sealed | сопоставление типов | в рантайме, но проверяется компилятором | ## Полиморфизм подтипов ```kotlin interface Shape5 { fun area(): Double fun name(): String } class Circle5(val r: Double) : Shape5 { override fun area() = Math.PI * r * r override fun name() = "круг" } class Square4(val side: Double) : Shape5 { override fun area() = side * side override fun name() = "квадрат" } fun describe(s: Shape5) = "${s.name()}: %.2f".format(java.util.Locale.US, s.area()) fun main() { val shapes: List = listOf(Circle5(1.0), Square4(2.0)) shapes.forEach { println(describe(it)) } // круг: 3.14 // квадрат: 4.00 println(shapes.sumOf { it.area() } > 7) // true } ``` Функция `describe` написана один раз и работает с любым `Shape5`, включая те, которые появятся позже. Выбор реализации происходит в рантайме по фактическому типу объекта - это динамическая диспетчеризация. ## Перегрузка Одно имя, разные параметры. Выбор - при компиляции, по статическим типам аргументов: ```kotlin fun format2(value: Int) = "целое: $value" fun format2(value: Double) = "дробное: $value" fun format2(value: String) = "строка: '$value'" fun format2(value: Any?) = "что-то: $value" fun main() { println(format2(1)) // целое: 1 println(format2(1.0)) // дробное: 1.0 println(format2("текст")) // строка: 'текст' println(format2(listOf(1))) // что-то: [1] val x: Any = 1 // статический тип Any! println(format2(x)) // что-то: 1 — не "целое: 1" } ``` Последняя строка показывает суть: компилятор видит тип переменной, а не объекта. Это основное отличие от полиморфизма подтипов. В Kotlin перегрузка нужна реже - её заменяют значения по умолчанию: ```kotlin fun connect(host: String, port: Int = 8080, secure: Boolean = false) = "$host:$port secure=$secure" fun main() { println(connect("localhost")) // localhost:8080 secure=false println(connect("localhost", secure = true)) // localhost:8080 secure=true } ``` Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md) ## Параметрический полиморфизм Один код для любого типа: ```kotlin fun firstOrDefault(items: List, default: T): T = if (items.isEmpty()) default else items[0] class Box4(private val value: T) { fun get(): T = value fun map(f: (T) -> R): Box4 = Box4(f(value)) } fun main() { println(firstOrDefault(listOf(1, 2), 0)) // 1 println(firstOrDefault(emptyList(), "нет")) // нет println(Box4(5).map { it * 2 }.get()) // 10 } ``` Ограничение параметра типа позволяет вызывать методы: ```kotlin fun > maxOf2(a: T, b: T): T = if (a > b) a else b fun main() { println(maxOf2(3, 7)) // 7 println(maxOf2("abc", "abd")) // abd } ``` Подробнее: [Параметрический полиморфизм](../Дженерики/Параметрический%20полиморфизм.md) ## Полиморфизм через sealed и when ```kotlin sealed interface Payment { data class Card(val number: String) : Payment data class Cash(val amount: Long) : Payment data object Invoice : Payment } fun process2(p: Payment): String = when (p) { is Payment.Card -> "карта ****${p.number.takeLast(4)}" is Payment.Cash -> "наличные ${p.amount}" Payment.Invoice -> "счёт" } fun main() { println(process2(Payment.Card("1234567890123456"))) // карта ****3456 println(process2(Payment.Cash(500))) // наличные 500 println(process2(Payment.Invoice)) // счёт } ``` Это альтернатива полиморфизму подтипов с обратным распределением ответственности: | | метод в классе | `when` по sealed | |---|---|---| | добавить новый тип | правок в одном месте | компилятор найдёт все `when` | | добавить новую операцию | правка во всех классах | один новый `when` | | логика операции | размазана по классам | собрана в одном месте | Выбор зависит от того, что меняется чаще. Набор типов стабилен, а операций много - `sealed` + `when`. Операции стабильны, а типы добавляются - методы в классах. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md) ## Что разрешается статически Это главный источник ошибок ожиданий. Статически (по типу ссылки, а не объекта) выбираются: **Функции-расширения:** ```kotlin open class Base37 class Derived33 : Base37() fun Base37.describe() = "база" fun Derived33.describe() = "подкласс" fun main() { val d: Base37 = Derived33() println(d.describe()) // база — по типу переменной! println(Derived33().describe()) // подкласс } ``` **Перегруженные функции:** ```kotlin open class Animal4 class Dog3 : Animal4() fun feed(a: Animal4) = "корм для животного" fun feed(d: Dog3) = "корм для собаки" fun main() { val a: Animal4 = Dog3() println(feed(a)) // корм для животного println(feed(Dog3())) // корм для собаки } ``` **Члены `companion object`** - они не переопределяются: ```kotlin open class Base38 { companion object { fun create() = "из Base" } } class Derived34 : Base38() { companion object { fun create() = "из Derived" } } fun main() { println(Base38.create()) // из Base println(Derived34.create()) // из Derived — выбор по имени класса } ``` Динамически выбираются только **члены класса**, помеченные `open`/`abstract` и переопределённые через `override`. ## Приведение и проверка типа ```kotlin fun handle(x: Any): String = when (x) { is String -> "строка длины ${x.length}" // умное приведение is Int -> "число ${x + 1}" is List<*> -> "список из ${x.size}" else -> "неизвестно: ${x::class.simpleName}" } fun main() { println(handle("abc")) // строка длины 3 println(handle(5)) // число 6 println(handle(listOf(1, 2))) // список из 2 println(handle(1.0)) // неизвестно: Double } ``` Длинная цепочка `is` часто означает, что иерархия просит `sealed`-типа или полиморфного метода. Подробнее: [Приведение типов](../Синтаксис%20и%20типы/Работа%20с%20данными/Приведение%20типов.md) ## Вариантность Полиморфизм распространяется на дженерики через `out` и `in`: ```kotlin open class Animal5(val name: String) class Dog4(name: String) : Animal5(name) fun printAll(animals: List) = animals.joinToString { it.name } fun main() { val dogs: List = listOf(Dog4("Рекс"), Dog4("Бобик")) println(printAll(dogs)) // Рекс, Бобик — List подходит как List } ``` Это работает, потому что `List` ковариантен. С `MutableList` так нельзя - он инвариантен. Подробнее: [Вариантность](../Дженерики/Вариантность.md) > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | метод виртуален по умолчанию | нужен `open` | > | `@Override` опциональна | `override` обязательно | > | перегрузка для опциональных параметров | значения по умолчанию | > | `instanceof` + каст | `is` + умное приведение | > | `switch` с pattern matching (Java 21) | `when` с `is` | > | Visitor для операций над иерархией | `sealed` + `when` | > | статические методы скрываются | члены компаньона не наследуются | > > **Механика полиморфизма подтипов идентична** - это та же виртуальная диспетчеризация JVM. Отличается значение по умолчанию: в Kotlin метод финален, пока не помечен `open`. > > **Visitor почти не нужен.** В Java для добавления операций к закрытой иерархии без правки классов применяют паттерн Visitor: интерфейс с методом на каждый тип, метод `accept` в каждом классе. `sealed` + `when` даёт ту же проверку полноты компилятором, но без дополнительного кода: > ```kotlin > sealed interface Expr2 { > data class Num2(val value: Int) : Expr2 > data class Add2(val l: Expr2, val r: Expr2) : Expr2 > } > > fun eval(e: Expr2): Int = when (e) { > is Expr2.Num2 -> e.value > is Expr2.Add2 -> eval(e.l) + eval(e.r) > } > > fun show(e: Expr2): String = when (e) { // новая операция — один when > is Expr2.Num2 -> "${e.value}" > is Expr2.Add2 -> "(${show(e.l)} + ${show(e.r)})" > } > > fun main() { > val expr = Expr2.Add2(Expr2.Num2(1), Expr2.Num2(2)) > println("${show(expr)} = ${eval(expr)}") // (1 + 2) = 3 > } > ``` > > **Умное приведение убирает двойную работу.** В Java после `instanceof` нужен явный каст (до версии 16, где появился pattern matching). > > Сравнить: [Полиморфизм в Java](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md), [Полиморфизм подтипов](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md), [Ad-hoc полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Ad-hoc%20(перегрузка).md) > [!warning] Подводные камни > **Расширения не полиморфны** - выбор по статическому типу. Это самая частая ошибка: > ```kotlin > open class Base39 > class Derived35 : Base39() > > fun Base39.name() = "база" > fun Derived35.name() = "подкласс" > > fun main() { > println(listOf(Derived35()).first().name()) // база > } > ``` > Нужен полиморфизм - нужен член класса. > > **Перегрузка выбирается по типу ссылки, а не объекта.** Код ведёт себя по-разному в зависимости от того, как объявлена переменная. > > **Смешение перегрузки и переопределения даёт неочевидный результат:** > ```kotlin > open class Base40 { open fun f(x: Any) = "any" } > class Derived36 : Base40() { > override fun f(x: Any) = "override" > fun f(x: Int) = "overload" > } > > fun main() { > val d = Derived36() > println(d.f(1)) // overload > println((d as Base40).f(1)) // override > } > ``` > > **Вызов `open`-метода из конструктора попадает в подкласс до его инициализации.** Подробнее: [Инициализация](Инициализация.md) > > **Стирание типов ломает перегрузку по дженерикам:** > ```kotlin > // fun show(items: List) = "строки" > // fun show(items: List) = "числа" // ошибка: та же JVM-сигнатура > > @JvmName("showStrings") > fun show2(items: List) = "строки: ${items.size}" > fun show2(items: List) = "числа: ${items.size}" > > fun main() { println(show2(listOf("a"))) } // строки: 1 > ``` > Обойти можно через `@JvmName`, но обычно это признак, что нужен один дженерик-метод. > > **Длинная цепочка `is` вместо полиморфизма.** Если `when (x) { is A -> ...; is B -> ... }` повторяется в нескольких местах, логику стоит перенести в методы типов или закрыть иерархию через `sealed`, чтобы компилятор контролировал полноту. > > **Члены `companion object` не переопределяются.** Попытка «переопределить» фабрику в подклассе создаёт независимый компаньон; вызов `Derived.create()` работает только потому, что имя разрешается статически. > > **Массивы инвариантны, а `List` ковариантен** - в отличие от Java, где массивы ковариантны и могут бросить `ArrayStoreException`: > ```kotlin > fun main() { > val strings: List = listOf("a") > val anys: List = strings // ok > println(anys.size) // 1 > // val arr: Array = arrayOf("a") // ошибка компиляции > } > ``` Дополнительно: [Переопределение](Переопределение.md) Далее: [Система исключений](../Исключения/Система%20исключений.md)