Algoritmos y Estructuras de Datos Herramientas Lenguaje de programación
!Prog C/C++ Rust
Linux Matemáticas
Mates Discretas
Programación Orientada a Objetos Sistemas Operativos

Herencia y Composición. Polimorfismo

[date: 15-01-2024 17:01] [last modification: 15-01-2024 22:24]
[words: 2394] [reading time: 12min] [size: 38770 bytes]

En este artículo se tratan los aspectos de la Herencia y el Polimorfimo en Java. Se verá el concepto de Clase Abstracta e Interfaz.

Herencia

Mecanismo en el que la clase derivada reutiliza los atributos y métodos de la clase padre. Es el mecanismo más básico de reutilización de código, que es un tema muy importante en programación.

Este mecanismo sale de la naturaleza y ayuda a clasificar y a montar jerarquías. La clase derivada se considera una extensión de la clase base, un tipo o una especialización.

Beneficios
  • Se puede implementar nuevos métodos que expandan la funcionalidad de la clase base o sobreescribir los existentes para cambiar su comportamiento.
  • Simplifica el código a la hora de implementar el mismo método en varios sitios.
  • Facilita el mantenimiento: si se corrige un error en la clase padre, todas las clases derivadas reciben el cambio.
  • Facilita la extensibilidad: nuevas clases se construyen a partir de otras, reutilizando el código ya existente.
Desventajas
  • Niveles de jerarquía profundos: el código se hace mucho más difícil de entender, las relaciones entre las clases y qué métodos tienen cada una.
  • Cambios en la clase padre hacen inconsistentes a las clases hijas.
  • Modificador de acceso diferente para que las clases hijas puedan acceder ==> Violación de la encapsulación.

Tipos de herencia

    flowchart BT
    1[Herencia Simple]
    A1(Clase A)
    B1(Clase B)

    2[Herencia de Varios Niveles]
    A2(Clase A)
    B2(Clase B)
    C2(Clase C)

    3[Herencia Jerárquica]
    A3(Clase A)
    B3(Clase B)
    C3(Clase C)

    4[Herencia Múltiple]
    A4(Clase A)
    B4(Clase B)
    C4(Clase C)

    B1:::a == es un/a ==> A1:::a
    A1 ~~~ 1:::t

    C2:::a ==> B2:::a
    B2:::a ==> A2:::a
    A2 ~~~ 2:::t

    B3:::a ==> A3:::a
    C3:::a ==> A3:::a
    A3 ~~~ 3:::t

    C4:::a ==> A4:::a
    C4:::a ==> B4:::a
    A4 ~~~ 4:::t
    B4 ~~~ 4

    classDef t stroke: none, fill: #222
    classDef a stroke: #0f5, stroke-width: 3px

En Java no hay herencia múltiple.

Composición

Mecanismo donde una clase contiene objetos de otras clases a las que delega ciertas operaciones.

Beneficios
  • Reparte responsabilidades entre diferentes objetos
  • Facilita el mantenimiento de los programas, incluso más que en la herencia, dado que un cambio solo afecta al objeto actual.
  • Facilita la extensibilidad
Desventajas
  • Genera más código y puede llegar a ser bastante complejo para soportar la misma funcionalidad que en herencia.
  • Se necesita más tiempo de desarrollo.

Composición vs Herencia

Hay una cierta diferencia semántica entre ambas opciones, y puede depender del contexto de cada programa:

    flowchart BT
    HA(A)
    HB(B)
    CA(A)
    CB(B)

    HB ==> HA
    CB ==o CA
Composición > Herencia
  • Si las clases no están relacionadas de forma lógica
  • Si una clase base tiene una sola clase derivada
  • Si las clases derivadas heredan mucho código que no necesitan
  • Si es necesario sobreescribir muchos métodos de la clase padre
  • Existe la posibilidad de que la clase base cambie
CasoHerenciaComposición
Inicio del desarrolloMás rápidoMás lento
Diseño del softwareMás sencilloMás complejo
Efectos no deseadosCon frecuencia
(jerarquías profundas)
Reducidos
(métodos delegados)
Aceptación a cambiosComplicado con jerarquías profundas y @OverridesSencillo dado que solo afecta a clases delegadas
ValidaciónComplicado con jerarquías profundas y @OverridesSencillo dado que solo afecta a clases delegadas
ExtensibilidadSencillo aunque problemático para jerarquías profundasSencillo

Herencia en Java

1public class A {
2    // ...
3}
4
5public class B extends A {
6    // ...
7}

En Java no existe la herencia múltiple. Solo se heredan los atributos y métodos que son visibles desde la clase derivada.

Tipo de acceso¿Hereda?
privateNunca
Solo si ambas clases están en el mismo paquete
protectedSiempre
publicSiempre
Importante

