Saltar al contenido
ES EN

Ejercicio resuelto: polimorfismo y enlace dinámico en Java

Ejercicio de examen resuelto en Java: colección de figuras con clase abstracta, métodos sobrescritos y enlace dinámico, con pistas, código probado y errores.

Actualizado el

Código de Examen Java Programación orientada a objetos Nivel examen Código probado

Código de Examen: Ejercicio resuelto: polimorfismo y enlace dinámico en Java
Programación II · Examen parcialEjercicio 2 · 2,5 puntos · 30 min

Se quiere gestionar una colección de figuras geométricas planas. Debes modelarla con polimorfismo: una clase abstracta Figura declara los métodos area(), nombre() y medidas(), y define toString() una sola vez, apoyándose en esos tres métodos. Las subclases concretas son Circulo (radio), Rectangulo (base y altura), Cuadrado (lado), que debe heredar de Rectangulo, y Triangulo (base y altura).

Una clase Figuras almacena las figuras en el orden en que se añaden, con posiciones numeradas desde 0. Ofrece los métodos annade(Figura f), borra(int pos) (devuelve false si la posición no existe), lista(...), areaTotal(), tamano() y mayor(). Ninguno de ellos puede usar instanceof ni preguntar por el tipo concreto de cada figura.

El programa Main lee órdenes de la entrada estándar, una por línea, hasta el final de la entrada. Las líneas en blanco se ignoran. Las órdenes son: C r añade un círculo; R b h añade un rectángulo; Q l añade un cuadrado; T b h añade un triángulo; B i borra la figura de la posición i; L lista la colección; S muestra el resumen; M muestra la figura de mayor área. Las medidas son números reales positivos escritos con punto decimal y las posiciones son enteros, que pueden ser negativos.

Formato de salida. Todos los reales se escriben con dos decimales y punto decimal. Cada figura se describe como Nombre(medidas) area=A, donde las medidas son r=... para el círculo, l=... para el cuadrado y b=... h=... para rectángulo y triángulo; los nombres son Circulo, Rectangulo, Cuadrado y Triangulo, sin tildes. La orden L imprime una línea i: descripcion por figura, o la línea (vacia) si no hay ninguna.

La orden S imprime Total: n figuras, area X. La orden M imprime Mayor: descripcion; en caso de empate gana la que ocupa la posición más baja, y si la colección está vacía imprime Mayor: ninguna. Un borrado válido no imprime nada; uno inválido imprime ERROR posicion i con el valor leído. Las órdenes de añadir no producen salida. Si la entrada está vacía, el programa no imprime nada.

Entrada de ejemplo

C 1
R 2 3
Q 1.5
T 4 2.5
L
B 1
B 7
L
S
M

Salida esperada

0: Circulo(r=1.00) area=3.14
1: Rectangulo(b=2.00 h=3.00) area=6.00
2: Cuadrado(l=1.50) area=2.25
3: Triangulo(b=4.00 h=2.50) area=5.00
ERROR posicion 7
0: Circulo(r=1.00) area=3.14
1: Cuadrado(l=1.50) area=2.25
2: Triangulo(b=4.00 h=2.50) area=5.00
Total: 3 figuras, area 10.39
Mayor: Triangulo(b=4.00 h=2.50) area=5.00
Jerarquía correcta: clase abstracta Figura con métodos abstractos y subclases Circulo, Rectangulo, Cuadrado (hija de Rectangulo) y Triangulo
0,75
Sobrescritura bien usada: toString definido una sola vez en Figura y apoyado en métodos sobrescritos; Cuadrado solo redefine lo que cambia
0,75
Clase Figuras: añadir, borrar por posición con validación de límites, listar, área total y figura de mayor área sin consultar el tipo concreto
0,5
Lectura de órdenes hasta fin de entrada y salida con el formato exacto (dos decimales con punto, mensajes de error)
0,5

Pistas

Pista 1 · ¿Dónde va el formato de la descripción?

Todas las descripciones tienen la misma forma: nombre, medidas entre paréntesis y área. Esa parte común debe estar escrita en un único sitio, el toString() de Figura. Lo que varía entre figuras (el nombre, las medidas y el cálculo del área) son los métodos abstractos que cada subclase sobrescribe. Así, cuando toString() llama a area(), se ejecuta la versión de la clase real del objeto.

Pista 2 · ¿Qué tiene que redefinir Cuadrado?

Si Cuadrado hereda de Rectangulo y su constructor llama a super(lado, lado), el área ya es correcta sin tocar nada. Solo cambian el nombre y la forma de mostrar las medidas, así que sobrescribe únicamente nombre() y medidas(). Para que pueda leer el lado, el campo base de Rectangulo debe ser protected.

