Saltar al contenido
ES EN

Ejercicio resuelto: inventario con clases, TreeMap y excepciones en Java

Ejercicio de examen de Java con clases, colecciones y excepciones propias: inventario de almacén con movimientos validados, resuelto y explicado.

Actualizado el

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

Código de Examen: Ejercicio resuelto: inventario con clases, TreeMap y excepciones en Java
Programación Orientada a Objetos · Examen finalEjercicio 2 · 3,0 puntos · 35 min

Una pequeña empresa gestiona el inventario de su almacén. Debes escribir un programa Java que lea el estado inicial del almacén y una lista de movimientos, los aplique uno a uno y muestre el inventario final.

Modela el problema con al menos estas piezas: una clase Articulo con código, nombre y existencias (atributos privados), con los métodos entrada(int) y salida(int); una excepción propia comprobada MovimientoInvalidoException que extienda Exception; y una colección TreeMap<String, Articulo> indexada por código para que el listado final salga ordenado alfabéticamente por código.

Entrada. La primera línea contiene un entero N, el número de artículos. Siguen N líneas con tres campos separados por espacios: código, nombre (una sola palabra) y existencias iniciales (entero no negativo). Después viene una línea con un entero M, el número de movimientos, y M líneas con el formato TIPO codigo cantidad, donde TIPO es ENTRADA o SALIDA. Las líneas en blanco, en cualquier posición, deben ignorarse y no cuentan como líneas de datos.

Reglas. Un movimiento es inválido, y debe rechazarse sin alterar el inventario, en estos casos, comprobados en este orden: tipo distinto de ENTRADA y SALIDA (motivo tipo desconocido); código que no existe en el inventario (articulo inexistente); cantidad menor o igual que cero (cantidad no valida); una SALIDA que dejaría las existencias por debajo de cero (existencias insuficientes). Los movimientos se numeran desde 1 según su orden de aparición.

Salida. Por cada movimiento rechazado, en orden, una línea Movimiento k: motivo. Después la línea Inventario:, seguida de una línea por artículo, ordenados por código, con el formato codigo nombre existencias. Por último la línea Movimientos aplicados: a, rechazados: r. Un movimiento rechazado nunca debe interrumpir el procesamiento de los siguientes.

Entrada de ejemplo

3
A-1 tornillos 10
B-2 tuercas 5
C-3 cubetas 0
4
ENTRADA A-1 5
SALIDA B-2 7
SALIDA C-3 0
SALIDA B-2 5

Salida esperada

Movimiento 2: existencias insuficientes
Movimiento 3: cantidad no valida
Inventario:
A-1 tornillos 15
B-2 tuercas 0
C-3 cubetas 0
Movimientos aplicados: 2, rechazados: 2
Clase Articulo bien encapsulada (atributos privados, métodos entrada y salida con validación)
0,75
Excepción propia comprobada MovimientoInvalidoException y su lanzamiento en cada caso de error
0,75
Uso correcto de una colección ordenada por código (TreeMap) y búsqueda de artículos
0,75
Lectura de la entrada, captura de excepciones sin abortar el proceso y formato exacto de la salida
0,75

Pistas

Pista 1 · ¿Dónde se lanza y dónde se captura la excepción?

Las comprobaciones de validez pertenecen a Articulo (cantidad y existencias), que lanza MovimientoInvalidoException con el motivo como mensaje. La captura se hace en el bucle del main, dentro del try de cada movimiento, para que un error no corte el bucle.

Pista 2 · ¿Qué orden siguen las comprobaciones?

El orden de la lista de reglas importa: antes de buscar el artículo hay que descartar el tipo desconocido, y solo después de encontrarlo se valida la cantidad. Un código inexistente con cantidad cero debe dar articulo inexistente, no cantidad no valida.

Pista 3 · ¿Cómo ignorar las líneas en blanco?

Lee todas las líneas de la entrada, descarta las que estén vacías tras trim() y trabaja sobre la lista resultante con un índice: primero N, luego N artículos, luego M y luego M movimientos.

Solución

Solución explicada

La solución separa responsabilidades: Articulo protege su propio estado y lanza la excepción cuando una operación no es válida; el main orquesta la lectura, el cuenta de aplicados y rechazados y la impresión final.

