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:
- Crea una clase
Inventarioque contenga unaList<Producto>(usa unArrayList). - 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 suid.consultarProducto(String nombre): Busca y devuelve el primer producto que coincida con el nombre.listarInventario(): Imprime todos los productos del inventario en la consola.
- En una clase
Main, instancia unInventario, 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":
- 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
Listes más eficiente y por qué? - 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
Listelegirías y por qué? - 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
Listes más adecuada para estas operacionesaddFirst/removeFirsty 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:
- Crea una clase
Camionque simule la carga de pallets. Debe tener unaDeque<String>para almacenar los identificadores de los pallets. - Implementa los siguientes métodos:
cargarPallet(String idPallet): Usapush()para añadir un pallet a la "cima" de la carga.descargarPallet(): Usapop()para quitar y devolver el último pallet cargado. Debe imprimir un mensaje si el camión ya está vacío.verProximoPallet(): Usapeek()para ver cuál es el siguiente pallet a descargar sin quitarlo.
- 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:
- Crea una clase
LineaEmpaquetadoque gestione una cola de pedidos pendientes. Usa unaQueue<String>(conLinkedListcomo implementación) para almacenar los números de pedido. - Implementa los siguientes métodos:
nuevoPedido(String numeroPedido): Usaoffer()para añadir un pedido al final de la cola.procesarSiguientePedido(): Usapoll()para obtener y eliminar el primer pedido de la cola. Debe devolver el número de pedido procesado.verSiguientePedido(): Usapeek()para ver cuál es el próximo pedido a procesar sin quitarlo de la cola.
- 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:
- Crea una clase
ControlStockque utilice unMap<String, Integer>para almacenar el stock. La clave será el ID del producto (String) y el valor será la cantidad (Integer). - Implementa los siguientes métodos:
actualizarStock(String idProducto, int cantidad): Usaput()para añadir o actualizar el stock de un producto.obtenerStock(String idProducto): UsagetOrDefault()para devolver el stock de un producto. Si el producto no existe, debe devolver 0.imprimirStock(): Recorre el mapa (usandoentrySet()) y muestra el stock de cada producto.
- 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:
- Crea una clase
ZonaDeCargacon unSet<Contenedor>para almacenar los contenedores que están listos para ser enviados. - Implementa un método
registrarContenedor(Contenedor c). Este método debe usaradd()para añadir un contenedor alSet. El método debe imprimir si el contenedor fue añadido con éxito o si ya existía (puedes saberlo por el booleano que devuelveadd()). - En
Main, crea varios objetosContenedor. Asegúrate de crear al menos dos objetos que sean "iguales" (mismo ID pero pueden tener diferente origen o ser instancias distintas). - Intenta registrar todos los contenedores en la
ZonaDeCargay 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:
- Ejecuta el código y observa la
ConcurrentModificationException. - Explica en una o dos frases por qué ocurre este error.
- Reescribe el bucle utilizando un
Iteratory su métodoremove()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:
- Modifica la clase
Productopara que implementeComparable<Producto>. - Implementa el método
compareTo(Producto otro). El orden natural de los productos en "LogiTrack" se define por sunombre, alfabéticamente. - En una clase
Main, crea unaList<Producto>y añádele varios productos en desorden. - Imprime la lista para ver el orden original.
- Usa
Collections.sort(miLista)para ordenar la lista. -
Vuelve a imprimir la lista para verificar que los productos ahora están ordenados por nombre.
-
Pista: La clase
Stringya implementaComparable, 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:
- En tu clase
Main, tienes una lista deProductos. El equipo de logística necesita poder ordenarla de varias maneras. - Crea una clase que implemente
Comparator<Producto>para ordenar los productos porcantidadde stock, de mayor a menor (descendente). - Usa una expresión lambda para ordenar la misma lista por
precio, de menor a mayor (ascendente). - Usa el método estático
Comparator.comparing()para crear un comparador que ordene poridy muéstralo por pantalla. -
Por cada criterio de ordenación, imprime la lista para verificar que el orden es correcto.
-
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:
- Crea una nueva clase
ControlStockOrdenado. - En esta nueva clase, cambia la declaración del
Mappara que sea unTreeMap:Map<String, Integer> stock = new TreeMap<>();. - Copia y pega los métodos
actualizarStock,obtenerStockeimprimirStockde la claseControlStockoriginal. No necesitas cambiar el código de los métodos. - En
Main, instanciaControlStockOrdenadoy añade los mismos productos que en el ejercicio 5, pero en un orden diferente (ej: empieza por "SKU-003", luego "SKU-001"). - Llama a
imprimirStock()y observa que, a diferencia delHashMap, 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:
- Crea una
List<String>con varias paradas de una ruta de reparto:["Almacén", "Cliente A", "Cliente B", "Cliente C", "Almacén"]. - Obtén un
ListIterator<String>de la lista. - Recorre hacia adelante: Usa un bucle
while (iterator.hasNext())para imprimir cada parada. - Inserta una parada extra: Después de "Cliente B", usa
iterator.add("Punto de Recogida D"). - Recorre hacia atrás: Ahora que el iterador está al final, usa un bucle
while (iterator.hasPrevious())para imprimir la ruta en orden inverso. - 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.