Kotlin
Tutorial Kotlin: Herencia y polimorfismo
Kotlin herencia y polimorfismo: domina herencia, open y override para escribir código más robusto y reutilizable en la programación orientada a objetos.
Aprende Kotlin GRATIS y certifícateConcepto de herencia en Kotlin
La herencia es un principio clave en la Programación Orientada a Objetos que permite crear nuevas clases basadas en clases existentes. En Kotlin, la herencia se utiliza para reutilizar código y establecer relaciones jerárquicas entre clases, facilitando la creación de estructuras más complejas y organizadas.
En Kotlin, todas las clases heredan de manera implícita de la clase base Any
, que es la raíz de la jerarquía de clases y proporciona métodos básicos como equals()
, hashCode()
y toString()
. Cuando definimos una clase sin especificar una clase padre, esta hereda automáticamente de Any
.
Para permitir que una clase sea heredable, es necesario marcarla con la palabra clave open
, ya que, por defecto, las clases en Kotlin son finales y no se pueden extender. Esto significa que si no indicamos lo contrario, nuestras clases no podrán ser utilizadas como clases base.
Por ejemplo, definamos una clase abierta que represente a una persona:
open class Persona(val nombre: String, val edad: Int) {
fun presentar() {
println("Hola, me llamo $nombre y tengo $edad años.")
}
}
En este caso, Persona
es una clase abierta y puede ser heredada. Para crear una clase que herede de Persona
, utilizamos el símbolo de dos puntos :
seguido del nombre de la clase base y sus parámetros de constructor:
class Estudiante(nombre: String, edad: Int, val universidad: String) : Persona(nombre, edad) {
fun estudiar() {
println("$nombre está estudiando en la universidad $universidad.")
}
}
Aquí, Estudiante
es una clase que extiende a Persona
e incluye una propiedad adicional universidad
y un método específico estudiar()
. La clase derivada utiliza el constructor de la clase base para inicializar las propiedades heredadas.
Al crear una instancia de Estudiante
, podemos acceder tanto a los miembros de la clase derivada como a los heredados de la clase base:
val estudiante = Estudiante("Luis", 21, "Complutense")
estudiante.presentar() // Salida: Hola, me llamo Luis y tengo 21 años.
estudiante.estudiar() // Salida: Luis está estudiando en la universidad Complutense.
La herencia nos permite aprovechar el polimorfismo, permitiendo que un objeto de una clase derivada sea tratado como un objeto de su clase base. Esto es útil cuando queremos escribir código que funcione con cualquier subclase de una clase determinada.
Es importante destacar que métodos y propiedades heredados pueden ser utilizados directamente, pero para modificarlos o sobreescribirlos en la clase derivada, deben estar definidos como open
en la clase base. La modificación de estos miembros se abordará en secciones posteriores.
Uso de open
y override
En Kotlin, las clases y sus miembros son finales por defecto, lo que significa que no pueden ser heredados ni modificados por defecto. Para permitir la herencia y la sobrescritura de métodos o propiedades, es necesario utilizar la palabra clave open
. Esto indica que una clase o miembro está abierto a extensión.
Para declarar una clase heredable, se antepone open
a la definición de la clase:
open class Animal(val nombre: String) {
open fun hacerSonido() {
println("$nombre hace un sonido.")
}
}
En este ejemplo, la clase Animal
y su función hacerSonido()
están marcadas como open
, lo que permite que otras clases hereden de Animal
y sobrescriban hacerSonido()
.
Al crear una subclase, se utiliza el símbolo :
seguido del nombre de la clase base y se llama a su constructor:
class Perro(nombre: String) : Animal(nombre) {
override fun hacerSonido() {
println("$nombre ladra.")
}
}
La palabra clave override
se utiliza para indicar que el método hacerSonido()
está sobrescribiendo una implementación de la clase base. Es obligatorio en Kotlin utilizar override
al sobrescribir métodos, lo que mejora la seguridad y claridad del código.
También es posible sobrescribir propiedades. Para ello, la propiedad en la clase base debe estar marcada con open
y en la subclase se utiliza override
:
open class Empleado {
open val salario: Double = 30000.0
}
class Gerente : Empleado() {
override val salario: Double = 50000.0
}
En este caso, la propiedad salario
en Gerente
sobrescribe el valor definido en Empleado
.
Si se desea llamar al método o propiedad original de la clase base dentro de la sobrescritura, se puede utilizar super
:
class Gato(nombre: String) : Animal(nombre) {
override fun hacerSonido() {
super.hacerSonido()
println("$nombre maúlla.")
}
}
Aquí, Gato
llama al método hacerSonido()
de Animal
y luego añade comportamiento adicional.
Es importante destacar que si un método o propiedad no está declarado como open
, no puede ser sobrescrito. Intentar hacerlo generará un error de compilación. Esto garantiza la seguridad y consistencia en nuestras jerarquías de clases.
Además, si deseamos impedir que una función o propiedad abierta sea sobrescrita en clases derivadas, podemos marcarla como final
:
open class Persona {
open fun mostrarIdentidad() {
println("Soy una persona.")
}
}
class Ciudadano : Persona() {
final override fun mostrarIdentidad() {
println("Soy un ciudadano.")
}
}
En este ejemplo, mostrarIdentidad()
en Ciudadano
no podrá ser sobrescrito por ninguna subclase de Ciudadano
debido al uso de final
.
El uso consciente de open
y override
permite diseñar clases que son extensibles cuando es necesario, manteniendo al mismo tiempo el control sobre qué partes del código pueden ser modificadas. Esto es esencial para crear estructuras robustas en Programación Orientada a Objetos con Kotlin.
Polimorfismo y clases derivadas
El polimorfismo es un principio fundamental en la Programación Orientada a Objetos que permite tratar objetos de diferentes clases derivadas como si fueran instancias de su clase base común. En Kotlin, el polimorfismo facilita la creación de código más flexible y reutilizable, ya que permite que una misma referencia pueda apuntar a objetos de distintas clases derivadas.
Cuando definimos una jerarquía de clases, es posible declarar variables del tipo de la clase base y asignarles instancias de sus clases derivadas. Esto es posible porque una clase derivada es, por definición, una especialización de la clase base. A continuación, se presenta un ejemplo ilustrativo:
open class Empleado(val nombre: String) {
open fun trabajar() {
println("$nombre está trabajando.")
}
}
class Ingeniero(nombre: String, val especialidad: String) : Empleado(nombre) {
override fun trabajar() {
println("$nombre está trabajando en el área de $especialidad.")
}
}
class Gerente(nombre: String) : Empleado(nombre) {
override fun trabajar() {
println("$nombre está gestionando el equipo.")
}
}
En este ejemplo, Ingeniero
y Gerente
son clases derivadas de Empleado
, y cada una sobrescribe el método trabajar()
para proporcionar una implementación específica. Gracias al polimorfismo, podemos manejar objetos de estas clases de manera uniforme:
fun main() {
val empleados: List<Empleado> = listOf(
Ingeniero("Ana", "Software"),
Gerente("Luis"),
Ingeniero("María", "Civil")
)
for (empleado in empleados) {
empleado.trabajar()
}
}
Al ejecutar este código, obtenemos:
Ana está trabajando en el área de Software.
Luis está gestionando el equipo.
María está trabajando en el área de Civil.
Como observamos, la llamada al método trabajar()
en cada iteración invoca la implementación correspondiente a la clase derivada del objeto, a pesar de que la variable empleado
es del tipo de la clase base Empleado
. Este es el poder del polimorfismo: permitir que el comportamiento sea determinado en tiempo de ejecución según el tipo real del objeto.
Además, podemos utilizar el operador is
para verificar el tipo de un objeto en tiempo de ejecución y realizar acciones específicas según su clase derivada:
for (empleado in empleados) {
if (empleado is Ingeniero) {
println("${empleado.nombre} es un ingeniero especializado en ${empleado.especialidad}.")
} else if (empleado is Gerente) {
println("${empleado.nombre} es un gerente.")
}
}
El resultado será:
Ana es un ingeniero especializado en Software.
Luis es un gerente.
María es un ingeniero especializado en Civil.
El uso de is
permite implementar comportamientos específicos basados en el tipo concreto del objeto, manteniendo al mismo tiempo la flexibilidad del polimorfismo. Además, Kotlin realiza smart casting, lo que significa que, tras comprobar el tipo con is
, la variable se autocastéa al tipo comprobado dentro del bloque correspondiente, permitiendo acceder a propiedades y métodos específicos sin necesidad de casting adicional.
El polimorfismo es especialmente útil en estructuras de datos y algoritmos que manejan colecciones de objetos de una jerarquía de clases. Por ejemplo, al procesar una lista de figuras geométricas:
open class Figura {
open fun dibujar() {
println("Dibujando una figura.")
}
}
class Circulo : Figura() {
override fun dibujar() {
println("Dibujando un círculo.")
}
}
class Rectangulo : Figura() {
override fun dibujar() {
println("Dibujando un rectángulo.")
}
}
fun main() {
val figuras: List<Figura> = listOf(Circulo(), Rectangulo(), Circulo())
for (figura in figuras) {
figura.dibujar()
}
}
La salida será:
Dibujando un círculo.
Dibujando un rectángulo.
Dibujando un círculo.
Gracias al polimorfismo, el código es más genérico y no necesita conocer los detalles de cada clase derivada para funcionar correctamente. Esto promueve el principio de sustitución de Liskov, que establece que los objetos de una clase base deben poder ser reemplazados por objetos de sus clases derivadas sin alterar el correcto funcionamiento del programa.
Finalmente, es importante diseñar nuestras clases teniendo en cuenta el polimorfismo para aprovechar al máximo sus beneficios. Esto implica definir métodos como open
cuando anticipamos que las clases derivadas necesitarán proporcionar implementaciones específicas y utilizar referencias de la clase base para manipular objetos de distintas clases derivadas de manera uniforme.
Sobreescritura de métodos
La sobreescritura de métodos en Kotlin es una característica esencial que permite a las clases derivadas proporcionar una implementación específica de un método que ya está definido en su clase base. Esta capacidad es fundamental para aprovechar el polimorfismo y adaptar el comportamiento de las clases heredadas según las necesidades concretas.
En Kotlin, para sobreescribir un método, dicho método debe estar declarado como open
en la clase base. La clase derivada utiliza la palabra clave override
para indicar que está proporcionando una nueva implementación del método. Es importante destacar que el modificador override
es obligatorio y mejora la legibilidad y seguridad del código.
Por ejemplo, consideremos una clase base Vehiculo
con un método arrancar()
:
open class Vehiculo {
open fun arrancar() {
println("El vehículo está arrancando.")
}
}
Ahora, si creamos una clase derivada Coche
que hereda de Vehiculo
y necesitamos que arrancar()
tenga un comportamiento específico, procedemos a sobreescribir el método:
class Coche : Vehiculo() {
override fun arrancar() {
println("El coche está encendiendo el motor.")
}
}
Al crear una instancia de Coche
y llamar a arrancar()
, se ejecutará la implementación definida en la clase Coche
:
val miCoche = Coche()
miCoche.arrancar() // Salida: El coche está encendiendo el motor.
Es posible que deseemos utilizar parte de la lógica de la clase base y extenderla en la clase derivada. Para ello, podemos invocar el método de la clase base utilizando la palabra clave super
:
class Coche : Vehiculo() {
override fun arrancar() {
super.arrancar()
println("El coche está encendiendo el motor.")
}
}
En este caso, la salida sería:
El vehículo está arrancando.
El coche está encendiendo el motor.
De esta manera, combinamos el comportamiento del método en la clase base con el de la clase derivada, lo cual es útil cuando queremos conservar la funcionalidad original y añadir nuevas características.
La sobreescritura también se aplica a las propiedades. Si una propiedad está declarada como open
en la clase base, puede ser sobreescrita en la clase derivada utilizando override
. Por ejemplo:
open class Persona {
open val profesion: String = "Desempleado"
}
class Doctor : Persona() {
override val profesion: String = "Médico"
}
Al acceder a la propiedad profesion
de una instancia de Doctor
, obtenemos el valor sobreescrito:
val doctor = Doctor()
println(doctor.profesion) // Salida: Médico
Es importante tener en cuenta que el tipo de la propiedad sobreescrita debe ser compatible con el de la propiedad original. Además, si la propiedad en la clase base es una var
, la propiedad sobreescrita puede ser una val
, pero no al revés.
En situaciones donde una clase derivada necesita sobreescribir un método que ya ha sido sobreescrito en una clase intermedia, se continúa utilizando override
sin necesidad de declarar el método como open
nuevamente, ya que este permanece abierto a menos que se declare como final
. Por ejemplo:
open class Instrumento {
open fun afinar() {
println("Afinando el instrumento.")
}
}
open class Cuerda : Instrumento() {
override fun afinar() {
println("Afinando el instrumento de cuerda.")
}
}
class Violín : Cuerda() {
override fun afinar() {
println("Afinando el violín.")
}
}
En este caso, Violín
sobreescribe el método afinar()
que ya fue sobreescrito en Cuerda
. La capacidad de seguir sobreescribiendo métodos en la cadena de herencia es esencial para refinar el comportamiento en cada nivel de especialización.
Si deseamos impedir que un método sea sobreescrito en clases derivadas, podemos marcarlo como final
:
open class Cuerda : Instrumento() {
final override fun afinar() {
println("Afinando el instrumento de cuerda.")
}
}
class Viola : Cuerda() {
// Error de compilación: 'afinar' no puede ser sobreescrito porque está marcado como 'final' en 'Cuerda'
override fun afinar() {
println("Afinando la viola.")
}
}
La palabra clave final
ayuda a mantener la integridad del comportamiento definido en una clase específica, evitando modificaciones en niveles inferiores de la jerarquía.
Otra consideración relevante es la compatibilidad de visibilidad. Al sobreescribir un método o propiedad, no es posible reducir su visibilidad. Por ejemplo, si un método en la clase base es public
, el método sobreescrito no puede ser protected
o private
.
Además, en Kotlin, es posible sobreescribir métodos y propiedades que provienen de interfaces implementadas por la clase base o por la propia clase derivada. En caso de que exista ambigüedad debido a múltiples implementaciones, se debe especificar claramente qué implementación se está sobreescribiendo:
interface Movil {
fun desplazarse() {
println("El objeto se está moviendo.")
}
}
open class Vehiculo : Movil {
override fun desplazarse() {
println("El vehículo se está desplazando.")
}
}
class Bicicleta : Vehiculo(), Movil {
override fun desplazarse() {
super<Vehiculo>.desplazarse()
println("La bicicleta avanza pedaleando.")
}
}
En este ejemplo, Bicicleta
hereda de Vehiculo
y también implementa Movil
. Al sobreescribir desplazarse()
, utilizamos super<Vehiculo>
para indicar explícitamente qué implementación de desplazarse()
estamos invocando.
La sobreescritura de métodos es una herramienta fundamental que permite personalizar y extender el comportamiento heredado, promoviendo la reutilización de código y la flexibilidad en el diseño de clases. Es esencial comprender cómo y cuándo sobreescribir métodos para construir aplicaciones robustas y mantenibles en Kotlin.
Ejercicios de esta lección Herencia y polimorfismo
Evalúa tus conocimientos de esta lección Herencia y polimorfismo con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Clases genéricas con varianza y restricciones
Introducción a las corutinas
Uso de asincronía con suspend, async y await
Formateo de cadenas texto
Uso de monads y manejo funcional de errores
Declaración y uso de variables y constantes
Uso de la concurrencia funcional con corutinas
Operaciones en colecciones
Uso de clases y objetos en Kotlin
Evaluación Kotlin
Funciones de orden superior y expresiones lambda en Kotlin
Herencia y polimorfismo en Kotlin
Inmutabilidad y datos inmutables
Uso de funciones parciales y currificaciones
Primer programa en Kotlin
Introducción a la programación funcional
Introducción a Kotlin
Uso de operadores y expresiones
Sistema de inventario de tienda
Uso de data classes y destructuring
Composición de funciones
Uso de interfaces y clases abstractas
Simulador de conversión de monedas
Programación funcional y concurrencia
Creación y uso de listas, conjuntos y mapas
Transformación en monads y functors
Crear e invocar funciones
Uso de las estructuras de control
Todas las lecciones de Kotlin
Accede a todas las lecciones de Kotlin y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Kotlin
Introducción Y Entorno
Instalación Y Primer Programa De Kotlin
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Cadenas De Texto Y Manipulación
Sintaxis
Estructuras De Control
Sintaxis
Funciones Y Llamada De Funciones
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Herencia Y Polimorfismo
Programación Orientada A Objetos
Interfaces Y Clases Abstractas
Programación Orientada A Objetos
Data Classes Y Destructuring
Programación Orientada A Objetos
Tipos Genéricos Y Varianza
Programación Orientada A Objetos
Listas, Conjuntos Y Mapas
Estructuras De Datos
Introducción A La Programación Funcional
Programación Funcional
Funciones De Primera Clase Y De Orden Superior
Programación Funcional
Inmutabilidad Y Datos Inmutables
Programación Funcional
Composición De Funciones
Programación Funcional
Monads Y Manejo Funcional De Errores
Programación Funcional
Operaciones Funcionales En Colecciones
Programación Funcional
Transformaciones En Monads Y Functors
Programación Funcional
Funciones Parciales Y Currificación
Programación Funcional
Introducción A Las Corutinas
Coroutines Y Asincronía
Asincronía Con Suspend, Async Y Await
Coroutines Y Asincronía
Concurrencia Funcional
Coroutines Y Asincronía
Evaluación
Evaluación
Certificados de superación de Kotlin
Supera todos los ejercicios de programación del curso de Kotlin y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender el concepto de herencia en Kotlin y su implementación.
- Aprender a crear clases heredables utilizando
open
. - Utilizar el polimorfismo para manipular objetos de clases derivadas.
- Sobreescribir métodos y propiedades en clases derivadas.
- Aplicar el uso de
super
para invocar implementaciones de clases base. - Diferenciar entre clases
final
yopen
para controlar la extensión de clases. - Implementar interfaces y gestionar ambigüedades en herencia múltiple.