Files
obsidian/Kotlin/ООП/Полиморфизм.md
T

326 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<Shape5> = 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 <T> firstOrDefault(items: List<T>, default: T): T =
if (items.isEmpty()) default else items[0]
class Box4<T>(private val value: T) {
fun get(): T = value
fun <R> map(f: (T) -> R): Box4<R> = 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 <T : Comparable<T>> 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<Animal5>) = animals.joinToString { it.name }
fun main() {
val dogs: List<Dog4> = listOf(Dog4("Рекс"), Dog4("Бобик"))
println(printAll(dogs)) // Рекс, Бобик — List<Dog4> подходит как List<Animal5>
}
```
Это работает, потому что `List<out E>` ковариантен. С `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<Base39>(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<String>) = "строки"
> // fun show(items: List<Int>) = "числа" // ошибка: та же JVM-сигнатура
>
> @JvmName("showStrings")
> fun show2(items: List<String>) = "строки: ${items.size}"
> fun show2(items: List<Int>) = "числа: ${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<String> = listOf("a")
> val anys: List<Any> = strings // ok
> println(anys.size) // 1
> // val arr: Array<Any> = arrayOf("a") // ошибка компиляции
> }
> ```
Дополнительно: [Переопределение](Переопределение.md)
Далее: [Система исключений](../Исключения/Система%20исключений.md)