Pista 3 · ¿Cómo se valida el borrado y se busca la mayor?

Una posición es válida si cumple 0 <= pos < tamaño. Compruébalo antes de llamar a remove, porque una posición negativa también es inválida. Para la mayor, recorre la lista guardando la mejor figura hasta el momento y sustitúyela solo si el área es estrictamente mayor: así, en caso de empate, se conserva la primera.

Solución

Solución explicada

La idea central es que Figuras trabaje siempre con referencias de tipo Figura y nunca necesite saber si tiene un círculo o un triángulo. El compilador comprueba que area() existe en Figura; la máquina virtual decide en tiempo de ejecución qué versión ejecutar según la clase real del objeto. Eso es el enlace dinámico, y es lo que permite que areaTotal() y mayor() sean bucles de pocas líneas.

Todas las clases van en un único fichero Main.java. Solo Main es pública; el resto tiene visibilidad de paquete, que en un examen es suficiente y evita tener varios ficheros.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

abstract class Figura {
    public abstract double area();

    public abstract String nombre();

    protected abstract String medidas();

    @Override
    public String toString() {
        // Plantilla común: las tres llamadas se resuelven por enlace dinámico
        return String.format(Locale.ROOT, "%s(%s) area=%.2f", nombre(), medidas(), area());
    }
}

class Circulo extends Figura {
    private final double radio;

    Circulo(double radio) {
        this.radio = radio;
    }

    @Override
    public double area() {
        return Math.PI * radio * radio;
    }

    @Override
    public String nombre() {
        return "Circulo";
    }

    @Override
    protected String medidas() {
        return String.format(Locale.ROOT, "r=%.2f", radio);
    }
}

class Rectangulo extends Figura {
    protected final double base;
    protected final double altura;

    Rectangulo(double base, double altura) {
        this.base = base;
        this.altura = altura;
    }

    @Override
    public double area() {
        return base * altura;
    }

    @Override
    public String nombre() {
        return "Rectangulo";
    }

    @Override
    protected String medidas() {
        return String.format(Locale.ROOT, "b=%.2f h=%.2f", base, altura);
    }
}

class Cuadrado extends Rectangulo {
    Cuadrado(double lado) {
        super(lado, lado);
    }

    // area() se hereda de Rectangulo: no hace falta redefinirla
    @Override
    public String nombre() {
        return "Cuadrado";
    }

    @Override
    protected String medidas() {
        return String.format(Locale.ROOT, "l=%.2f", base);
    }
}

class Triangulo extends Figura {
    private final double base;
    private final double altura;

    Triangulo(double base, double altura) {
        this.base = base;
        this.altura = altura;
    }

    @Override
    public double area() {
        return base * altura / 2.0;
    }

    @Override
    public String nombre() {
        return "Triangulo";
    }

    @Override
    protected String medidas() {
        return String.format(Locale.ROOT, "b=%.2f h=%.2f", base, altura);
    }
}

class Figuras {
    private final List<Figura> elementos = new ArrayList<>();

    void annade(Figura f) {
        elementos.add(f);
    }

    boolean borra(int pos) {
        if (pos < 0 || pos >= elementos.size()) {
            return false;
        }
        elementos.remove(pos);
        return true;
    }

    void lista(PrintWriter out) {
        if (elementos.isEmpty()) {
            out.println("(vacia)");
            return;
        }
        for (int i = 0; i < elementos.size(); i++) {
            out.println(i + ": " + elementos.get(i));
        }
    }

    double areaTotal() {
        double total = 0.0;
        for (Figura f : elementos) {
            total += f.area();
        }
        return total;
    }

    int tamano() {
        return elementos.size();
    }

