Laboratorio 07 — Manejo de Excepciones en Java - jorgealbertopineda78-del/java GitHub Wiki

Laboratorio 07 — Manejo de Excepciones en Java

Unidad: 7 — Manejo de Excepciones Duración estimada: 90 minutos Nivel: Intermedio-Avanzado


Conexión con laboratorios anteriores

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.


Objetivo

  • Usar bloques try-catch-finally para manejar errores en tiempo de ejecución
  • Crear excepciones personalizadas que representen errores del dominio del juego
  • Usar throw y throws correctamente
  • Distinguir entre excepciones chequeadas y no chequeadas
  • Registrar errores en una bitácora de combate

Contexto del problema

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.


Jerarquía de excepciones del juego

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 }       │
└────────────────────────────────────────────┘

Instrucciones

Parte 1 — Excepciones personalizadas

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é RuntimeException y no Exception? PersonajeNuloException representa un error del programador (pasar null donde no debía), no un error del usuario. Las excepciones no chequeadas no obligan al que llama a usar try-catch.


Parte 2 — Modificar Personaje (en clase con el profesor)

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.");
    }
}

Parte 3 — Modificar Arquero y crear MotorCombate (en clase con el profesor)

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.


Parte 4 — Validaciones en Druida y Nigromante (trabajo independiente)

En Druida, modifica:

  • atacar() throws RpgException:

    1. Verifica que esté vivo → lanza PersonajeDerrotadoException
    2. Verifica que tenga mana >= 10 → lanza RecursoInsuficienteException("mana", mana)
    3. Si pasa las validaciones, resta 10 al mana y ejecuta el ataque normal
  • curarAliado(Personaje aliado) throws RpgException:

    1. Verifica que aliado no sea null → lanza PersonajeNuloException("curarAliado")
    2. Verifica que el aliado esté vivo → lanza AccionInvalidaException("curarAliado", "No se puede curar a un personaje derrotado")
    3. Si pasa las validaciones, cura normalmente

En Nigromante, modifica:

  • atacar() throws RpgException:
    1. Verifica que esté vivo → lanza PersonajeDerrotadoException
    2. Verifica que tenga mana >= 15 → lanza RecursoInsuficienteException("mana", mana)
    3. Si pasa, resta 15 al mana y ejecuta el ataque

Parte 5 — Clase principal Main (trabajo independiente)

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 PersonajeDerrotadoException

Escenario 3 — Arquero sin flechas:

Arquero sinFlechas = new Arquero("Legolas", 6, 150, "Arco Largo", 0, 95);
motor.ejecutarTurno(sinFlechas, nigromante);  // captura RecursoInsuficienteException

Escenario 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();

Flujo visual de una excepción

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 ✓

Ejemplo de salida esperada

=== 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

Criterios de evaluación

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

Preguntas de reflexión

Responde en respuestas.docx:

  1. ¿Cuál es la diferencia entre una excepción chequeada y una no chequeada? ¿Por qué PersonajeNuloException extiende RuntimeException y no Exception?
  2. ¿Para qué sirve el bloque finally? Da un ejemplo real de cuándo es indispensable.
  3. ¿Cuál es la diferencia entre throw y throws? Muestra un ejemplo de cada uno tomado de este laboratorio.

Entregable

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

⚠️ **GitHub.com Fallback** ⚠️