16 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Полиморфизм | Полиморфизм подтипов, перегрузка, параметрический полиморфизм и что разрешается статически |
|
Полиморфизм - возможность работать с разными типами через один интерфейс. В 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сisVisitor для операций над иерархией 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") // ошибка компиляции }
Дополнительно: Переопределение Далее: Система исключений