    Figura mayor() {
        Figura mejor = null;
        for (Figura f : elementos) {
            if (mejor == null || f.area() > mejor.area()) {
                mejor = f;
            }
        }
        return mejor;
    }
}

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter out = new PrintWriter(System.out);
        Figuras figuras = new Figuras();
        String linea;
        while ((linea = in.readLine()) != null) {
            linea = linea.trim();
            if (linea.isEmpty()) {
                continue;
            }
            String[] p = linea.split("\\s+");
            switch (p[0]) {
                case "C" -> figuras.annade(new Circulo(Double.parseDouble(p[1])));
                case "R" -> figuras.annade(new Rectangulo(Double.parseDouble(p[1]), Double.parseDouble(p[2])));
                case "Q" -> figuras.annade(new Cuadrado(Double.parseDouble(p[1])));
                case "T" -> figuras.annade(new Triangulo(Double.parseDouble(p[1]), Double.parseDouble(p[2])));
                case "B" -> {
                    int pos = Integer.parseInt(p[1]);
                    if (!figuras.borra(pos)) {
                        out.println("ERROR posicion " + pos);
                    }
                }
                case "L" -> figuras.lista(out);
                case "S" -> out.println(String.format(Locale.ROOT, "Total: %d figuras, area %.2f",
                        figuras.tamano(), figuras.areaTotal()));
                case "M" -> {
                    Figura m = figuras.mayor();
                    out.println("Mayor: " + (m == null ? "ninguna" : m));
                }
                default -> {
                    // orden desconocida: se ignora
                }
            }
        }
        out.flush();
    }
}

La clase abstracta. Figura no se puede instanciar y obliga a cada subclase a implementar area(), nombre() y medidas(). Su toString() es un pequeño método plantilla: fija el formato Nombre(medidas) area=A y delega lo variable en las subclases. Se usa Locale.ROOT en todos los String.format para que el separador decimal sea siempre el punto. En un sistema configurado en español, %.2f escribiría una coma y la salida dejaría de coincidir.

Las subclases. Circulo, Rectangulo y Triangulo sobrescriben los tres métodos con la anotación @Override, que hace que el compilador avise si la firma no coincide. Cuadrado es el caso interesante: su constructor llama a super(lado, lado) y no redefine area(), porque la de Rectangulo ya calcula base * altura. Solo sobrescribe nombre() y medidas(), y para eso lee base, que por ese motivo es protected. Si quieres repasar cómo encadenar constructores, tienes el ejercicio sobre herencia, constructores y super.

Enlace dinámico en acción. En el ejemplo, la tercera figura es un Cuadrado de lado 1.5. Cuando lista concatena i + ": " + elementos.get(i), Java llama a toString(), que es el de Figura. Dentro de él, nombre() y medidas() se resuelven en Cuadrado, mientras que area() sube hasta Rectangulo. Por eso aparece 2: Cuadrado(l=1.50) area=2.25. El tipo estático de la referencia es Figura, pero lo que decide qué código se ejecuta es la clase del objeto.

La colección. Figuras guarda un List<Figura>. El método borra comprueba pos < 0 || pos >= elementos.size() antes de llamar a remove(pos). Sin esa comprobación saltaría una IndexOutOfBoundsException. El test «Un elemento y posiciones negativas» comprueba justamente B -1, y el ejemplo comprueba B 7 cuando solo quedan tres figuras. Como pos es un int, se invoca remove(int index) y no remove(Object).

Área total y mayor. areaTotal() suma f.area() sin saber qué figura es cada f. En el ejemplo, después del borrado, suma π + 2.25 + 5 = 10.3916, que se imprime como 10.39. mayor() sustituye la mejor solo si el área es estrictamente mayor. En el test «Empate en la mayor», tras borrar el círculo quedan tres figuras de área 12, y se elige la primera, el rectángulo de 3 por 4. Si la colección está vacía, devuelve null y main imprime Mayor: ninguna.

El intérprete. main lee con readLine() hasta que devuelve null, descarta las líneas en blanco y despacha cada orden con un switch de flechas. Se usa PrintWriter con un único flush() al final. Con entrada vacía el bucle no da ninguna vuelta y no se imprime nada, como pide el test «Entrada vacía». En el test «Colección vacía», las cuatro órdenes producen sus mensajes de caso vacío, una línea cada una.

Casos de prueba

