Laboratorio 07 — Manejo de Excepciones en Java - jorgealbertopineda78-del/java GitHub Wiki
Unidad: 7 — Manejo de Excepciones Duración estimada: 90 minutos Nivel: Intermedio-Avanzado
En los labs anteriores el sistema RPG nunca validaba situaciones imposibles:
guerrero.recibirDanio(-50); // daño negativo — ¿tiene sentido?
druida.curarAliado(null); // curar a nadie — ¿qué pasa?
arquero.atacar(); // sin flechas — ¿el sistema explota?Hasta ahora imprimíamos un mensaje de error y seguíamos. En un sistema real eso no es suficiente. Java tiene un mecanismo formal para manejar estas situaciones: las excepciones.
- Usar bloques
try-catch-finallypara manejar errores en tiempo de ejecución - Crear excepciones personalizadas que representen errores del dominio del juego
- Usar
throwythrowscorrectamente - Distinguir entre excepciones chequeadas y no chequeadas
- Registrar errores en una bitácora de combate
El motor del videojuego RPG necesita ser robusto. Si un jugador intenta una acción inválida (atacar sin recursos, curar a un aliado muerto, aplicar daño negativo), el sistema debe lanzar una excepción descriptiva, manejarla correctamente y nunca caerse.
Tu tarea es agregar manejo de excepciones a la jerarquía RPG existente.
java.lang.Exception
└── RpgException ← excepción base del juego (checked)
├── PersonajeDerrotadoException ← el personaje no puede actuar
├── RecursoInsuficienteException ← sin mana, sin flechas, etc.
└── AccionInvalidaException ← acción no permitida en este estado
java.lang.RuntimeException
└── PersonajeNuloException ← se pasó null donde no debía (unchecked)
┌──────────────────────────────────────────┐
│ «abstract» Personaje │
│ + atacar() throws RpgException │ ← ahora declara excepción
│ + recibirDanio(int) │
│ throws AccionInvalidaException │
└──────────────────┬───────────────────────┘
│ extends
┌─────────────┼──────────────┐
▼ ▼ ▼
Arquero Druida Nigromante
valida vida valida vida valida vida
y flechas y mana y mana
┌────────────────────────────────────────────┐
│ MotorCombate │
│ + ejecutarTurno(Personaje, Personaje) │
│ try { atacar / recibirDanio } │
│ catch (PersonajeDerrotadoException) │
│ catch (RecursoInsuficienteException) │
│ catch (RpgException) │
│ finally { registrar en bitácora } │
└────────────────────────────────────────────┘
Crea los siguientes archivos. Son clases pequeñas pero son la base de todo el sistema de errores.
RpgException.java — excepción base chequeada:
public class RpgException extends Exception {
public RpgException(String mensaje) {
super(mensaje);
}
public RpgException(String mensaje, Throwable causa) {
super(mensaje, causa);
}
}PersonajeDerrotadoException.java:
public class PersonajeDerrotadoException extends RpgException {
private String nombrePersonaje;
public PersonajeDerrotadoException(String nombrePersonaje) {
super("El personaje '" + nombrePersonaje +
"' está derrotado y no puede realizar esta acción.");
this.nombrePersonaje = nombrePersonaje;
}
public String getNombrePersonaje() {
return nombrePersonaje;
}
}RecursoInsuficienteException.java:
public class RecursoInsuficienteException extends RpgException {
public RecursoInsuficienteException(String recurso, int disponible) {
super("Recurso insuficiente: " + recurso +
". Disponible: " + disponible);
}
}AccionInvalidaException.java:
public class AccionInvalidaException extends RpgException {
public AccionInvalidaException(String accion, String razon) {
super("Acción inválida '" + accion + "': " + razon);
}
}PersonajeNuloException.java — excepción no chequeada:
public class PersonajeNuloException extends RuntimeException {
public PersonajeNuloException(String metodo) {
super("Se pasó un personaje nulo al método '" +
metodo + "'. Esto es un error de programación.");
}
}💡 ¿Por qué
RuntimeExceptiony noException?PersonajeNuloExceptionrepresenta un error del programador (pasarnulldonde no debía), no un error del usuario. Las excepciones no chequeadas no obligan al que llama a usartry-catch.
Modifica la clase abstracta Personaje:
Cambio 1 — atacar() ahora declara excepción:
// Antes:
public abstract void atacar();
// Ahora:
public abstract void atacar() throws RpgException;Cambio 2 — recibirDanio() valida el parámetro:
public void recibirDanio(int danio) throws AccionInvalidaException {
if (danio < 0) {
throw new AccionInvalidaException(
"recibirDanio",
"El daño no puede ser negativo: " + danio
);
}
puntosVida -= danio;
if (puntosVida <= 0) {
puntosVida = 0;
estaVivo = false;
}
System.out.println(nombre + " recibe " + danio +
" de daño. Vida: " + puntosVida);
if (!estaVivo) {
System.out.println(nombre + " ha sido derrotado.");
}
}Modifica Arquero.atacar() para validar estado y recursos:
@Override
public void atacar() throws RpgException {
if (!isEstaVivo()) {
throw new PersonajeDerrotadoException(getNombre());
}
if (flechasDisponibles <= 0) {
throw new RecursoInsuficienteException("flechas", flechasDisponibles);
}
flechasDisponibles--;
System.out.println("[" + getNombre() + "] dispara una flecha. " +
"Flechas restantes: " + flechasDisponibles);
}Crea MotorCombate.java — maneja los turnos y captura las excepciones:
import java.util.ArrayList;
public class MotorCombate {
private ArrayList<String> bitacora;
public MotorCombate() {
bitacora = new ArrayList<>();
}
public void ejecutarTurno(Personaje atacante, Personaje defensor) {
if (atacante == null) throw new PersonajeNuloException("ejecutarTurno(atacante)");
if (defensor == null) throw new PersonajeNuloException("ejecutarTurno(defensor)");
System.out.println("\n--- Turno: " + atacante.getNombre() +
" vs " + defensor.getNombre() + " ---");
try {
atacante.atacar();
int danio = atacante.calcularDanio();
defensor.recibirDanio(danio);
bitacora.add("OK | " + atacante.getNombre() +
" atacó a " + defensor.getNombre() +
" (daño: " + danio + ")");
} catch (PersonajeDerrotadoException e) {
bitacora.add("DERROTA | " + e.getNombrePersonaje() +
" no pudo actuar");
System.out.println("⚠ " + e.getMessage());
} catch (RecursoInsuficienteException e) {
bitacora.add("SIN RECURSO | " + atacante.getNombre() +
" no pudo atacar");
System.out.println("⚠ " + e.getMessage());
} catch (RpgException e) {
bitacora.add("ERROR RPG | " + e.getMessage());
System.out.println("⚠ Error de juego: " + e.getMessage());
} finally {
// Siempre se ejecuta, haya o no excepción
System.out.println("[Bitácora] Turno registrado.");
}
}
public void mostrarBitacora() {
System.out.println("\n=== Bitácora de Combate ===");
for (int i = 0; i < bitacora.size(); i++) {
System.out.println((i + 1) + ". " + bitacora.get(i));
}
}
}💡 Observa el orden de los
catch: el más específico (PersonajeDerrotadoException) va primero, el más general (RpgException) va al final. Si lo inviertes, el compilador te dará error porque el catch general atraparía todo antes que los específicos.
En Druida, modifica:
-
atacar() throws RpgException:- Verifica que esté vivo → lanza
PersonajeDerrotadoException - Verifica que tenga
mana >= 10→ lanzaRecursoInsuficienteException("mana", mana) - Si pasa las validaciones, resta
10al mana y ejecuta el ataque normal
- Verifica que esté vivo → lanza
-
curarAliado(Personaje aliado) throws RpgException:- Verifica que
aliadono seanull→ lanzaPersonajeNuloException("curarAliado") - Verifica que el aliado esté vivo → lanza
AccionInvalidaException("curarAliado", "No se puede curar a un personaje derrotado") - Si pasa las validaciones, cura normalmente
- Verifica que
En Nigromante, modifica:
-
atacar() throws RpgException:- Verifica que esté vivo → lanza
PersonajeDerrotadoException - Verifica que tenga
mana >= 15→ lanzaRecursoInsuficienteException("mana", mana) - Si pasa, resta
15al mana y ejecuta el ataque
- Verifica que esté vivo → lanza
Demuestra el manejo de excepciones en los siguientes 6 escenarios:
Escenario 1 — Turno normal sin excepción:
MotorCombate motor = new MotorCombate();
motor.ejecutarTurno(druida, nigromante);Escenario 2 — Personaje derrotado intenta atacar:
druida.recibirDanio(9999); // primero derrota al druida
motor.ejecutarTurno(druida, nigromante); // captura PersonajeDerrotadoExceptionEscenario 3 — Arquero sin flechas:
Arquero sinFlechas = new Arquero("Legolas", 6, 150, "Arco Largo", 0, 95);
motor.ejecutarTurno(sinFlechas, nigromante); // captura RecursoInsuficienteExceptionEscenario 4 — Curar aliado derrotado:
try {
druida2.curarAliado(personajeDerrotado);
} catch (RpgException e) {
System.out.println("No se pudo curar: " + e.getMessage());
}Escenario 5 — Daño negativo con finally:
try {
nigromante.recibirDanio(-50);
} catch (AccionInvalidaException e) {
System.out.println("Capturado: " + e.getMessage());
} finally {
System.out.println("El bloque finally siempre se ejecuta.");
}Escenario 6 — Mostrar bitácora completa:
motor.mostrarBitacora();arquero.atacar()
│
├─ flechasDisponibles == 0
│ │
│ └─ throw new RecursoInsuficienteException("flechas", 0)
│ │
│ sube por la pila de llamadas
│ ▼
│ MotorCombate.ejecutarTurno()
│ catch (RecursoInsuficienteException e)
│ → registra en bitácora
│ → imprime mensaje amigable
│ finally
│ → siempre se ejecuta
│
└─ el programa sigue ejecutándose normalmente ✓
=== RPG — Sistema con Manejo de Excepciones ===
--- Turno: Sylva vs Malachar ---
[Sylva] invoca raíces del bosque y ataca con furia natural.
Malachar recibe 240 de daño. Vida: 0
Malachar ha sido derrotado.
[Bitácora] Turno registrado.
--- Turno: Sylva vs Malachar ---
⚠ El personaje 'Sylva' está derrotado y no puede realizar esta acción.
[Bitácora] Turno registrado.
--- Turno: Legolas vs Malachar ---
⚠ Recurso insuficiente: flechas. Disponible: 0
[Bitácora] Turno registrado.
-- Intento de curar aliado derrotado --
No se pudo curar: Acción inválida 'curarAliado': No se puede curar a un personaje derrotado
-- Bloque manual try-catch-finally --
Capturado: Acción inválida 'recibirDanio': El daño no puede ser negativo: -50
El bloque finally siempre se ejecuta.
=== Bitácora de Combate ===
1. OK | Sylva atacó a Malachar (daño: 240)
2. DERROTA | Sylva no pudo actuar
3. SIN RECURSO | Legolas no pudo atacar
| Criterio | Puntos |
|---|---|
| Las 5 clases de excepción creadas con herencia correcta | 15 |
Personaje.atacar() y recibirDanio() declaran/lanzan excepciones |
10 |
Arquero.atacar() valida vida y flechas con excepciones correctas |
10 |
MotorCombate con try-catch-finally ordenado y bitácora funcional |
15 |
Druida.atacar() valida vida y mana, descuenta mana al atacar |
15 |
Druida.curarAliado() valida null y aliado derrotado |
15 |
Nigromante.atacar() valida vida y mana |
10 |
Main demuestra los 6 escenarios con salida coherente |
10 |
| Total | 100 |
Responde en respuestas.docx:
- ¿Cuál es la diferencia entre una excepción chequeada y una no chequeada? ¿Por qué
PersonajeNuloExceptionextiendeRuntimeExceptiony noException? - ¿Para qué sirve el bloque
finally? Da un ejemplo real de cuándo es indispensable. - ¿Cuál es la diferencia entre
throwythrows? Muestra un ejemplo de cada uno tomado de este laboratorio.
Sube al repositorio en la carpeta lab07_excepciones:
lab07_excepciones/
├── RpgException.java
├── PersonajeDerrotadoException.java
├── RecursoInsuficienteException.java
├── AccionInvalidaException.java
├── PersonajeNuloException.java
├── Hechicero.java
├── Sanador.java
├── Personaje.java
├── Arquero.java
├── Druida.java
├── Nigromante.java
├── MotorCombate.java
└── Main.java
Laboratorio preparado para el curso de Programación Orientada a Objetos — Ago-Dic 2025