Realmente los métodos y atributos se heredan todos, sean del tipo que sean y tengan el modificador de acceso que sea. Porque de lo contrario, ¿cómo se pueden ejecutar los métodos públicos que llaman a los métodos privados?

Lo único es que su visibilidad está restringida.

Sin embargo, dado que el programador no puede utilizarlos, vamos a considerar como si no se heredasen.

Constructores

Los constructores no se heredan (ni con public), son propios de la clase.

Entonces, ¿cómo se asigna memoria a los atributos de la superclase? Se pueden distinguir varios casos:

super también permite acceder a los métodos o atributos visibles de la clase superior. Nótese que super solo hace referencia a la clase inmediatamente superior:

    flowchart BT
    A(Clase A)
    B(Clase B)
    C(Clase C)

    C ===> B
    B ===> A

    C -- super --> B
    B -- super --> A

Sobreescritura de métodos

La sobreescritura de métodos es un mecanismo que permite al programador de la clase derivada cambiar la implementación de un método de la clase base.

Esto se hace cuando el método de la clase base no es válido, ya hemos visto un par de ejemplos en métodos especiales (toString, equals, hashCode…).

El nombre del método, el tipo de datos devuelto y sus parámetros deben ser iguales al original; y el tipo de acceso puede ser igual o superior (menos restrictivo).

1@Override    // Etiqueta para indicar que se sobreescribe
2public String toString() {
3    return "HOLA";
4}

Como se comentó anteriormente, una forma de reutilizar el código, es utilizando super.

1@Override
2public int sueldo() {
3    return (int) (1.2 * super.sueldo());
4}

Modificador final

La palabra clave final se utiliza para indicar que no puede cambiar.

Nótese que para crear constantes, se necesitan los modificadores static final en los atributos. Sin embargo, no tiene mucho sentido combinar añadir ambos a un método: los métodos estáticos no se pueden sobreescribir.

Clases abstractas

Clases especiales declaradas con abstract que no pueden tener instancias, es decir, no se pueden crear objetos con new.

Pueden tener métodos, atributos y constructores. Estos se usan a través de super, por lo que solo son útiles cuando forman parte de una jerarquía, habitualmente los niveles altos. Entonces, tenemos que:

    flowchart BT
    A(Clase abstracta)
    C(Clase)
    F(Clase final)

    C ==> A
    F ==> C

Se pueden usar para modelar diferentes tipos de jerarquías:

Métodos abstractos

1public abstract class Figura {
2    public abstract float perimetro();
3    public abstract float area();
4}

Otra utilidad de las clases abstractas es que pueden tener métodos de los que se desconoce su implementación, métodos abstractos.

Todas las clases derivadas no abstractas, deben sobreescribir este método para darle una implementación. Este mecanismo nos permite especificar qué métodos debería tener cada clase.

Buenas prácticas
  • Debería tener el máximo de métodos implementados posibles.
    Mayor reutilización del código y las clases derivadas tienen que implementar menos cosas.
  • Debería ocupar los niveles más altos de la jerarquía de clases.
  • Su clase base debería ser también abstracta.
  • Debería tener constructores.

Interfaces

Interfaz

Permite establecer de forma muy precisa los requisitos que deben cumplir las clases de un programa. Esto es muy útil para el diseño de programas.

  • Definen los tipos de datos mínimos
  • Definen exactamente los métodos: nombre, parámetros y valor de retorno.
  • Cada clase que implemente una interfaz puede tener más métodos, pero como mínimo debe tener los de la interfaz.

Entonces, una interfaz se puede ver como un acuerdo/estándar/plantilla entre programadores.

Un cambio en la clase interna no afectará tanto a otras clases como un cambio en la propia interfaz ==> Las interfaces deben ser invariantes.

Interfaces en Java

 1public interface Interfaz {
 2    void metodo();
 3}
 4
 5public class Clase implements Interfaz, ... {
 6    // ...
 7
 8    @Override
 9    public void metodo() {
10        System.out.println("Hola");
11    }
12}

Contienen métodos abstractos públicos. No tiene sentido declararlos como final o protected dado que las clases que implementen la interfaz deben sobreescribir los métodos (salvo clases abstractas, que pueden tener métodos abstractos).

En caso de que no se especifiquen modificadores, por defecto serán abstractos para los métodos y static final para los atributos, todos ellos públicos.

Las interfaces tienen herencia múltiple con otras interfaces (que heredarán todo menos los métodos estáticos), pero no pueden implementar otras interfaces.

Importante

Cuando una clase implementa una interfaz, todo funciona «como si» fuese herencia y la interfaz fuese una clase padre.

Dado que una clase puede implementar más de una interfaz, se puede decir que existe la herencia múltiple en Java.

También se pueden aplicar conceptos de herencia a las interfaces.

