Saltar al contenido
CodigoDev

10 Ejercicios resueltos de Herencia y Polimorfismo en Java

Java Intermedio
lightbulb Introducción al ejercicio

Los ejercicios resueltos de herencia y polimorfismo en Java permiten comprender cómo organizar clases relacionadas sin duplicar comportamientos. En esta práctica trabajarás con superclases, subclases, constructores, sobrescritura de métodos, clases abstractas e interfaces. También aprenderás a procesar objetos de distintos tipos mediante una misma referencia y a reconocer cuándo conviene reutilizar código. Los diez problemas avanzan desde ejemplos concretos hasta un pequeño sistema de pedidos. Cada uno incluye un enunciado, una pista, una solución completa y su salida para que puedas comparar tu razonamiento con una implementación funcional.

menu_book

Aprendizaje teórico

Herencia: compartir sin duplicar

La herencia permite definir una clase a partir de otra mediante extends. La clase hija recibe los métodos accesibles de la clase padre y puede añadir sus propias capacidades. Por ejemplo, Empleado puede concentrar un nombre y Vendedor incorporar una comisión. Java permite extender una sola clase directamente; esto ayuda a mantener una jerarquía comprensible.

Los constructores no se heredan: la subclase puede invocar al constructor de su padre con super(...). Es recomendable mantener los atributos privados y ofrecer métodos para consultar o modificar los datos cuando corresponda. Así se protege el estado de los objetos.

Sobrescritura y polimorfismo

Cuando una subclase redefine un método heredado con la misma firma, hablamos de sobrescritura. La anotación @Override permite al compilador detectar errores en esa intención. El polimorfismo aparece cuando una referencia de tipo padre, por ejemplo Empleado, apunta a un objeto Vendedor: al llamar un método sobrescrito, Java ejecuta la versión correspondiente al objeto real.

Esto permite recorrer una lista de Empleado sin preguntar constantemente qué subtipo contiene. La sobrecarga, en cambio, utiliza métodos con el mismo nombre y diferentes parámetros; no debe confundirse con la sobrescritura.

Clases abstractas e interfaces

Una clase abstract puede compartir estado y código, pero también exigir que sus hijas implementen ciertos métodos. Una interfaz expresa un contrato que pueden cumplir clases distintas mediante implements. Por ejemplo, varios medios de pago pueden compartir una operación pagar() sin necesitar una misma clase padre.

Conviene utilizar herencia cuando exista una relación real de «es un tipo de» y preferir interfaces o composición para capacidades independientes. En los ejercicios verás ambas posibilidades y cómo validar los datos antes de calcular resultados.

Ejercicio 1: Especializar un empleado

help_outline Problema:

Crea una clase Empleado con nombre y un método que devuelva su descripción. Define Desarrollador como subclase y agrega el lenguaje que utiliza. Muestra los datos de un desarrollador sin repetir la lógica del nombre.

Pista

Piensa qué información es común y cuál pertenece únicamente al tipo especializado.

code Solución explicada:
java
public class Main {
    static class Empleado {
        private final String nombre;
        Empleado(String nombre) { this.nombre = nombre; }
        String descripcion() { return "Empleado: " + nombre; }
    }
    static class Desarrollador extends Empleado {
        private final String lenguaje;
        Desarrollador(String nombre, String lenguaje) {
            super(nombre);
            this.lenguaje = lenguaje;
        }
        String especialidad() { return "Lenguaje: " + lenguaje; }
    }
    public static void main(String[] args) {
        Desarrollador persona = new Desarrollador("Ana", "Java");
        System.out.println(persona.descripcion());
        System.out.println(persona.especialidad());
    }
}

terminal Resultado esperado:

Empleado: Ana
Lenguaje: Java

Ejercicio 2: Calcular salarios con sobrescritura

help_outline Problema:

Define Empleado con un salario base y Vendedor con una comisión. Sobrescribe el método que calcula el pago mensual para sumar la comisión. Comprueba el cálculo con un salario de 2500 y una comisión de 450.

Pista

Busca un método cuyo comportamiento deba cambiar sin alterar su nombre ni sus parámetros.

code Solución explicada:
java
public class Main {
    static class Empleado {
        private final double salarioBase;
        Empleado(double salarioBase) {
            if (salarioBase < 0) throw new IllegalArgumentException("Salario negativo");
            this.salarioBase = salarioBase;
        }
        double pagoMensual() { return salarioBase; }
    }
    static class Vendedor extends Empleado {
        private final double comision;
        Vendedor(double salario, double comision) {
            super(salario);
            if (comision < 0) throw new IllegalArgumentException("Comision negativa");
            this.comision = comision;
        }
        @Override double pagoMensual() { return super.pagoMensual() + comision; }
    }
    public static void main(String[] args) {
        Empleado empleado = new Vendedor(2500, 450);
        System.out.printf(java.util.Locale.US, "Pago: %.2f%n", empleado.pagoMensual());
    }
}

