Прямые ссылки на публичные уроки для быстрого старта и стабильной индексации lesson-страниц.
Полиморфизм — частый вопрос на собеседованиях, поэтому разберём его структурно: сначала суть, затем типы, в конце — примеры.
Название происходит от греческих слов «поли» (много) и «морф» (форма). Полиморфизм часто путают с наследованием. Разница принципиальная: наследование — это про экземпляр класса-наследника, который включает поля и функциональность родителя. Полиморфизм делает акцент на типах, а не на классах. Полиморфизм — это когда один интерфейс используется для разных типов. Иными словами, в одну функцию можно передавать параметры разных типов.
Ad hoc (по случаю) — одна функция определяется для различных типов данных. В классе прописывается несколько функций с одинаковым именем, но разными параметрами. Компилятор определяет нужную реализацию по количеству и типам аргументов. Минус: требуется создавать отдельную реализацию для каждого типа.
Subtyping (полиморфизм включения) — реализация через принцип подстановки Барбары Лисков (LSP из SOLID): функции, работающие с базовым типом, должны иметь возможность работать с его подтипами, не зная об этом. Объект более узкого типа всегда может использоваться там, где ожидается объект более широкого типа. Именно здесь возникает путаница с наследованием.
Parametric (параметрический) — программа реализуется через обобщённые типы без привязки к конкретному. В Kotlin это дженерики (generics). Рассмотрим их отдельно.
Воспроизведём Ad hoc полиморфизм. Создадим класс NotesAppItem с методом addItemToCell(), принимающим параметры:
title — заголовок ячейки (String)creationDate — дата создания (Date из стандартной библиотеки)type — тип содержимого (String)data — тело заметкиclass NotesAppItem { fun addItemToCell( title: String, creationDate: Date, type: String, data: String, ) { println("Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n") } }
Создадим экземпляр и вызовем метод:
fun main() { val creationDate = Date() val notes = NotesAppItem() notes.addItemToCell( "call sister", creationDate, "message", "call sister to congratulate", ) }
По условиям приложение должно сохранять заметки с разным типом контента: текст, номер телефона, список дел. Согласно Ad hoc полиморфизму, добавляем перегруженные версии метода с тем же именем, но разными типами параметра data:
class NotesItem { fun addItemToCell( title: String, creationDate: Date, type: String, data: String, ) { println("Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n") } fun addItemToCell( title: String, creationDate: Date, type: String, data: Int, ) { println("Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n") } fun addItemToCell( title: String, creationDate: Date, type: String, data: List<String>, ) { println("Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n") } }
При вызове метода компилятор определяет нужную реализацию по типу передаваемого аргумента. Так работает Ad hoc полиморфизм.
Второй тип реализуется с помощью наследования, однако между ними есть фундаментальное различие. Полиморфизм — это способность объекта вести себя множеством способов. Наследование — это создание нового класса с использованием свойств и методов родителя.
Под каждый тип заметки создадим класс-наследник от NotesAppItem. Базовый класс помечаем как open. Метод переименуем в getItemData() — без параметров, возвращает строку. В каждом подклассе поля объявлены приватными, метод переопределён с индивидуальной реализацией:
open class NotesAppItem { open fun getItemData() = "" } class MessageItem( private val title: String, private val creationDate: Date, private val type: String, private val data: String, ) : NotesAppItem() { override fun getItemData(): String { return "Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n" } } class PhoneItem( private val title: String, private val creationDate: Date, private val type: String, private val data: Long, ) : NotesAppItem() { override fun getItemData(): String { return "Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n" } } class ListItem( private val title: String, private val creationDate: Date, private val type: String, private val data: List<String>, ) : NotesAppItem() { override fun getItemData(): String { return "Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n" } }
Создаём экземпляры и вызываем методы:
val creationDate = Date() val notes = NotesItem() val messageItem = MessageItem( "call sister", creationDate, "message", "call sister to congratulate", ) val phoneItem = PhoneItem( "sister's number", creationDate, "phone", 89914424242, ) val toDoListItem = ListItem( "todolist", creationDate, "list", listOf("wash dog", "do the cleaning", "buy new shoes"), ) println(messageItem.getItemData()) println(phoneItem.getItemData()) println(toDoListItem.getItemData())
Вывод не отличается от предыдущей реализации, но это ещё не полиморфизм. Код работал бы даже без наследования. Наследование начинает играть роль, когда появляется общий код для этих типов объектов.
Ключевые свойства: родитель может хранить ссылку на дочерний класс и вызывать его переопределённый метод с одним именем.
Это позволяет объединить объекты разных типов в массив базового типа и обработать их единообразно:
fun main() { val creationDate = Date() val notes = NotesAppItem() val messageItem: MessageItem = MessageItem( "call sister", creationDate, "message", "call sister to congratulate", ) val phoneItem: PhoneItem = PhoneItem( "sister's number", creationDate, "phone", 89914424242, ) val toDoListItem: ListItem = ListItem( "todolist", creationDate, "list", listOf("wash dog", "do the cleaning", "buy new shoes"), ) val list = arrayOf<NotesAppItem>(messageItem, phoneItem, toDoListItem) showAllNotes(list) } fun showAllNotes(notes: Array<NotesAppItem>) { notes.forEach { println(it.getItemData()) } }
Благодаря полиморфизму новый тип заметки легко встраивается в существующую логику без изменения функции showAllNotes() — достаточно добавить новый подкласс и включить его объект в массив.