Se usa un TreeMap porque el enunciado exige listar por código ordenado y además necesitamos búsqueda por clave. Con un HashMap habría que ordenar después.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class Main {

    static class MovimientoInvalidoException extends Exception {
        public MovimientoInvalidoException(String motivo) {
            super(motivo);
        }
    }

    static class Articulo {
        private final String codigo;
        private final String nombre;
        private int existencias;

        Articulo(String codigo, String nombre, int existencias) {
            this.codigo = codigo;
            this.nombre = nombre;
            this.existencias = existencias;
        }

        void entrada(int cantidad) throws MovimientoInvalidoException {
            if (cantidad <= 0) {
                throw new MovimientoInvalidoException("cantidad no valida");
            }
            existencias += cantidad;
        }

        void salida(int cantidad) throws MovimientoInvalidoException {
            if (cantidad <= 0) {
                throw new MovimientoInvalidoException("cantidad no valida");
            }
            if (cantidad > existencias) {
                throw new MovimientoInvalidoException("existencias insuficientes");
            }
            existencias -= cantidad;
        }

        @Override
        public String toString() {
            return codigo + " " + nombre + " " + existencias;
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        List<String> lineas = new ArrayList<>();
        String linea;
        while ((linea = br.readLine()) != null) {
            if (!linea.trim().isEmpty()) {
                lineas.add(linea.trim());
            }
        }

        int pos = 0;
        int n = Integer.parseInt(lineas.get(pos++));
        Map<String, Articulo> inventario = new TreeMap<>();
        for (int i = 0; i < n; i++) {
            String[] p = lineas.get(pos++).split("\\s+");
            inventario.put(p[0], new Articulo(p[0], p[1], Integer.parseInt(p[2])));
        }

        int m = Integer.parseInt(lineas.get(pos++));
        int aplicados = 0;
        int rechazados = 0;
        StringBuilder errores = new StringBuilder();

        for (int k = 1; k <= m; k++) {
            String[] p = lineas.get(pos++).split("\\s+");
            try {
                String tipo = p[0];
                if (!tipo.equals("ENTRADA") && !tipo.equals("SALIDA")) {
                    throw new MovimientoInvalidoException("tipo desconocido");
                }
                Articulo a = inventario.get(p[1]);
                if (a == null) {
                    throw new MovimientoInvalidoException("articulo inexistente");
                }
                int cantidad = Integer.parseInt(p[2]);
                if (tipo.equals("ENTRADA")) {
                    a.entrada(cantidad);
                } else {
                    a.salida(cantidad);
                }
                aplicados++;
            } catch (MovimientoInvalidoException e) {
                rechazados++;
                errores.append("Movimiento ").append(k).append(": ")
                       .append(e.getMessage()).append("\n");
            }
        }

        StringBuilder out = new StringBuilder();
        out.append(errores);
        out.append("Inventario:\n");
        for (Articulo a : inventario.values()) {
            out.append(a).append("\n");
        }
        out.append("Movimientos aplicados: ").append(aplicados)
           .append(", rechazados: ").append(rechazados).append("\n");
        System.out.print(out);
    }
}

La clase anidada MovimientoInvalidoException extiende Exception, de modo que es comprobada: el compilador obliga a declarar throws en entrada y salida y a capturarla en main. El motivo viaja en el mensaje y se recupera con getMessage().

En Articulo, los atributos son privados y solo se modifican a través de entrada y salida. Ambos métodos validan primero la cantidad; salida además comprueba que no se supere existencias. Si lanzan la excepción, el estado no cambia, porque la resta o la suma se hace después de las comprobaciones.

En main se leen todas las líneas y se descartan las vacías, así el índice pos avanza solo sobre líneas de datos. Esto resuelve el caso de líneas en blanco del test «Precedencia y líneas en blanco», donde tras N y entre movimientos aparecen líneas vacías.

El bucle por k aplica el orden de comprobaciones del enunciado: primero el tipo, luego la búsqueda en el TreeMap y por último la cantidad, que valida el propio artículo. Así, en el test 4, ENTRADA X-1 2 da articulo inexistente y DEVOLVER A-1 1 da tipo desconocido, mientras que ENTRADA A-1 6 se aplica y deja A-1 en 10.

Los mensajes de error se acumulan en errores y se imprimen antes del inventario, tal como pide el formato. En el ejemplo, el movimiento 2 (SALIDA 7 sobre 5 existencias) se rechaza por insuficiencia y el 3 (cantidad 0) por cantidad no válida; los movimientos 1 y 4 se aplican, y el recuento final es 2 aplicados y 2 rechazados.

Al recorrer inventario.values() el TreeMap entrega los artículos ordenados por código sin trabajo extra. En el test sin movimientos, B-1 se introdujo antes que A-1 y aun así la salida muestra primero A-1.

Casos de prueba

CasoEntradaSalida esperadaSalida realResultado
Ejemplo del enunciado3 A-1 tornillos 10 B-2 tuercas 5 C-3 cubetas 0 4 ENTRADA A-1 5 SALIDA B-2 7 SALIDA C-3 0 SALIDA B-2 5Movimiento 2: existencias insuficientes Movimiento 3: cantidad no valida Inventario: A-1 tornillos 15 B-2 tuercas 0 C-3 cubetas 0 Movimientos aplicados: 2, rechazados: 2Movimiento 2: existencias insuficientes Movimiento 3: cantidad no valida Inventario: A-1 tornillos 15 B-2 tuercas 0 C-3 cubetas 0 Movimientos aplicados: 2, rechazados: 2OK
Sin movimientos y orden por código2 B-1 clavos 3 A-1 sierras 1 0Inventario: A-1 sierras 1 B-1 clavos 3 Movimientos aplicados: 0, rechazados: 0Inventario: A-1 sierras 1 B-1 clavos 3 Movimientos aplicados: 0, rechazados: 0OK
Precedencia y líneas en blanco2 C-3 cubetas 2 D-4 embudos 0 5 SALIDA Z-9 1 SALIDA D-4 1 ENTRADA C-3 3 SALIDA C-3 5 SALIDA D-4 2Movimiento 1: articulo inexistente Movimiento 2: existencias insuficientes Movimiento 5: existencias insuficientes Inventario: C-3 cubetas 0 D-4 embudos 0 Movimientos aplicados: 2, rechazados: 3Movimiento 1: articulo inexistente Movimiento 2: existencias insuficientes Movimiento 5: existencias insuficientes Inventario: C-3 cubetas 0 D-4 embudos 0 Movimientos aplicados: 2, rechazados: 3OK
Tipo desconocido y código inexistente1 A-1 pilas 4 3 ENTRADA X-1 2 DEVOLVER A-1 1 ENTRADA A-1 6Movimiento 1: articulo inexistente Movimiento 2: tipo desconocido Inventario: A-1 pilas 10 Movimientos aplicados: 1, rechazados: 2Movimiento 1: articulo inexistente Movimiento 2: tipo desconocido Inventario: A-1 pilas 10 Movimientos aplicados: 1, rechazados: 2OK

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

Complejidad

Leer y filtrar las líneas es O(L) con L el número de líneas. Cada inserción y búsqueda en el TreeMap cuesta O(log N), por lo que el procesamiento total es O(N log N + M log N). El recorrido final del inventario es O(N).

La memoria es O(N + L): el mapa guarda N artículos y la lista conserva las líneas leídas. Para entradas de examen es irrelevante, pero si los movimientos fueran millones convendría procesarlos en streaming sin guardar todas las líneas.

Errores típicos

  • Declarar la excepción como RuntimeException y no capturarla, o capturar Exception genérica: se pierde la distinción de motivos y se ocultan errores reales como un NumberFormatException.
  • Modificar las existencias antes de validar, de forma que un movimiento rechazado deje el inventario alterado.
  • Usar HashMap y olvidar ordenar el listado final, con lo que la salida depende del orden interno de la tabla hash.
  • Alterar el orden de las comprobaciones: validar la cantidad antes de buscar el artículo cambia el motivo informado en códigos inexistentes con cantidad cero.
  • No ignorar las líneas en blanco y tratar una línea vacía como un movimiento, lo que provoca un ArrayIndexOutOfBoundsException.
  • Numerar los movimientos desde 0 en lugar de desde 1, o contar solo los aplicados al numerar.

Variantes

Añadir un movimiento TRASPASO entre dos artículos

Se añade el tipo TRASPASO origen destino cantidad. Para que sea atómico, primero se valida que ambos códigos existen y que el origen tiene existencias suficientes llamando a salida sobre el origen; solo si no lanza excepción se llama a entrada sobre el destino. Si el destino fallara, habría que revertir la salida, por lo que conviene comprobar todo antes de modificar nada.

Sustituir la excepción comprobada por una no comprobada

Si MovimientoInvalidoException extiende RuntimeException, desaparecen los throws de entrada y salida, y el catch del main sigue funcionando. El coste es que el compilador ya no obliga a tratar el error, por lo que la validación depende de la disciplina del programador.

En los temarios

El contenido de este ejercicio (clases con encapsulación, colecciones y tratamiento de errores) encaja con el enfoque de asignaturas de programación orientada a objetos. El curso ISIS1209 APO II de la Universidad de los Andes incluye en su descripción el estudio de mecanismos de reutilización de clases y de definición de contratos, y la Pontificia Universidad Católica de Chile (IIC2113) menciona entre sus objetivos el diseño y la programación orientados a objetos con pericia.

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 · Revisado por Jose Santamaria

Kernel

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

«Un movimiento rechazado no debe tocar el inventario ni cortar el bucle: valida primero, modifica después.»

Comentarios