CasoEntradaSalida esperadaSalida realResultado
Ejemplo del enunciadoC 1 R 2 3 Q 1.5 T 4 2.5 L B 1 B 7 L S M0: Circulo(r=1.00) area=3.14 1: Rectangulo(b=2.00 h=3.00) area=6.00 2: Cuadrado(l=1.50) area=2.25 3: Triangulo(b=4.00 h=2.50) area=5.00 ERROR posicion 7 0: Circulo(r=1.00) area=3.14 1: Cuadrado(l=1.50) area=2.25 2: Triangulo(b=4.00 h=2.50) area=5.00 Total: 3 figuras, area 10.39 Mayor: Triangulo(b=4.00 h=2.50) area=5.000: Circulo(r=1.00) area=3.14 1: Rectangulo(b=2.00 h=3.00) area=6.00 2: Cuadrado(l=1.50) area=2.25 3: Triangulo(b=4.00 h=2.50) area=5.00 ERROR posicion 7 0: Circulo(r=1.00) area=3.14 1: Cuadrado(l=1.50) area=2.25 2: Triangulo(b=4.00 h=2.50) area=5.00 Total: 3 figuras, area 10.39 Mayor: Triangulo(b=4.00 h=2.50) area=5.00OK
Colección vacíaL S M B 0(vacia) Total: 0 figuras, area 0.00 Mayor: ninguna ERROR posicion 0(vacia) Total: 0 figuras, area 0.00 Mayor: ninguna ERROR posicion 0OK
Entrada vacía(vacía)(vacía)(vacía)OK
Un elemento y posiciones negativasQ 2 M B -1 B 0 L SMayor: Cuadrado(l=2.00) area=4.00 ERROR posicion -1 (vacia) Total: 0 figuras, area 0.00Mayor: Cuadrado(l=2.00) area=4.00 ERROR posicion -1 (vacia) Total: 0 figuras, area 0.00OK
Empate en la mayorR 3 4 T 6 4 C 2 R 2 6 M B 2 M SMayor: Circulo(r=2.00) area=12.57 Mayor: Rectangulo(b=3.00 h=4.00) area=12.00 Total: 3 figuras, area 36.00Mayor: Circulo(r=2.00) area=12.57 Mayor: Rectangulo(b=3.00 h=4.00) area=12.00 Total: 3 figuras, area 36.00OK

Salidas reales: código compilado con Java 21.0.12.1 (Temurin) y ejecutado en un contenedor aislado el 9 de octubre de 2026.

Complejidad

Sea n el número de figuras en un momento dado. Añadir es O(1) amortizado gracias a ArrayList. Borrar en la posición i cuesta O(n − i) porque hay que desplazar los elementos posteriores. Listar, sumar áreas y buscar la mayor son O(n): un recorrido único en el que cada llamada polimórfica cuesta un tiempo constante. La búsqueda de la mayor vuelve a calcular mejor.area() en cada comparación, lo cual sigue siendo O(1) por iteración.

Si la entrada tiene k órdenes, el peor caso total es O(k · n). La memoria es O(n): una referencia por figura en la lista más los dos o tres double de cada objeto. El enlace dinámico no añade un coste apreciable, porque la máquina virtual resuelve la llamada mediante la tabla de métodos de la clase y, en la práctica, el compilador JIT suele optimizarla.

Errores típicos

  • Escribir el formato completo en el toString() de cada subclase, duplicando código, en lugar de definirlo una vez en Figura y sobrescribir solo lo que cambia.
  • Implementar areaTotal() o mayor() con cadenas de instanceof y castings: funciona, pero anula el polimorfismo y el enunciado lo prohíbe expresamente.
  • Validar solo pos >= size() y olvidar las posiciones negativas, de modo que B -1 lanza una excepción en vez de imprimir el error.
  • Redefinir area() en Cuadrado con un campo lado propio, o declarar base como private en Rectangulo y no poder acceder a ella desde la subclase.
  • Usar String.format sin Locale.ROOT: en una máquina configurada en español se imprime 3,14 y la salida no coincide.
  • Usar >= al buscar la mayor, con lo que en caso de empate se devuelve la última figura y no la primera.

Variantes

Ordenar la colección por área

Se añade una orden O que ordena las figuras de menor a mayor área sin perder el orden relativo de las que empatan. Basta con elementos.sort(Comparator.comparingDouble(Figura::area)): List.sort es estable y la referencia a método Figura::area también se resuelve dinámicamente para cada objeto. Otra opción es hacer que Figura implemente Comparable<Figura>. Puedes repasar ese enfoque en el ejercicio sobre interfaces.

Añadir el perímetro como segundo método abstracto

Se declara public abstract double perimetro() en Figura y se añade per=%.2f al toString(). El compilador obligará a implementarlo en Circulo, Rectangulo y Triangulo. Para el triángulo hay que fijar qué tipo es, por ejemplo isósceles, cuyo lado vale Math.hypot(base / 2, altura). Cuadrado vuelve a heredarlo sin cambios. Es una buena pregunta para comprobar si el alumno entiende qué obliga a hacer un método abstracto.

Historial de cambios

  1. Reescrito con la plantilla de Código de Examen: enunciado de examen, pistas, solución explicada, casos de prueba y código compilado y ejecutado; versión en inglés enlazada.
  2. Publicación original.

Artículo generado mediante AI.larebelion

Kernel

· Dirección de tecnología · España

«Si para sumar áreas necesitas preguntar qué figura es cada una, no has usado polimorfismo: has escrito un switch disfrazado.»

Comentarios

Publicar un comentario