terminal Resultado esperado:

Pago: 2950.00

Ejercicio 3: Mostrar notificaciones polimórficas

help_outline Problema:

Crea una clase Notificacion con un método enviar(). Implementa las subclases Correo y SMS, cada una con un mensaje distinto. Guarda ambos objetos en un arreglo del tipo padre y envíalos con un solo bucle.

Pista

Una variable del tipo padre puede referirse a cualquiera de sus subclases.

code Solución explicada:
java
public class Main {
    static class Notificacion {
        void enviar() { System.out.println("Notificacion general"); }
    }
    static class Correo extends Notificacion {
        @Override void enviar() { System.out.println("Correo enviado"); }
    }
    static class SMS extends Notificacion {
        @Override void enviar() { System.out.println("SMS enviado"); }
    }
    public static void main(String[] args) {
        Notificacion[] avisos = {new Correo(), new SMS()};
        for (Notificacion aviso : avisos) aviso.enviar();
    }
}

terminal Resultado esperado:

Correo enviado
SMS enviado

Ejercicio 4: Calcular áreas con clases abstractas

help_outline Problema:

Modela una clase abstracta Figura con el método area(). Implementa un rectángulo de base 5 y altura 3 y un círculo de radio 2. Recorre las figuras e imprime sus áreas con dos decimales.

Pista

El método común puede declararse sin implementación para obligar a cada figura a calcular su propia área.

code Solución explicada:
java
public class Main {
    static abstract class Figura { abstract double area(); }
    static class Rectangulo extends Figura {
        private final double base, altura;
        Rectangulo(double base, double altura) {
            if (base <= 0 || altura <= 0) throw new IllegalArgumentException("Medidas invalidas");
            this.base = base; this.altura = altura;
        }
        @Override double area() { return base * altura; }
    }
    static class Circulo extends Figura {
        private final double radio;
        Circulo(double radio) {
            if (radio <= 0) throw new IllegalArgumentException("Radio invalido");
            this.radio = radio;
        }
        @Override double area() { return Math.PI * radio * radio; }
    }
    public static void main(String[] args) {
        Figura[] figuras = {new Rectangulo(5, 3), new Circulo(2)};
        for (Figura figura : figuras)
            System.out.printf(java.util.Locale.US, "Area: %.2f%n", figura.area());
    }
}

terminal Resultado esperado:

Area: 15.00
Area: 12.57

Ejercicio 5: Aplicar descuentos según el cliente

help_outline Problema:

Diseña una clase Cliente cuyo método descuento() devuelva cero. Las subclases ClienteFrecuente y ClientePremium deben ofrecer descuentos del 10 % y 20 %. Calcula el precio final de una compra de 200 para cada tipo de cliente.

Pista

Deja que cada objeto determine su porcentaje y centraliza el cálculo del precio final.

code Solución explicada:
java
public class Main {
    static class Cliente {
        double descuento() { return 0; }
        double total(double importe) {
            if (importe < 0) throw new IllegalArgumentException("Importe negativo");
            return importe * (1 - descuento());
        }
    }
    static class ClienteFrecuente extends Cliente {
        @Override double descuento() { return 0.10; }
    }
    static class ClientePremium extends Cliente {
        @Override double descuento() { return 0.20; }
    }
    public static void main(String[] args) {
        Cliente[] clientes = {new Cliente(), new ClienteFrecuente(), new ClientePremium()};
        for (Cliente cliente : clientes)
            System.out.printf(java.util.Locale.US, "Total: %.2f%n", cliente.total(200));
    }
}

terminal Resultado esperado:

Total: 200.00
Total: 180.00
Total: 160.00

Ejercicio 6: Implementar diferentes medios de pago

help_outline Problema:

Define una interfaz MedioPago con pagar(double importe). Implementa pago con tarjeta y transferencia, mostrando una confirmación distinta para cada uno. Procesa un pago de 120 con ambos medios y rechaza importes no positivos.

Pista

Una interfaz permite que objetos sin un padre específico compartan la misma operación.

code Solución explicada:
java
public class Main {
    interface MedioPago { void pagar(double importe); }
    static class Tarjeta implements MedioPago {
        @Override public void pagar(double importe) {
            validar(importe);
            System.out.printf(java.util.Locale.US, "Tarjeta: %.2f%n", importe);
        }
    }
    static class Transferencia implements MedioPago {
        @Override public void pagar(double importe) {
            validar(importe);
            System.out.printf(java.util.Locale.US, "Transferencia: %.2f%n", importe);
        }
    }
    static void validar(double importe) {
        if (!Double.isFinite(importe) || importe <= 0)
            throw new IllegalArgumentException("Importe invalido");
    }
    public static void main(String[] args) {
        MedioPago[] medios = {new Tarjeta(), new Transferencia()};
        for (MedioPago medio : medios) medio.pagar(120);
    }
}