Clases AbstractasInterfaces
Se definen cuando las clases derivadas tienen métodos comunesSe definen cuando existen diferentes implementaciones
No hay herencia múltipleHay herencia múltiple entre interfaces
Hay herencia jerárquica entre clasesImplementar interfaces crea una jerarquía
Pueden tener métodos abstractos y concretosSolo métodos abstractos y métodos por defecto
Los métodos pueden ser public o protectedSolo public o private
Tienen constructoresNo tiene constructores
Pueden tener cualquier tipo de atributoSolo static final

Métodos por defecto

El problema de las interfaces es la adaptación de las clases derivadas cuando se modificar la interfaz. Los métodos por defecto intentar resolver eso.

No genera errores de compilación si no se implementa en la clase: se usa la implementación por defecto.

Importante

En la implementación por defecto, no se tiene acceso a los atributos locales, solo a los métodos definidos en la interfaz.

Por este limitación, hay que valorar si merece la pena hacer métodos por defecto.

Para llamar a un método por defecto cuando se sobreescribe y reutilizar su código, se usar la siguiente sintaxis:

<NombreInterfaz>.super.<método>(...);

Resumen de herencia

Conviene recordar que:

Tipo de acceso¿Hereda?
privateNunca
Solo si ambas clases están en el mismo paquete
protectedSiempre
publicSiempre

La siguiente tabla muestra si los diferentes tipos de métodos y atributos (suponiendo que se han declarado como public) se heredan de clases normales, abstractas o interfaces. Habrá dos ✅ en caso de que se pueda sobreescribir.

¿Se heredan de …?ClaseClase abstractaInterfaz
ConstructorNo tiene
Método normal✅ ✅✅ ✅No tiene
Método static
Método finalNo tieneNo tiene
Método abstractNo tiene✅ ✅ (sobreescritura obligatoria en algunos casos)
Método defaultNo tieneNo tiene✅ ✅
Atributo normalNo tiene
Atributo staticSolo si también es final
Atributo finalSolo si también es static

En el caso de las interfaces, recuerde que solo hay herencia entre interfaces. Porque en cuanto a la implementación de interfaces:

¿Se implementan?Interfaz
Método abstract✅ ✅
Método default✅ ✅
Método private
Método static final
Atributo static final

Polimorfismo

En herencia, el polimorfismo es el mecanismo mediante es el que un objeto se puede comportar de múltiples formas en función del contexto.

    flowchart LR
    A(Animal)
    M(Mascota)
    G(Gato)

    G ==> M
    M ==> A

    style A stroke: #0f5, stroke-width: 3px
    style M stroke: #0f5, stroke-width: 3px
    style G stroke: #0f5, stroke-width: 3px
   Animal bigotes = new Gato();
// ~~~~~~           ~~~~~~~~~~
// comportamiento    instancia

Ahora mismo, el Gato bigotes se comporta como Animal, solo tiene accesibles los métodos de la clase Animal, no todos los de Gato.

    flowchart BT
    C -- Upcasting --> A

    C(C) ==> B(B)
    B(B) ==> A(A)

    A -- Downcasting --> C

    style A stroke: #0f5, stroke-width: 3px
    style B stroke: #0f5, stroke-width: 3px
    style C stroke: #0f5, stroke-width: 3px
Upcasting y Downcasting
  • Upcasting: se convierte una instancia de una clase a alguna de sus superclases.
  • Downcasting: se convierte una instancia de una clase a alguna de sus subclases. Esta es una operación unsafe, dado que en caso de que no se pueda realizar la conversión, se lanza ClassCastException.
// Upcasting
B b = new C();

// Downcasting para recuperar la instancia
C c = (C) b;

En el caso de Upcasting, el objeto apuntado por b es realmente una instancia de C, dado que se usó su constructor. Se ha convertido a un objeto de tipo B, por lo que solo se tiene acceso a sus métodos. Sin embargo, en memoria no se ha cambiado nada, se sigue teniendo una instancia de C, por lo que podemos recuperarla haciendo Downcasting.

// Downcasting
B b = new B();
C c = (B) c;

En este caso, en memoria hay una instancia de B, por lo que al intentar convertirlo a C dará un error, naturalmente. De lo contrario, ¿qué sucederá cuando se intente llamar a un método propio de C, si dicho método no existe?

Debido a esto, existe un mecanismo para comprobar de qué tipo es cada clase realmente: getClass().getName() devuelve un String con el nombre completo de la clase, incluyendo el paquete (getClass().getSimpleName() devuelve solo el nombre). Luego se puede hacer una comparación de Strings y listo.

En otros casos, se quiere comprobar si es una subclase de otra:

B b = new B();

b instanceof B // true
b instanceof A // true
c instanceof C // false
Anterior: Conjuntos de Datos Volver a Programación Orientada a Objetos