CodeGym /Cursos /JAVA 25 SELF /Problemas de polimorfismo y abstracciones

Problemas de polimorfismo y abstracciones

JAVA 25 SELF
Nivel 23 , Lección 3
Disponible

1. Polimorfismo: qué es y para qué sirve

Si crees que el polimorfismo es algo del mundo de los mutantes de Marvel, siento decepcionarte: en programación todo es mucho más tranquilo, pero no menos mágico. El polimorfismo es la capacidad de objetos con implementaciones distintas de responder de forma diferente a las mismas llamadas de método.

Ejemplo de la vida real:
Tienes la clase Book y la clase Magazine, ambas heredan de la clase abstracta LibraryItem. Quieres que para cualquier elemento de la biblioteca se pueda invocar el método printInfo() y que muestre la información adecuada: para un libro, autor y título; para una revista, número de edición.

Ejemplo de código:

abstract class LibraryItem {
    String title;

    LibraryItem(String title) {
        this.title = title;
    }

    abstract void printInfo();
}

class Book extends LibraryItem {
    String author;

    Book(String title, String author) {
        super(title);
        this.author = author;
    }

    @Override
    void printInfo() {
        System.out.println("Libro: " + title + ", autor: " + author);
    }
}

class Magazine extends LibraryItem {
    int issueNumber;

    Magazine(String title, int issueNumber) {
        super(title);
        this.issueNumber = issueNumber;
    }

    @Override
    void printInfo() {
        System.out.println("Revista: " + title + ", número: " + issueNumber);
    }
}

Ahora puedes crear un array de distintos elementos e invocar printInfo() para cada uno:

LibraryItem[] items = {
    new Book("El señor de las moscas", "William Golding"),
    new Magazine("Ciencia y vida", 5)
};

for (LibraryItem item : items) {
    item.printInfo();
}
// Mostrará:
// Libro: El señor de las moscas, autor: William Golding
// Revista: Ciencia y vida, número: 5

¡Así funciona el polimorfismo!

2. Errores típicos con el polimorfismo

Intentar llamar a métodos que no existen en el tipo base

Uno de los errores más frecuentes es intentar acceder a un método que solo está declarado en la clase hija a través de una referencia del tipo base.

LibraryItem item = new Book("Harry Potter", "J. K. Rowling");
// item.getAuthor(); // Error de compilación: LibraryItem no tiene el método getAuthor()

Java compila el código basándose en lo que ve en el tipo de la variable (LibraryItem), no en el objeto real (Book). Por eso, si necesitas llamar a un método específico de libro, debes hacer un cast:

if (item instanceof Book) {
    Book book = (Book) item;
    // Ahora se puede llamar a book.getAuthor()
}

Conversión de tipos sin comprobación

Si estás seguro de que el objeto es un Book, pero en realidad no lo es, obtendrás un ClassCastException en tiempo de ejecución. Por ejemplo:

LibraryItem item = new Magazine("Forbes", 12);
Book book = (Book) item; // ¡BOOM! ClassCastException

La forma correcta es comprobar siempre el tipo:

if (item instanceof Book) {
    Book book = (Book) item;
    // OK
} else {
    System.out.println("¡No es un libro!");
}

No aprovechar las ventajas del polimorfismo

A veces los desarrolladores escriben el código de tal manera que queda fuertemente acoplado a tipos concretos, cuando podrían usar abstracciones. Por ejemplo, si escribes:

Book[] books = ...;
for (Book book : books) {
    book.printInfo();
}

Esto solo funciona para libros. ¿Y si mañana aparecen revistas, periódicos, cómics? Es mejor usar un array LibraryItem[] y trabajar con los métodos de la clase base o de la interfaz.

3. Abstracciones: para qué sirven y cómo no estropearlas

Clases abstractas e interfaces

La abstracción es el arte de destacar lo esencial y ocultar los detalles. En Java, para ello existen las clases abstractas y las interfaces.

  • Clase abstracta: es una clase que no puede instanciarse directamente, solo heredarse.
  • Interfaz: es un contrato: qué debe poder hacer una clase, pero no cómo lo hace.

Error 1: crear una clase abstracta sin métodos abstractos

Si tu clase abstracta no tiene ningún método abstracto, plantéate si realmente debe ser abstracta. ¿No será más sencillo hacer una clase normal?

abstract class UselessAbstract {
    void sayHello() {
        System.out.println("Hello!");
    }
}
// Mejor hacer una clase normal si no hay métodos abstractos

Error 2: ausencia de implementación de métodos obligatorios en las subclases

Si una clase hereda de una clase abstracta o implementa una interfaz, está obligada a implementar todos los métodos abstractos. Si se te olvida, el compilador te lo recordará, pero a veces los métodos se implementan «por cumplir» y no hacen nada. Eso dificulta el mantenimiento del código.