terminal Resultado esperado:

Tarjeta: 120.00
Transferencia: 120.00

Ejercicio 7: Calcular el costo de envíos distintos

help_outline Problema:

Crea una clase abstracta Envio que almacene el peso en kilogramos. Implementa EnvioLocal, con costo fijo de 8 más 2 por kilogramo, y EnvioExpress, con costo fijo de 15 más 5 por kilogramo. Calcula ambos costos para un paquete de 3 kg.

Pista

La validación del peso y el atributo compartido pueden vivir en la clase base.

code Solución explicada:
java
public class Main {
    static abstract class Envio {
        private final double peso;
        Envio(double peso) {
            if (!Double.isFinite(peso) || peso <= 0) throw new IllegalArgumentException("Peso invalido");
            this.peso = peso;
        }
        double peso() { return peso; }
        abstract double costo();
    }
    static class EnvioLocal extends Envio {
        EnvioLocal(double peso) { super(peso); }
        @Override double costo() { return 8 + 2 * peso(); }
    }
    static class EnvioExpress extends Envio {
        EnvioExpress(double peso) { super(peso); }
        @Override double costo() { return 15 + 5 * peso(); }
    }
    public static void main(String[] args) {
        Envio[] envios = {new EnvioLocal(3), new EnvioExpress(3)};
        for (Envio envio : envios)
            System.out.printf(java.util.Locale.US, "Costo: %.2f%n", envio.costo());
    }
}

terminal Resultado esperado:

Costo: 14.00
Costo: 30.00

Ejercicio 8: Generar reportes de diferentes formatos

help_outline Problema:

Crea una clase abstracta Reporte que guarde un título y obligue a implementar generar(). Construye las clases ReporteTexto y ReporteCSV. Reúne ambas en una lista y genera su contenido sin comprobar el tipo concreto de cada objeto.

Pista

Separa los datos compartidos del formato que cada subclase debe producir.

code Solución explicada:
java
import java.util.List;
public class Main {
    static abstract class Reporte {
        private final String titulo;
        Reporte(String titulo) {
            if (titulo == null || titulo.isBlank()) throw new IllegalArgumentException("Titulo vacio");
            this.titulo = titulo;
        }
        String titulo() { return titulo; }
        abstract String generar();
    }
    static class ReporteTexto extends Reporte {
        ReporteTexto(String titulo) { super(titulo); }
        @Override String generar() { return "Informe: " + titulo(); }
    }
    static class ReporteCSV extends Reporte {
        ReporteCSV(String titulo) { super(titulo); }
        @Override String generar() { return "titulo\n" + titulo(); }
    }
    public static void main(String[] args) {
        List<Reporte> reportes = List.of(new ReporteTexto("Ventas"), new ReporteCSV("Ventas"));
        for (Reporte reporte : reportes)
            System.out.println(reporte.generar().replace("\n", " | "));
    }
}

terminal Resultado esperado:

Informe: Ventas
titulo | Ventas

Ejercicio 9: Administrar una flota con diferentes vehículos

help_outline Problema:

Modela una clase abstracta Vehiculo con placa y método consumo(double kilometros). Un automóvil consume 7 litros por cada 100 km y una motocicleta 3.5 litros. Recorre una colección de vehículos, calcula su consumo para 200 km y suma los litros necesarios.

Pista

El recorrido puede trabajar con el tipo general, mientras cada vehículo aporta su fórmula.

code Solución explicada:
java
import java.util.List;
public class Main {
    static abstract class Vehiculo {
        private final String placa;
        Vehiculo(String placa) { this.placa = placa; }
        String placa() { return placa; }
        abstract double consumo(double kilometros);
        void validar(double kilometros) {
            if (!Double.isFinite(kilometros) || kilometros < 0)
                throw new IllegalArgumentException("Distancia invalida");
        }
    }
    static class Automovil extends Vehiculo {
        Automovil(String placa) { super(placa); }
        @Override double consumo(double km) { validar(km); return km * 7 / 100; }
    }
    static class Motocicleta extends Vehiculo {
        Motocicleta(String placa) { super(placa); }
        @Override double consumo(double km) { validar(km); return km * 3.5 / 100; }
    }
    public static void main(String[] args) {
        List<Vehiculo> flota = List.of(new Automovil("ABC-123"), new Motocicleta("XYZ-789"));
        double total = 0;
        for (Vehiculo vehiculo : flota) {
            double litros = vehiculo.consumo(200);
            total += litros;
            System.out.printf(java.util.Locale.US, "%s: %.1f L%n", vehiculo.placa(), litros);
        }
        System.out.printf(java.util.Locale.US, "Total: %.1f L%n", total);
    }
}

