Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
+325
View File
@@ -0,0 +1,325 @@
---
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)