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

16 KiB
Raw Blame History

title, description, tags
title description tags
Полиморфизм Полиморфизм подтипов, перегрузка, параметрический полиморфизм и что разрешается статически
kotlin
oop

Полиморфизм - возможность работать с разными типами через один интерфейс. В Kotlin четыре формы:

Форма Механизм Когда выбирается реализация
подтипов override в рантайме, по фактическому типу
ad-hoc (перегрузка) одно имя, разные параметры при компиляции
параметрический дженерики при компиляции (типы стираются)
через when по sealed сопоставление типов в рантайме, но проверяется компилятором

Полиморфизм подтипов

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, включая те, которые появятся позже. Выбор реализации происходит в рантайме по фактическому типу объекта - это динамическая диспетчеризация.

Перегрузка

Одно имя, разные параметры. Выбор - при компиляции, по статическим типам аргументов:

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 перегрузка нужна реже - её заменяют значения по умолчанию:

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
}

Подробнее: Функции

Параметрический полиморфизм

Один код для любого типа:

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
}

Ограничение параметра типа позволяет вызывать методы:

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
}

Подробнее: Параметрический полиморфизм

Полиморфизм через sealed и when

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

Что разрешается статически

Это главный источник ошибок ожиданий. Статически (по типу ссылки, а не объекта) выбираются:

Функции-расширения:

open class Base37
class Derived33 : Base37()

fun Base37.describe() = "база"
fun Derived33.describe() = "подкласс"

fun main() {
    val d: Base37 = Derived33()
    println(d.describe())              // база — по типу переменной!
    println(Derived33().describe())    // подкласс
}

Перегруженные функции:

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 - они не переопределяются:

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.

Приведение и проверка типа

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-типа или полиморфного метода. Подробнее: Приведение типов

Вариантность

Полиморфизм распространяется на дженерики через out и in:

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 так нельзя - он инвариантен. Подробнее: Вариантность

[!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 даёт ту же проверку полноты компилятором, но без дополнительного кода:

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, Полиморфизм подтипов, Ad-hoc полиморфизм

[!warning] Подводные камни Расширения не полиморфны - выбор по статическому типу. Это самая частая ошибка:

open class Base39
class Derived35 : Base39()

fun Base39.name() = "база"
fun Derived35.name() = "подкласс"

fun main() {
    println(listOf<Base39>(Derived35()).first().name())   // база
}

Нужен полиморфизм - нужен член класса.

Перегрузка выбирается по типу ссылки, а не объекта. Код ведёт себя по-разному в зависимости от того, как объявлена переменная.

Смешение перегрузки и переопределения даёт неочевидный результат:

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-метода из конструктора попадает в подкласс до его инициализации. Подробнее: Инициализация

Стирание типов ломает перегрузку по дженерикам:

// 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:

fun main() {
    val strings: List<String> = listOf("a")
    val anys: List<Any> = strings            // ok
    println(anys.size)                        // 1
    // val arr: Array<Any> = arrayOf("a")    // ошибка компиляции
}

Дополнительно: Переопределение Далее: Система исключений