terminal Resultado esperado:

ABC-123: 14.0 L
XYZ-789: 7.0 L
Total: 21.0 L

Ejercicio 10: Procesar pedidos con envíos y descuentos

help_outline Problema:

Construye un sistema de pedidos con una clase abstracta Pedido que almacene subtotal y defina total(). Un pedido estándar añade 10 por envío; uno premium aplica 15 % de descuento y ofrece envío gratuito. Ambos deben implementar una interfaz Resumible. Procesa pedidos de 100 y 200, imprime sus resúmenes y suma la facturación. Valida que los subtotales sean positivos.

Pista

Combina una clase base para los datos, un método abstracto para el cálculo y una interfaz para presentar cada pedido.

code Solución explicada:
java
import java.util.List;
public class Main {
    interface Resumible { String resumen(); }
    static abstract class Pedido implements Resumible {
        private final double subtotal;
        Pedido(double subtotal) {
            if (!Double.isFinite(subtotal) || subtotal <= 0)
                throw new IllegalArgumentException("Subtotal invalido");
            this.subtotal = subtotal;
        }
        double subtotal() { return subtotal; }
        abstract double total();
    }
    static class PedidoEstandar extends Pedido {
        PedidoEstandar(double subtotal) { super(subtotal); }
        @Override double total() { return subtotal() + 10; }
        @Override public String resumen() { return "Estandar"; }
    }
    static class PedidoPremium extends Pedido {
        PedidoPremium(double subtotal) { super(subtotal); }
        @Override double total() { return subtotal() * 0.85; }
        @Override public String resumen() { return "Premium"; }
    }
    public static void main(String[] args) {
        List<Pedido> pedidos = List.of(new PedidoEstandar(100), new PedidoPremium(200));
        double facturacion = 0;
        for (Pedido pedido : pedidos) {
            facturacion += pedido.total();
            System.out.printf(java.util.Locale.US, "%s: %.2f%n", pedido.resumen(), pedido.total());
        }
        System.out.printf(java.util.Locale.US, "Facturacion: %.2f%n", facturacion);
    }
}

terminal Resultado esperado:

Estandar: 110.00
Premium: 170.00
Facturacion: 280.00
help

Preguntas frecuentes

¿En qué se diferencian herencia y polimorfismo?

La herencia establece una relación entre clases: una subclase reutiliza y especializa elementos de su clase padre. El polimorfismo permite tratar objetos de distintas subclases mediante un tipo común y ejecutar el método adecuado según el objeto real. Se complementan, pero no son sinónimos: también puedes lograr polimorfismo mediante interfaces.

¿Cuándo conviene utilizar una clase abstracta en lugar de una interfaz?

Una clase abstracta resulta útil cuando varios tipos estrechamente relacionados comparten atributos, validaciones o implementaciones. Una interfaz es preferible cuando quieres definir una capacidad que pueden ofrecer clases de familias diferentes, como pagar o generar un resumen. Recuerda que una clase solo puede extender una clase, pero puede implementar varias interfaces.

¿Por qué se recomienda utilizar @Override?

La anotación @Override indica que un método debe sobrescribir uno heredado o implementar uno declarado en una interfaz. Si escribes mal su nombre o cambias accidentalmente los parámetros, el compilador te avisará. Además, facilita la lectura: quien revise el código podrá identificar rápidamente qué comportamientos se están especializando.

task_alt

Conclusión

Estos diez ejercicios muestran cómo pasar de una jerarquía sencilla de empleados a sistemas que procesan notificaciones, figuras, clientes, envíos y pedidos. Practicaste extends, super, la sobrescritura de métodos y el despacho dinámico, además de utilizar clases abstractas e interfaces para establecer contratos claros. La ventaja principal no consiste en crear más clases, sino en evitar decisiones repetitivas sobre el tipo de cada objeto y mantener las responsabilidades bien distribuidas.

Como siguiente práctica, intenta agregar una tercera variante a los ejemplos de pagos o pedidos sin modificar el bucle que los procesa. Si puedes incorporar nuevos comportamientos conservando el código que trabaja con el tipo común, estarás aplicando correctamente una de las ideas más útiles de la programación orientada a objetos.

rocket_launch

Sigue aprendiendo

Aprende a definir subclases, acceder a miembros heredados y utilizar constructores.

Comprende cómo Java selecciona el método sobrescrito según el objeto real.

Consulta las reglas de sobrescritura y el uso de @Override.

A
Autor del artículo

Alejandro Nes

calendar_today update Actualizado

Desarrollador de Software y creador de contenido educativo

Otros ejercicios prácticos recomendados

Ver todos arrow_forward