Kotlin

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ícate

Concepto 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.

Aprende Kotlin GRATIS online

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.

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

Kotlin

Introducción Y Entorno

Instalación Y Primer Programa De Kotlin

Kotlin

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

Kotlin

Sintaxis

Operadores Y Expresiones

Kotlin

Sintaxis

Cadenas De Texto Y Manipulación

Kotlin

Sintaxis

Estructuras De Control

Kotlin

Sintaxis

Funciones Y Llamada De Funciones

Kotlin

Sintaxis

Clases Y Objetos

Kotlin

Programación Orientada A Objetos

Herencia Y Polimorfismo

Kotlin

Programación Orientada A Objetos

Interfaces Y Clases Abstractas

Kotlin

Programación Orientada A Objetos

Data Classes Y Destructuring

Kotlin

Programación Orientada A Objetos

Tipos Genéricos Y Varianza

Kotlin

Programación Orientada A Objetos

Listas, Conjuntos Y Mapas

Kotlin

Estructuras De Datos

Introducción A La Programación Funcional

Kotlin

Programación Funcional

Funciones De Primera Clase Y De Orden Superior

Kotlin

Programación Funcional

Inmutabilidad Y Datos Inmutables

Kotlin

Programación Funcional

Composición De Funciones

Kotlin

Programación Funcional

Monads Y Manejo Funcional De Errores

Kotlin

Programación Funcional

Operaciones Funcionales En Colecciones

Kotlin

Programación Funcional

Transformaciones En Monads Y Functors

Kotlin

Programación Funcional

Funciones Parciales Y Currificación

Kotlin

Programación Funcional

Introducción A Las Corutinas

Kotlin

Coroutines Y Asincronía

Asincronía Con Suspend, Async Y Await

Kotlin

Coroutines Y Asincronía

Concurrencia Funcional

Kotlin

Coroutines Y Asincronía

Evaluación

Kotlin

Evaluación

Accede GRATIS a Kotlin y certifícate

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

  1. Comprender el concepto de herencia en Kotlin y su implementación.
  2. Aprender a crear clases heredables utilizando open.
  3. Utilizar el polimorfismo para manipular objetos de clases derivadas.
  4. Sobreescribir métodos y propiedades en clases derivadas.
  5. Aplicar el uso de super para invocar implementaciones de clases base.
  6. Diferenciar entre clases final y open para controlar la extensión de clases.
  7. Implementar interfaces y gestionar ambigüedades en herencia múltiple.