Herencia en Java — ejercicios con animales, figuras y la plantilla de un restaurante (y el cast que convirtió a mi gato en perro)
En la práctica de herencia tenía un array de Animal con perros y gatos, y quería imprimir la raza de los perros. getRaza() solo existe en Perro, así que hice lo que me pareció lógico: convertir cada animal en perro.
for (Animal a : refugio) {
Perro p = (Perro) a;
System.out.println(p.getRaza());
}
Compiló sin un aviso. El primer perro imprimió su raza y, al llegar al gato, el programa explotó con un ClassCastException. El compilador no se quejó porque **un Animal podría ser un Perro**; el problema es que ese animal en concreto no lo era. Un cast no transforma el objeto: solo le dice a Java «confía en mí». Y yo no tenía motivos para que confiara.
En estos ejercicios vas a construir jerarquías completas: clases abstractas, constructores con super, métodos sobrescritos que reutilizan los del padre y arrays polimórficos. Antes de las soluciones, tres fragmentos con trampa (el primero es justo mi cast). Si necesitas repasar antes de empezar, tienes la teoría y la práctica del tema.
Tabla de Contenidos
Cómo usar este artículo
- Lee el enunciado y la salida esperada.
- Inténtalo al menos 15 minutos antes de abrir la pista.
- Antes de escribir código, dibuja la jerarquía: quién hereda de quién y qué método va en cada piso.
- Compara con la solución y lee el porqué de cada decisión.
Cada clase va en su propio archivo (Animal.java, Perro.java…) y un Main.java con las pruebas. Escribe @Override en todo lo que sobrescribas: te ahorrará más de un fragmento de la sección de errores.
Nivel 1 — Básico: animales del refugio
Enunciado. Crea una jerarquía para un refugio de animales:
Animal, abstracta, con nombre y edad (private final). Si la edad es negativa, se guarda0.- Un método abstracto
String sonido()y un métodoString presentarse()que devuelvaToby (3 años) dice ¡Guau!. Perro extends Animal, con raza. Su sonido es¡Guau!y supresentarse()añade al finaly es un beagle, reutilizando el del padre.Gato extends Animal, con unboolean casero. Si es casero diceMiau; si no,¡Fffff!.- En
main, un arrayAnimal[]con los cuatro animales de la prueba: presenta a todos, cuenta los perros y calcula la edad media.
Pruebas y salida esperada:
Animal[] refugio = {
new Perro("Toby", 3, "beagle"),
new Gato("Luna", 2, true),
new Gato("Sombra", -1, false),
new Perro("Rex", 7, "pastor alemán")
};
Toby (3 años) dice ¡Guau! y es un beagle Luna (2 años) dice Miau Sombra (0 años) dice ¡Fffff! Rex (7 años) dice ¡Guau! y es un pastor alemán Perros: 2 · edad media: 3,0
El presentarse() del perro es super.presentarse() + " y es un " + raza. Para la edad media, ojo con la división entera: convierte la suma a double antes de dividir.
Nivel 2 — Intermedio: figuras que se comparan y se ordenan
Enunciado. Esta vez no buscamos la figura mayor, como en la práctica, sino ordenarlas todas:
Figura, abstracta, condouble area()yString getNombre()abstractos.- Un método
final boolean esMayorQue(Figura otra)que compare por área. toString()con el formatoCírculo r=1.5 de área 7,07.- Un método
static void ordenar(Figura[] figuras)que las ordene de menor a mayor área usando soloesMayorQue(método de la burbuja). Circulo(radio) yTriangulo(tres lados, área con la fórmula de Herón).Triangulotiene un métodostatic boolean esValido(double a, double b, double c): los tres lados positivos y cada uno menor que la suma de los otros dos.
Pruebas y salida esperada:
System.out.println(Triangulo.esValido(3, 4, 5) + " " + Triangulo.esValido(1, 2, 10));
Figura[] figuras = {
new Circulo(1.5),
new Triangulo(3, 4, 5),
new Circulo(1),
new Triangulo(5, 5, 6)
};
System.out.println(figuras[0].esMayorQue(figuras[1]));
Figura.ordenar(figuras);
for (Figura f : figuras) {
System.out.println(f);
}
true false true Círculo r=1.0 de área 3,14 Triángulo 3.0-4.0-5.0 de área 6,00 Círculo r=1.5 de área 7,07 Triángulo 5.0-5.0-6.0 de área 12,00
Fórmula de Herón: con el semiperímetro s = (a + b + c) / 2, el área es Math.sqrt(s * (s - a) * (s - b) * (s - c)). En la burbuja, si figuras[j].esMayorQue(figuras[j + 1]), intercámbialas con una variable auxiliar.
Nivel 3 — Desafío: la plantilla de un restaurante
Enunciado. Modela la plantilla del restaurante Casa Pepe. Cada hora trabajada cuesta 10 €:
Trabajador, abstracta, con nombre (private) y horas semanales (protected final, nunca negativas). ConstantePRECIO_HORA = 10.0.double costeSemanal()(horas × precio),String getPuesto()abstracto ytoString()con el formatoCocinero Ana 500,00 €.Cocinero: si hace turno de noche, su coste es un 25 % mayor.Camarero: cuesta lo normal, pero acumula propinas (registrarPropina, ignorando cantidades negativas). Las propinas no son coste del restaurante.Encargado extends Camarero: cuesta lo que un camarero más 120 € de plus de responsabilidad.Plantilla, con un array de capacidad fija:contratar(falla si está llena o ya hay alguien con ese nombre, sin distinguir mayúsculas),costeTotal(),masCaro(),propinasSala()(las de todos los camareros, encargados incluidos) ylistar().
Pruebas y salida esperada:
Plantilla casaPepe = new Plantilla(4);
Camarero lucia = new Camarero("Lucía", 30);
Encargado tomas = new Encargado("Tomás", 40);
System.out.println(casaPepe.contratar(new Cocinero("Ana", 40, true)));
casaPepe.contratar(lucia);
casaPepe.contratar(tomas);
System.out.println(casaPepe.contratar(new Cocinero("ana", 20, false))); // repetida
casaPepe.contratar(new Cocinero("Raúl", 35, false));
System.out.println(casaPepe.contratar(new Camarero("Eva", 20))); // llena
lucia.registrarPropina(35.5);
tomas.registrarPropina(20);
lucia.registrarPropina(-5); // ignorada
casaPepe.listar();
System.out.printf("Coste semanal: %.2f €%n", casaPepe.costeTotal());
System.out.println("Más caro: " + casaPepe.masCaro().getNombre());
System.out.printf("Propinas de sala: %.2f €%n", casaPepe.propinasSala());
true false false Cocinero Ana 500,00 € Camarero Lucía 300,00 € Encargado Tomás 520,00 € Cocinero Raúl 350,00 € Coste semanal: 1670,00 € Más caro: Tomás Propinas de sala: 55,50 €
El costeSemanal() del cocinero y el del encargado empiezan por super.costeSemanal(). Para las propinas de sala, instanceof Camarero también es true para el encargado: no necesitas preguntar por los dos.
¿Qué falla aquí?
Tres fragmentos que compilan. Antes de leer la explicación, intenta predecir qué pasa.
Fragmento 1 — El cast de confianza
Animal[] refugio = { new Perro("Toby", 3, "beagle"), new Gato("Luna", 2, true) };
for (Animal a : refugio) {
Perro p = (Perro) a;
System.out.println(p.getRaza());
}
Imprime beagle y después lanza ClassCastException: class Gato cannot be cast to class Perro. Es mi historia del principio. El compilador acepta el cast porque algún Animal podría ser un Perro, pero quien decide al ejecutar es el objeto real, y Luna es una gata. La solución es preguntar antes:
for (Animal a : refugio) {
if (a instanceof Perro p) {
System.out.println(p.getRaza());
}
}
«arreglar» el ClassCastException con un try-catch alrededor del cast. Ocultas el síntoma, pero el diseño sigue mal: si sabes que puede no ser un perro, pregúntalo con instanceof.
Fragmento 2 — El perro que no ladraba
class Animal {
public String sonido() { return "..."; }
public String hablar() { return "Dice " + sonido(); }
}
class Perro extends Animal {
public String sonidos() { return "¡Guau!"; } // sin @Override
}
Animal toby = new Perro();
System.out.println(toby.hablar());
Imprime Dice .... El perro no ha sobrescrito sonido(): ha creado un método nuevo, sonidos(), con una s de más. Para Java son dos métodos distintos, así que hablar() sigue llamando al sonido() de Animal. No hay error de compilación ni de ejecución: solo un resultado equivocado, que es lo peor.
Con @Override encima de sonidos(), el compilador lo habría cazado al momento: method does not override or implement a method from a supertype. Y si sonido() fuera abstracto en Animal, como en el nivel 1, tampoco compilaría, porque Perro no lo implementaría.
Fragmento 3 — El toString infinito
class Animal {
protected String nombre = "Toby";
@Override
public String toString() { return nombre; }
}
class Perro extends Animal {
@Override
public String toString() {
return toString() + " (perro)";
}
}
System.out.println(new Perro());
Lanza StackOverflowError. La intención era «lo que imprima Animal más (perro)«, pero toString() sin nada delante llama al toString() de este mismo objeto, que es… este mismo método. Se llama a sí mismo una y otra vez hasta llenar la pila. La solución es una palabra:
return super.toString() + " (perro)"; // Toby (perro)
Cada vez que un método sobrescrito necesite «lo que hacía el padre», escribe super. delante. Sin él, estás llamando a tu propia versión.
Soluciones
Nivel 1 — Animales del refugio
public abstract class Animal {
private final String nombre;
private final int edad;
public Animal(String nombre, int edad) {
this.nombre = nombre;
this.edad = Math.max(0, edad); // nada de edades negativas
}
public String getNombre() { return nombre; }
public int getEdad() { return edad; }
// Todo animal hace un sonido, pero cada uno el suyo
public abstract String sonido();
public String presentarse() {
return nombre + " (" + edad + " años) dice " + sonido();
}
}
public class Perro extends Animal {
private final String raza;
public Perro(String nombre, int edad, String raza) {
super(nombre, edad); // primero la parte de Animal
this.raza = raza;
}
public String getRaza() { return raza; }
@Override
public String sonido() { return "¡Guau!"; }
@Override
public String presentarse() {
return super.presentarse() + " y es un " + raza;
}
}
public class Gato extends Animal {
private final boolean casero;
public Gato(String nombre, int edad, boolean casero) {
super(nombre, edad);
this.casero = casero;
}
public boolean isCasero() { return casero; }
@Override
public String sonido() { return casero ? "Miau" : "¡Fffff!"; }
}
public class Main {
public static void main(String[] args) {
Animal[] refugio = {
new Perro("Toby", 3, "beagle"),
new Gato("Luna", 2, true),
new Gato("Sombra", -1, false),
new Perro("Rex", 7, "pastor alemán")
};
int perros = 0, sumaEdades = 0;
for (Animal a : refugio) {
System.out.println(a.presentarse());
if (a instanceof Perro) perros++;
sumaEdades += a.getEdad();
}
System.out.printf("Perros: %d · edad media: %.1f%n", perros, (double) sumaEdades / refugio.length);
}
}
Por qué así:
- **
Animales abstracta** porque en el refugio no hay «animales genéricos»: cada uno es perro o gato. Ysonido()es abstracto porque no hay un sonido que valga para todos. - **La validación de la edad vive en
Animal.** Así sirve para perros, gatos y cualquier hija futura. Sombra se creó con-1y sale con 0 años sin queGatohaga nada. - **
presentarse()usasonido()sin saber cuál será.** Cuando el objeto es un gato, se ejecuta elsonido()deGato. El perro, además, sobrescribepresentarse()y reutiliza el del padre consuper. - **
(double) sumaEdades / refugio.length:** sin el cast, 12 / 4 sería división entera. Aquí daría 3 igualmente, pero con otras edades perderías los decimales.
Nivel 2 — Figuras que se comparan
public abstract class Figura {
public abstract double area();
public abstract String getNombre();
// final: comparar por área es igual para todas las figuras
public final boolean esMayorQue(Figura otra) {
return area() > otra.area();
}
@Override
public String toString() {
return String.format("%s de área %.2f", getNombre(), area());
}
// Ordena de menor a mayor área (burbuja): solo usa esMayorQue
public static void ordenar(Figura[] figuras) {
for (int i = 0; i < figuras.length - 1; i++) {
for (int j = 0; j < figuras.length - 1 - i; j++) {
if (figuras[j].esMayorQue(figuras[j + 1])) {
Figura tmp = figuras[j]; // intercambiamos referencias,
figuras[j] = figuras[j + 1]; // no copiamos objetos
figuras[j + 1] = tmp;
}
}
}
}
}
public class Circulo extends Figura {
private final double radio;
public Circulo(double radio) {
this.radio = Math.abs(radio);
}
@Override
public double area() { return Math.PI * radio * radio; }
@Override
public String getNombre() { return "Círculo r=" + radio; }
}
public class Triangulo extends Figura {
private final double a, b, c;
public Triangulo(double a, double b, double c) {
this.a = a;
this.b = b;
this.c = c;
}
// static: se puede comprobar ANTES de crear el triángulo
public static boolean esValido(double a, double b, double c) {
return a > 0 && b > 0 && c > 0 && a + b > c && a + c > b && b + c > a;
}
@Override
public double area() {
double s = (a + b + c) / 2; // semiperímetro
return Math.sqrt(s * (s - a) * (s - b) * (s - c)); // fórmula de Herón
}
@Override
public String getNombre() { return "Triángulo " + a + "-" + b + "-" + c; }
}
Por qué así:
- **
Figurano tiene constructor ni atributos**, y no pasa nada: Java le pone uno vacío, y elsuper()implícito deCirculoyTriangulolo encuentra. Las hijas no necesitan escribirsuper. - **
esMayorQueesfinal.** Si una hija pudiera redefinir qué significa «mayor»,ordenardejaría de ser fiable. Las hijas deciden cómo calcular su área, pero no cómo se comparan. - **
ordenaresstaticy recibeFigura[].** No pertenece a ninguna figura concreta, y funciona con cualquier mezcla de círculos y triángulos (y con las figuras que añadas mañana) porque solo usa métodos deFigura. - En el intercambio se mueven referencias, no objetos. Los cuatro objetos siguen en el mismo sitio del montón; lo que cambia es qué posición del array apunta a cada uno.
- **
esValidoesstaticporque se usa antes** de que exista el triángulo. Las excepciones, que son la forma de rechazar datos dentro del constructor, llegan en el siguiente tema.
El triángulo 3-4-5 es rectángulo, así que su área también sale de base × altura / 2 = 3 × 4 / 2 = 6. Herón da exactamente lo mismo: buena forma de comprobar que la fórmula está bien escrita.
Nivel 3 — La plantilla del restaurante
public abstract class Trabajador {
public static final double PRECIO_HORA = 10.0;
private final String nombre;
protected final int horasSemana; // las hijas lo usan en sus cálculos
public Trabajador(String nombre, int horasSemana) {
this.nombre = nombre;
this.horasSemana = Math.max(0, horasSemana);
}
public String getNombre() { return nombre; }
public abstract String getPuesto();
// Coste base: horas × precio. Las hijas lo amplían si hace falta
public double costeSemanal() {
return horasSemana * PRECIO_HORA;
}
@Override
public String toString() {
return String.format("%-9s %-6s %7.2f €", getPuesto(), nombre, costeSemanal());
}
}
public class Cocinero extends Trabajador {
private final boolean turnoNoche;
public Cocinero(String nombre, int horasSemana, boolean turnoNoche) {
super(nombre, horasSemana);
this.turnoNoche = turnoNoche;
}
@Override
public String getPuesto() { return "Cocinero"; }
@Override
public double costeSemanal() {
double base = super.costeSemanal(); // reutilizamos el cálculo del padre
return turnoNoche ? base * 1.25 : base; // +25 % por nocturnidad
}
}
public class Camarero extends Trabajador {
private double propinas; // no es coste: lo pagan los clientes
public Camarero(String nombre, int horasSemana) {
super(nombre, horasSemana);
}
public void registrarPropina(double cantidad) {
if (cantidad > 0) propinas += cantidad;
}
public double getPropinas() { return propinas; }
@Override
public String getPuesto() { return "Camarero"; }
// costeSemanal(): el heredado de Trabajador sirve tal cual
}
public class Encargado extends Camarero {
private static final double PLUS_RESPONSABILIDAD = 120.0;
public Encargado(String nombre, int horasSemana) {
super(nombre, horasSemana);
}
@Override
public String getPuesto() { return "Encargado"; }
@Override
public double costeSemanal() {
return super.costeSemanal() + PLUS_RESPONSABILIDAD; // lo del camarero + el plus
}
}
public class Plantilla {
private final Trabajador[] trabajadores;
private int numTrabajadores = 0;
public Plantilla(int capacidad) {
trabajadores = new Trabajador[capacidad];
}
public boolean contratar(Trabajador t) {
if (t == null || numTrabajadores == trabajadores.length) return false;
for (int i = 0; i < numTrabajadores; i++) {
if (trabajadores[i].getNombre().equalsIgnoreCase(t.getNombre())) return false;
}
trabajadores[numTrabajadores++] = t;
return true;
}
public double costeTotal() {
double total = 0;
for (int i = 0; i < numTrabajadores; i++) {
total += trabajadores[i].costeSemanal(); // cada uno con su fórmula
}
return total;
}
public Trabajador masCaro() {
if (numTrabajadores == 0) return null;
Trabajador max = trabajadores[0];
for (int i = 1; i < numTrabajadores; i++) {
if (trabajadores[i].costeSemanal() > max.costeSemanal()) max = trabajadores[i];
}
return max;
}
// Solo los que están en sala (Camarero y sus hijas) tienen propinas
public double propinasSala() {
double total = 0;
for (int i = 0; i < numTrabajadores; i++) {
if (trabajadores[i] instanceof Camarero c) total += c.getPropinas();
}
return total;
}
public void listar() {
for (int i = 0; i < numTrabajadores; i++) {
System.out.println(trabajadores[i]);
}
}
}
Por qué así:
- **
costeSemanal()no es abstracto enTrabajador. Hay una fórmula por defecto (horas × 10 €) que sirve para el camarero tal cual. Solo la cambian quienes tienen algo distinto, y lo hacen ampliándola** consuper.costeSemanal(), no copiándola. - **El encargado hereda de
Camarero, no deTrabajador. Un encargado es un** camarero (está en sala y recibe propinas) con más responsabilidad. Así heredaregistrarPropinaygetPropinassin escribir nada. Susuper.costeSemanal()llama al deCamarero, que a su vez es el deTrabajador: 40 × 10 + 120 = 520 €. - **
horasSemanaesprotected final.** Las hijas pueden leerlo si lo necesitan, pero nadie puede cambiarlo después de contratar.nombreesprivateporque las hijas no lo necesitan: tienengetNombre(). - **
propinasSala()usainstanceof Camarero**, y eso incluye a Tomás. Lucía tiene 35,50 € (la propina negativa se ignoró) y Tomás 20 €: 55,50 €. - **
Plantillasolo conoceTrabajador.** Si mañana contratas unRepartidor,costeTotal,masCaroylistarfuncionan sin tocarlos.
Escribir en Encargado el coste completo, horasSemana * PRECIO_HORA + 120. Hoy da lo mismo, pero si un día cambia cómo cobra un camarero, el encargado se quedará con la fórmula vieja. Con super.costeSemanal() hereda el cambio solo.
Míralo paso a paso en Python Tutor
Python Tutor ejecuta el código línea a línea y dibuja los objetos y las llamadas. Vamos a ver en directo el fragmento 2, el del perro que no ladraba. Elige Java, pega esto y avanza con Next:
public class Tutor {
static class Animal {
public String sonido() { return "..."; }
public String hablar() { return "Dice " + sonido(); }
}
static class Perro extends Animal {
public String sonidos() { return "¡Guau!"; } // sin @Override
}
public static void main(String[] args) {
Animal toby = new Perro();
System.out.println(toby.hablar());
Perro rex = new Perro();
System.out.println(rex.sonidos());
}
}
Salida:
Dice ... ¡Guau!
Qué observar:
- Tras
new Perro(), el objeto del montón es de tipoPerro, aunque la variabletobysea de tipoAnimal. El objeto real no cambia por cómo lo declares. - Al ejecutar
toby.hablar(), entra en elhablar()deAnimal(el perro no tiene uno propio). Fíjate en quethisapunta al objetoPerro. - Dentro de
hablar(), la llamada asonido()busca primero enPerro… y no encuentra ningúnsonido(), solosonidos(). Así que salta al deAnimaly devuelve"...". Ese salto es el error, en directo. - Con
rex.sonidos(), el método sí se ejecuta: existe, pero nadie lo usa desdehablar(). Ahora añade@Overrideencima desonidos()en tu editor y mira el error que te da el compilador.
Chuletario: herencia en Java
Lo esencial en 30 segundos
class Hija extends Padre: la hija tiene todo lo del padre más lo suyo. Un solo padre por clase; si no pones ninguno, esObject.- Constructores:
super(...)en la primera línea. Si no lo escribes, Java ponesuper()sin parámetros, y falla si el padre no tiene constructor vacío. - Sobrescribir = mismo método, otra versión (
@Overridesiempre). Sobrecargar = mismo nombre, otros parámetros. - Polimorfismo: la variable decide qué puedes llamar; el objeto decide qué versión se ejecuta.
- Cast hacia abajo: solo después de
instanceof.abstract= «complétame»;final= «no me cambies».
Sintaxis
| Quiero… | Código | Resultado |
|---|---|---|
| Heredar | public class Perro extends Animal { … } | Perro tiene todo lo de Animal |
| Llamar al constructor del padre | super(nombre, edad); (primera línea) | construye la parte heredada |
| Usar la versión del padre | super.presentarse() | reutiliza y amplía |
| Sobrescribir | @Override public String sonido() { … } | sustituye la del padre |
| Sobrecargar | presentarse() y presentarse(String) | conviven las dos |
| Atributo visible para las hijas | protected double saldo; | hijas sí, resto no |
| Clase que no se instancia | public abstract class Animal | new Animal() no compila |
| Método que las hijas deben escribir | public abstract String sonido(); | sin cuerpo |
| Método que nadie puede cambiar | public final boolean esMayorQue(…) | no se sobrescribe |
| Clase de la que nadie hereda | public final class X | extends X no compila |
| Guardar hijas en el padre | Animal a = new Perro(…); | polimorfismo |
| Array con varios tipos | Animal[] r = { new Perro(…), new Gato(…) }; | mismo bucle para todos |
| Comprobar y convertir | if (a instanceof Perro p) { p.getRaza(); } | cast seguro |
Plantillas de los ejercicios
Hija que amplía un método del padre (Perro, Cocinero, Encargado):
@Override
public String presentarse() {
return super.presentarse() + " y es un " + raza;
}
Toby → Toby (3 años) dice ¡Guau! y es un beagle
Método final que se apoya en uno abstracto (Figura):
public abstract double area();
public final boolean esMayorQue(Figura otra) {
return area() > otra.area();
}
Ordenadas: 3,14 → 6,00 → 7,07 → 12,00
Recorrido polimórfico + instanceof (Plantilla):
for (int i = 0; i < numTrabajadores; i++) {
total += trabajadores[i].costeSemanal(); // cada uno su fórmula
if (trabajadores[i] instanceof Camarero c) propinas += c.getPropinas();
}
Coste → 1670,00 € · propinas de sala → 55,50 €
Errores que más se repiten
| Síntoma | Causa probable |
|---|---|
constructor X in class X cannot be applied to given types | El padre no tiene constructor vacío y no llamaste a super(...) |
call to super must be first statement in constructor | Hay código antes de super(...) |
X has private access in Padre | El atributo es private: usa el getter o hazlo protected |
method does not override or implement a method from a supertype | Nombre o parámetros distintos a los del padre |
X is not abstract and does not override abstract method… | La hija no implementa un método abstracto |
cannot find symbol al llamar a un método de la hija | La variable es del tipo padre: comprueba con instanceof |
ClassCastException | Cast hacia abajo sin comprobar el tipo real |
StackOverflowError en un método sobrescrito | Falta super. y el método se llama a sí mismo |
| Se ejecuta la versión del padre y no la tuya | No sobrescribiste: hay un error de nombre o de parámetros (sin @Override) |
Antes de entregar, pregúntate
- ¿Cada
extendspasa la prueba del «es un»? Si es «tiene un», es composición, no herencia. - ¿Todos los constructores de las hijas llaman a
super(...)con los datos del padre en la primera línea? - ¿He puesto
@Overrideen todo lo que sobrescribo, ysuper.cuando amplío en vez de sustituir? - ¿Cada cast hacia abajo va protegido por un
instanceof? ¿Podría ser un método sobrescrito en su lugar?
¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