class Magazine extends LibraryItem {
    Magazine(String title, int issueNumber) {
        super(title);
        // ...
    }

    @Override
    void printInfo() {
        // ¡Vacío! ¡Mal!
    }
}

Error 3: jerarquía de abstracciones demasiado profunda o enrevesada

Cuando las clases heredan unas de otras en cinco–diez niveles, entenderlas se vuelve muy complicado. Es mejor crear jerarquías «planas», donde todo sea claro.

Mal ejemplo:

LibraryItem
  |
  BookItem
    |
    PrintedBook
      |
      IllustratedBook
        |
        ChildrenIllustratedBook

Complicado, ¿verdad? Mejor limitarse a dos o tres niveles.

4. Práctica: aplicación del polimorfismo y las abstracciones en una aplicación didáctica

Vamos a mejorar tu aplicación educativa para la biblioteca. Antes solo tenías libros. Ahora añadiremos revistas y unificaremos un comportamiento común para las publicaciones impresas.

Declaremos una clase abstracta:

abstract class LibraryItem {
    protected String title;

    public LibraryItem(String title) {
        this.title = title;
    }

    public abstract void printInfo();
}

Añadamos clases hijas:

class Book extends LibraryItem {
    private String author;

    public Book(String title, String author) {
        super(title);
        this.author = author;
    }

    @Override
    public void printInfo() {
        System.out.println("Libro: " + title + ", autor: " + author);
    }
}

class Magazine extends LibraryItem {
    private int issueNumber;

    public Magazine(String title, int issueNumber) {
        super(title);
        this.issueNumber = issueNumber;
    }

    @Override
    public void printInfo() {
        System.out.println("Revista: " + title + ", número: " + issueNumber);
    }
}

Usemos el polimorfismo:

LibraryItem[] items = {
    new Book("Código limpio", "Robert Martin"),
    new Magazine("Java World", 3)
};

for (LibraryItem item : items) {
    item.printInfo();
}

Añadamos una interfaz para publicaciones electrónicas

Supongamos que algunas publicaciones se pueden leer en línea. Introduzcamos una interfaz:

interface ReadableOnline {
    void openOnline();
}

class EBook extends Book implements ReadableOnline {
    private String url;

    public EBook(String title, String author, String url) {
        super(title, author);
        this.url = url;
    }

    @Override
    public void openOnline() {
        System.out.println("Abrimos el libro electrónico en la dirección: " + url);
    }
}

Ahora se puede trabajar con libros electrónicos a través de la interfaz:

ReadableOnline ebook = new EBook("Java para Dummies", "Barry Burd", "https://example.com/java");
ebook.openOnline();

5. Cómo evitar problemas con el polimorfismo y las abstracciones: mejores prácticas

  • Utiliza interfaces y clases abstractas para describir el comportamiento, no el estado.
    Por ejemplo, la interfaz Printable describe bien la capacidad de «imprimir», pero guardar en una interfaz un campo String title ya es una mala idea.
  • Comprueba el tipo del objeto con instanceof antes de hacer el cast.
    Especialmente si el objeto puede ser de distintos tipos. Esto te evitará un ClassCastException.
  • Aspira a jerarquías «planas» y comprensibles.
    Cuanto más simple sea el árbol de herencia, más fácil será mantener y ampliar el código.
  • Evita crear abstracciones «sin sentido».
    Si la clase no contiene métodos abstractos y no está pensada para ser heredada, no la hagas abstracta.
  • Usa la anotación @Override siempre que sobrescribas un método.
    Ayuda al compilador a detectar errores en la firma.

6. Errores típicos al trabajar con polimorfismo y abstracciones

Error n.º 1: conversión de tipo sin comprobación

A veces apetece «atajar» y hacer el cast sin comprobar. Puede que funcione, o puede provocar una caída inesperada del programa. Usa siempre instanceof:

if (item instanceof Book) {
    Book book = (Book) item;
    // ...
}

Error n.º 2: intentar llamar a un método de la subclase mediante una referencia del tipo base

LibraryItem item = new Book("Java", "Autor");
item.getAuthor(); // Error de compilación: en LibraryItem no existe ese método

La solución es o bien hacer el cast, o bien añadir el método necesario a la clase base (si tiene sentido).

Error n.º 3: implementación incompleta de una interfaz o clase abstracta

Si olvidas implementar todos los métodos de la interfaz, el compilador no permitirá compilar el proyecto. Pero si implementas «stubs» que no hacen nada, eso puede llevar a comportamientos inesperados.

Error n.º 4: jerarquía de herencia demasiado profunda

Si tienes más de tres niveles de herencia, plantéate si no se puede simplificar la arquitectura.

Error n.º 5: violación del principio de responsabilidad única

Si una abstracción describe demasiadas responsabilidades, se vuelve difícil de mantener. Es mejor dividirla en varias interfaces o clases.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION