Skip to content

Ejercicios Prácticos: Estructuras de Datos Dinámicas en Java

Narrativa del Proyecto: ¡Felicidades! Has sido fichado como desarrollador Java en "LogiTrack", una startup en plena expansión que está revolucionando el sector de la logística y la gestión de inventarios. Nuestro producto estrella es un sistema de gestión de almacenes (SGA) que necesita ser robusto, eficiente y escalable. Los arrays estáticos que usaba el prototipo inicial ya no son suficientes.

Tu misión es rediseñar y construir los componentes clave del sistema utilizando el Java Collections Framework. Tendrás que gestionar inventarios de productos, procesar colas de pedidos, rastrear envíos únicos y optimizar la consulta de datos. Cada decisión que tomes sobre qué estructura de datos usar tendrá un impacto directo en el rendimiento y la fiabilidad del sistema. ¡Es hora de poner orden en el almacén!

Ejercicios de Consolidación

1) Primer Día en el Almacén: Gestionando el Inventario | Nivel: Fácil

Objetivo:

Utilizar la colección ArrayList para gestionar una lista de productos, aplicando las operaciones básicas de inserción, consulta, modificación y eliminación.

Setup Inicial:

Crea una clase Producto simple con atributos para id (String), nombre (String) y cantidad (int). Incluye un constructor y un método toString() para una fácil visualización.

Tarea a realizar:
  1. Crea una clase Inventario que contenga una List<Producto> (usa un ArrayList).
  2. Implementa los siguientes métodos en la clase Inventario:
    • agregarProducto(Producto producto): Añade un nuevo producto al final de la lista.
    • eliminarProducto(String id): Busca y elimina un producto por su id.
    • consultarProducto(String nombre): Busca y devuelve el primer producto que coincida con el nombre.
    • listarInventario(): Imprime todos los productos del inventario en la consola.
  3. En una clase Main, instancia un Inventario, añade varios productos, lista el inventario, elimina uno y vuelve a listarlo para comprobar el resultado.
Aplicación en el Mundo Real:

ArrayList es la estructura de datos por defecto para manejar listas de elementos cuyo tamaño puede variar. Es la base para mostrar listados de usuarios, catálogos de productos, historiales de transacciones y prácticamente cualquier secuencia de datos en una aplicación.

2) La Herramienta Adecuada: ¿ArrayList o LinkedList? | Nivel: Fácil

Objetivo:

Evaluar y justificar la elección entre ArrayList y LinkedList basándose en los requisitos de un caso de uso específico.

Tarea a realizar:

Responde a las siguientes preguntas teóricas, justificando tu elección para cada escenario dentro del sistema "LogiTrack":

  1. Escenario A: Necesitas implementar el historial de cambios de ubicación de un paquete. Se añadirán nuevas ubicaciones constantemente al final de la lista, y rara vez se consultará una ubicación específica por su posición. ¿Qué implementación de List es más eficiente y por qué?
  2. Escenario B: Estás construyendo una función que muestra los 10 productos más vendidos del mes en una pantalla. Los datos se cargan una vez desde la base de datos y luego se accede a ellos por su posición (índice) para mostrarlos en la interfaz gráfica. ¿Qué implementación de List elegirías y por qué?
  3. Escenario C: Debes gestionar la "cola de preparación de envíos". Los nuevos pedidos se añaden al principio de la cola (máxima prioridad) y se procesan (eliminan) también desde el principio. ¿Qué implementación de List es más adecuada para estas operaciones addFirst/removeFirst y por qué?
Aplicación en el Mundo Real:

Elegir la implementación correcta de una colección no es una cuestión menor. Una mala elección puede llevar a degradaciones significativas de rendimiento en aplicaciones a gran escala, especialmente cuando se trabaja con miles o millones de elementos.

3) Cargando el Camión: La Pila de Pallets | Nivel: Fácil

Objetivo:

Utilizar una implementación de Deque (como ArrayDeque) para simular una Pila (LIFO - Last-In, First-Out).

Tarea a realizar:
  1. Crea una clase Camion que simule la carga de pallets. Debe tener una Deque<String> para almacenar los identificadores de los pallets.
  2. Implementa los siguientes métodos:
    • cargarPallet(String idPallet): Usa push() para añadir un pallet a la "cima" de la carga.
    • descargarPallet(): Usa pop() para quitar y devolver el último pallet cargado. Debe imprimir un mensaje si el camión ya está vacío.
    • verProximoPallet(): Usa peek() para ver cuál es el siguiente pallet a descargar sin quitarlo.
  3. En una clase Main, simula el proceso: carga 3-4 pallets, muestra cuál es el próximo a descargar, y luego descarga todos los pallets uno por uno, imprimiendo el ID de cada uno a medida que se descarga.
Aplicación en el Mundo Real:

Las pilas son fundamentales en informática para gestionar llamadas a funciones (la "pila de llamadas"), analizar la sintaxis de código (parsing), implementar algoritmos de búsqueda en grafos (DFS) y, por supuesto, para la funcionalidad de "Deshacer" (Ctrl+Z) en cualquier editor.

4) Línea de Empaquetado: La Cola de Pedidos | Nivel: Fácil

Objetivo:

Utilizar una implementación de Queue (como LinkedList) para simular una Cola (FIFO - First-In, First-Out).

Tarea a realizar:
  1. Crea una clase LineaEmpaquetado que gestione una cola de pedidos pendientes. Usa una Queue<String> (con LinkedList como implementación) para almacenar los números de pedido.
  2. Implementa los siguientes métodos:
    • nuevoPedido(String numeroPedido): Usa offer() para añadir un pedido al final de la cola.
    • procesarSiguientePedido(): Usa poll() para obtener y eliminar el primer pedido de la cola. Debe devolver el número de pedido procesado.
    • verSiguientePedido(): Usa peek() para ver cuál es el próximo pedido a procesar sin quitarlo de la cola.
  3. En Main, simula el flujo: añade varios pedidos, muestra cuál es el siguiente, y luego procesa todos los pedidos en orden hasta que la cola esté vacía.
Aplicación en el Mundo Real:

Las colas son omnipresentes en sistemas concurrentes y de procesamiento de tareas. Se usan para gestionar colas de impresión, solicitudes de red a un servidor, procesamiento de mensajes en sistemas de mensajería (como RabbitMQ o Kafka), y en algoritmos de búsqueda en grafos (BFS).

5) Control de Stock: HashMap al Rescate | Nivel: Fácil

Objetivo:

Utilizar un HashMap para implementar un sistema de control de stock eficiente, asociando un identificador de producto (clave) con su cantidad (valor).

Tarea a realizar:
  1. Crea una clase ControlStock que utilice un Map<String, Integer> para almacenar el stock. La clave será el ID del producto (String) y el valor será la cantidad (Integer).
  2. Implementa los siguientes métodos:
    • actualizarStock(String idProducto, int cantidad): Usa put() para añadir o actualizar el stock de un producto.
    • obtenerStock(String idProducto): Usa getOrDefault() para devolver el stock de un producto. Si el producto no existe, debe devolver 0.
    • imprimirStock(): Recorre el mapa (usando entrySet()) y muestra el stock de cada producto.
  3. En Main, añade varios productos al control de stock, actualiza la cantidad de uno de ellos, consulta el stock de un producto existente y de uno inexistente, y finalmente imprime el stock completo.
Aplicación en el Mundo Real:

HashMap es posiblemente la estructura de datos más útil para un acceso rápido a la información. Se usa para cachés en memoria (guardar datos de la BBDD para no tener que consultarla constantemente), para almacenar datos de sesión de usuario, para representar objetos JSON, y en general, para cualquier situación que requiera una búsqueda de datos por un identificador único.

6) Envíos Únicos: Registrando Contenedores con HashSet | Nivel: Fácil

Objetivo:

Utilizar un HashSet para garantizar que no se registren identificadores de contenedores de envío duplicados y comprender la importancia de equals() y hashCode().

Setup Inicial:

Crea una clase Contenedor con un id (String) y un origen (String). Sobrescribe correctamente los métodos equals() y hashCode() para que dos contenedores se consideren iguales si su id es el mismo.

Tarea a realizar:
  1. Crea una clase ZonaDeCarga con un Set<Contenedor> para almacenar los contenedores que están listos para ser enviados.
  2. Implementa un método registrarContenedor(Contenedor c). Este método debe usar add() para añadir un contenedor al Set. El método debe imprimir si el contenedor fue añadido con éxito o si ya existía (puedes saberlo por el booleano que devuelve add()).
  3. En Main, crea varios objetos Contenedor. Asegúrate de crear al menos dos objetos que sean "iguales" (mismo ID pero pueden tener diferente origen o ser instancias distintas).
  4. Intenta registrar todos los contenedores en la ZonaDeCarga y observa la salida. Comprueba que el duplicado no se añade.
Aplicación en el Mundo Real:

HashSet es ideal para mantener colecciones de elementos únicos donde el orden no importa. Se utiliza para detectar elementos duplicados en grandes conjuntos de datos, para gestionar listas de usuarios únicos que han dado "like" a una publicación, o para realizar operaciones de conjuntos como uniones, intersecciones y diferencias de forma muy eficiente.

7) ¡Bug en el Reparto! La ConcurrentModificationException | Nivel: Fácil

Objetivo:

Identificar y corregir el error común ConcurrentModificationException que ocurre al intentar modificar una colección mientras se recorre con un bucle for-each.

Setup Inicial (Código con Bug):

El siguiente código intenta eliminar de una lista de paquetes todos aquellos cuyo destino sea "Zona Fantasma". Sin embargo, lanza una excepción.

import java.util.ArrayList;
import java.util.List;

public class Repartidor {
    public static void main(String[] args) {
        List<String> paquetes = new ArrayList<>(List.of(
            "Paquete a Madrid",
            "Paquete a Zona Fantasma",
            "Paquete a Barcelona",
            "Paquete a Sevilla",
            "Paquete a Zona Fantasma"
        ));

        System.out.println("Lista de paquetes inicial: " + paquetes);

        // Código con bug
        for (String paquete : paquetes) {
            if (paquete.contains("Zona Fantasma")) {
                paquetes.remove(paquete); // ¡ERROR! No se puede modificar así
            }
        }

        System.out.println("Lista de paquetes final: " + paquetes);
    }
}
Tarea a realizar:
  1. Ejecuta el código y observa la ConcurrentModificationException.
  2. Explica en una o dos frases por qué ocurre este error.
  3. Reescribe el bucle utilizando un Iterator y su método remove() para eliminar los elementos de forma segura y conseguir que el programa funcione correctamente.
Aplicación en el Mundo Real:

Este es uno de los errores más clásicos al empezar a trabajar con colecciones en Java. Entender el "fallo rápido" (fail-fast) de los iteradores y saber cómo modificar colecciones durante la iteración es una habilidad fundamental para evitar bugs sutiles y difíciles de rastrear en aplicaciones complejas.

Ejercicios de Refuerzo

8) Orden Natural: Productos Comparables | Nivel: Medio

Objetivo:

Implementar la interfaz Comparable para definir un "orden natural" para los objetos de una clase personalizada y utilizarlo para ordenar una colección.

Setup Inicial:

Usa la clase Producto del ejercicio 1. Añádele un atributo double precio.

Tarea a realizar:
  1. Modifica la clase Producto para que implemente Comparable<Producto>.
  2. Implementa el método compareTo(Producto otro). El orden natural de los productos en "LogiTrack" se define por su nombre, alfabéticamente.
  3. En una clase Main, crea una List<Producto> y añádele varios productos en desorden.
  4. Imprime la lista para ver el orden original.
  5. Usa Collections.sort(miLista) para ordenar la lista.
  6. Vuelve a imprimir la lista para verificar que los productos ahora están ordenados por nombre.

  7. Pista: La clase String ya implementa Comparable, así que puedes delegar la comparación directamente a los nombres: this.nombre.compareTo(otro.getNombre()).

Aplicación en el Mundo Real:

Definir un orden natural es útil para que las clases "sepan" cómo ordenarse por defecto. Estructuras como TreeSet y TreeMap dependen de ello para mantener sus elementos ordenados automáticamente, y métodos como Collections.sort() o Arrays.sort() lo usan como criterio de ordenación principal.

9) Orden a la Carta: Comparator para Múltiples Criterios | Nivel: Medio

Objetivo:

Utilizar la interfaz Comparator para definir múltiples criterios de ordenación externos a una clase, incluyendo el uso de expresiones lambda.

Setup Inicial:

Usa la clase Producto del ejercicio anterior (con id, nombre, cantidad, precio).

Tarea a realizar:
  1. En tu clase Main, tienes una lista de Productos. El equipo de logística necesita poder ordenarla de varias maneras.
  2. Crea una clase que implemente Comparator<Producto> para ordenar los productos por cantidad de stock, de mayor a menor (descendente).
  3. Usa una expresión lambda para ordenar la misma lista por precio, de menor a mayor (ascendente).
  4. Usa el método estático Comparator.comparing() para crear un comparador que ordene por id y muéstralo por pantalla.
  5. Por cada criterio de ordenación, imprime la lista para verificar que el orden es correcto.

  6. Pista para el comparador por cantidad (descendente): Integer.compare(p2.getCantidad(), p1.getCantidad()) invierte el orden de los parámetros para lograr un orden descendente.

Aplicación en el Mundo Real:

En aplicaciones reales, es muy raro que baste con un único criterio de ordenación. Los usuarios esperan poder ordenar tablas y listados por diferentes columnas (nombre, fecha, precio, etc.). Comparator es la herramienta estándar de Java para implementar esta funcionalidad de forma limpia y desacoplada.

10) Informes Ordenados: El Poder del TreeMap | Nivel: Medio

Objetivo:

Refactorizar un HashMap a un TreeMap para aprovechar su capacidad de mantener las claves ordenadas automáticamente.

Setup Inicial:

Usa la clase ControlStock del ejercicio 5, que utilizaba un HashMap.

Tarea a realizar:
  1. Crea una nueva clase ControlStockOrdenado.
  2. En esta nueva clase, cambia la declaración del Map para que sea un TreeMap: Map<String, Integer> stock = new TreeMap<>();.
  3. Copia y pega los métodos actualizarStock, obtenerStock e imprimirStock de la clase ControlStock original. No necesitas cambiar el código de los métodos.
  4. En Main, instancia ControlStockOrdenado y añade los mismos productos que en el ejercicio 5, pero en un orden diferente (ej: empieza por "SKU-003", luego "SKU-001").
  5. Llama a imprimirStock() y observa que, a diferencia del HashMap, la salida está siempre ordenada por la clave (ID del producto).
Aplicación en el Mundo Real:

TreeMap es la elección perfecta cuando se necesita tanto la búsqueda rápida de un mapa como la garantía de que los datos se pueden recorrer en un orden predecible. Es ideal para generar informes, índices de libros, directorios telefónicos o cualquier visualización de datos que deba presentarse ordenada por su clave.

11) Ruta de Reparto Interactiva: ListIterator | Nivel: Medio

Objetivo:

Utilizar un ListIterator para recorrer una lista en ambas direcciones y modificarla durante la iteración.

Tarea a realizar:
  1. Crea una List<String> con varias paradas de una ruta de reparto: ["Almacén", "Cliente A", "Cliente B", "Cliente C", "Almacén"].
  2. Obtén un ListIterator<String> de la lista.
  3. Recorre hacia adelante: Usa un bucle while (iterator.hasNext()) para imprimir cada parada.
  4. Inserta una parada extra: Después de "Cliente B", usa iterator.add("Punto de Recogida D").
  5. Recorre hacia atrás: Ahora que el iterador está al final, usa un bucle while (iterator.hasPrevious()) para imprimir la ruta en orden inverso.
  6. Imprime la lista final para ver que la nueva parada se ha añadido correctamente.
Aplicación en el Mundo Real:

ListIterator es útil en escenarios donde se necesita una navegación bidireccional, como en un editor de texto (moverse por caracteres o palabras hacia adelante y atrás), un historial de navegador, o para algoritmos que necesitan inspeccionar elementos vecinos en una lista para realizar modificaciones complejas.

Ejercicios de Ampliación

12) Diseño y Revisión por Pares: Sistema de Trazabilidad de Envíos | Nivel: Alto

Objetivo:

Diseñar una solución de software de pequeña escala que combine múltiples estructuras de datos, documentarla con un diagrama de clases y realizar una revisión por pares (Peer Review) para mejorar el diseño.

Tarea a realizar:

"LogiTrack" necesita un sistema para la trazabilidad de envíos. Un envío tiene un ID único y pasa por varios estados a lo largo de su ciclo de vida (ej: "Registrado", "En almacén", "En tránsito", "Entregado").

Parte 1: Tu Diseño (Individual) 1. Diseña la arquitectura de clases para este módulo. Piensa en las clases que necesitas: Envio, RegistroEstado, SistemaTrazabilidad, etc. 2. Elige las estructuras de datos más adecuadas para los siguientes requisitos: * El SistemaTrazabilidad debe poder buscar un Envio por su ID de forma muy rápida. * Cada Envio debe mantener un historial de sus cambios de estado, en el orden en que ocurrieron. * El sistema necesita mantener una cola de envíos que están pendientes de ser procesados por el personal del almacén. 3. Crea un diagrama de clases en Mermaid que represente tu diseño, mostrando las clases, sus atributos principales y las relaciones entre ellas. 4. Escribe una justificación (2-3 párrafos) explicando tus decisiones sobre las estructuras de datos. Por ejemplo: "Para el acceso rápido por ID, he elegido un HashMap<String, Envio>... Para el historial de estados, he usado un ArrayList<RegistroEstado> porque el orden es crucial...".

Parte 2: Revisión por Pares (En parejas) 1. Intercambia tu diagrama y justificación con un compañero. 2. Analiza su diseño y proporciona feedback constructivo por escrito sobre al menos dos puntos. Céntrate en la elección y el uso de las estructuras de datos. Ejemplos de buen feedback: * "Has usado un ArrayList para la cola de procesamiento. ¿Has considerado que un LinkedList o ArrayDeque sería más performante para las operaciones de encolar y desencolar?" * "Tu HashMap para los envíos es una buena elección. Como mejora, podrías considerar un LinkedHashMap si el sistema necesitara mostrar los envíos en el orden en que fueron registrados." 3. Recibe el feedback de tu compañero y reflexiona sobre cómo podrías mejorar tu diseño inicial.

Aplicación en el Mundo Real:

Esta tarea simula el día a día de un desarrollador de software: analizar requisitos, diseñar una solución, elegir las herramientas (estructuras de datos) adecuadas, documentar el diseño y, fundamentalmente, colaborar con el equipo a través de revisiones de código y diseño para construir un producto de mayor calidad.