Las colecciones son estructuras de datos avanzadas que pueden cambiar de tamaño dinámicamente. Todas las colecciones principales derivan de la interfaz base java.util.Collection.
Principales métodos comunes de la interfaz Collection:
int size(): Devuelve el número de elementos.boolean isEmpty(): Indica si está vacía.boolean contains (Object element): retorna verdadero si la colección tiene el elemento pasado como parámetro.boolena add(Object element): Añade un elemento.boolean remove(Object elementt): Elimina un elemento.void clear(): Vacía la colección.
Las colecciones en Java hacen uso intensivo de los genéricos (definidos entre <>), los cuales permiten especificar el tipo de datos que almacenará la colección en tiempo de compilación.
Las colecciones solo pueden almacenar objetos (tipos por referencia). Está estrictamente prohibido utilizar tipos primitivos (int, double, char, boolean, etc.) como parámetros de tipo genérico.
Para trabajar con valores primitivos dentro de una colección, es obligatorio utilizar sus correspondientes clases de envoltorio (Integer, Double, Character, Boolean, etc.).
Gracias al Autoboxing y Unboxing automático de Java, la conversión entre el tipo primitivo y el objeto envoltorio se realiza de forma transparente:
List<Integer> numeros = new ArrayList<>(); numeros.add(10); // Autoboxing: el int '10' se convierte automáticamente en Integer numeros.add(20); int primerNumero = numeros.get(0); // Unboxing: el Integer se convierte a int primitivo
Una de las mejores prácticas de diseño en Java es el principio de programar hacia una interfaz, no hacia una implementación.
Al crear una colección, se debe declarar la variable utilizando el tipo de la interfaz (List, Set), e instanciar la clase concreta deseada (ArrayList, HashSet).
// No recomendado: Acopla el código a una implementación específica ArrayList nombres = new ArrayList<>();
Declarar las variables utilizando la interfaz permite cambiar la implementación subyacente en una sola línea de código sin modificar el resto de la aplicación que consume la colección:
List nombres = new ArrayList<>();
nombres.add("Ana");
nombres.add("Carlos");
IO.println(nombres.get(0));
Un Set es una colección que NO permite elementos duplicados.
Implementaciones más comunes:
HashSet: Muy rápido. No garantiza ningún orden de los elementos.LinkedHashSet: Rápido. Mantiene el orden en el que se insertaron los elementos.TreeSet: Ordena los elementos automáticamente según su valor.
Set<Integer> numbers = new HashSet<Integer>(); //Una práctica habitual es definir el tipo de datos conjunto como la interfaz genérica (Set) numbers.add(10); numbers.add(5); numbers.add(10); // Devuelve false y NO se añade porque está duplicado
A partir de Java 9, se pueden crear conjuntos de forma estática mediante el método factory Set.of():
Set<Integer> numbers = Set.of(2, 10, 3, 23, 99);
of(), el conjunto que se crea es inmutable, es decir, no podemos cambiarlo, con lo que no podremos añadir, eliminar ni modificar elementos.
Para recorrer un conjunto se utiliza habitualmente un bucle for-each:
for (Integer number : numbers) {
IO.println(number);
}
Un List es una colección ordenada que SÍ permite elementos duplicados y cuenta con acceso posicional mediante índice (comenzando siempre desde $0$).
Implementaciones principales:
ArrayList: Basada en un array interno redimensionable. Muy eficiente para consultar elementos por índice (get).LinkedList: Lista doblemente enlazada. Muy eficiente al realizar inserciones o eliminaciones frecuentes en posiciones intermedias.
Métodos clave de la interfaz List:
E get(int index): Obtiene un elemento partiendo de su posición (index).E set(int index, E element): Cambia el elemento almacenado en una posición de la lista (index), por otro (element).void add(int index, E element): Inserta un elemento (element) en la lista en una posición concreta (index), desplazando los existentes. Si le pasamos solo el elemento (element) la inserción la hará al final de la lista.E remove(int index): Elimina un elemento indicando su posición (index) en la lista.boolean addAll(int index, Collection<? extends E> c): Inserta una colección pasada por parámetro en una posición de la lista, desplazando el resto de elementos.int indexOf(Object o): Devuelve la posición de un elemento en la lista o $-1$ si el elemento no está en la lista.int lastIndexOf(Object o): Devuelve la última ocurrencia del objeto en la lista (dado que la lista si puede almacenar duplicados) o $-1$ si el elemento no está en la lista.List<E> subList(int from, int to): Genera una sublista (una vista parcial de la lista) con los elementos comprendidos entre la posición inicial (from, incluida) y la posición final (to, no incluida).Las listas se utilizan de forma muy parecida a los conjuntos:
List<Integer> numbers = new ArrayList<>();
numbers.add(1); // Añade un elemento al final de la lista.
numbers.add(3); // Añade otro elemento al final de la lista.
numbers.add(1,2); // Añade en la posición 1 el elemento 2.
numbers.add(numbers.get(1)+numbers.get(2)); // Suma los valores contenidos en la posición 1 y 2, y lo agrega al final.
numbers.remove(0); // Elimina el primer elementos de la lista.
for (Integer number: numbers)
IO.println("Elemento:" + number); // Muestra la lista.
}
Al igual que con los conjuntos, se pueden inicializar listas inmutables mediante List.of():
List<Integer> numbers = List.of(1, 3, 5, 67);
Cuando se recorre una colección utilizando un bucle for-each tradicional, no está permitido modificar ni eliminar elementos de la colección. Si se intenta realizar un remove() o un add() directamente sobre la colección durante la iteración, Java lanzará una excepción ConcurrentModificationException.
Para eliminar elementos de forma segura mientras se recorre una colección, se debe emplear un objeto Iterator:
hasNext(): Devuelve true si quedan elementos por recorrer.next(): Avanza al siguiente elemento y lo devuelve.remove(): Elimina de la colección el último elemento devuelto por next().
// Obtención del iterador
Iterator it = estudiantes.iterator();
while (it.hasNext()) {
String nombre = it.next();
if (nombre.startsWith("J")) {
it.remove(); // Eliminación segura sin lanzar ConcurrentModificationException
}
}
Un Map es una estructura que almacena asociaciones de clave-valor (Key $\rightarrow$ Value). Las claves son únicas (no se pueden repetir) y sirven para acceder directamente al valor asociado.
Implementaciones principales:
HashMap: Sin orden específico en las claves.TreeMap: Ordena las claves por valor.LinkedHashMap: Mantiene el orden de inserción de las claves.
Métodos clave de Map:
V put(K key, V value): Asocia el valor (value) con la clave (key) en el map. Si la clave no existe en el map crea un nuevo par clave-valor. Si ya existe, reemplazará el valor.V get(Object key): Obtiene el valor asociado a una clave (key) ya almacenada en el mapa. Si no existe la clave, retornará null.V remove(Object key): Elimina la clave (key) y el valor (value) asociado. Retorna el valor asociado a la clave, por si lo queremos utilizar para algo, o null, si la clave no existe.boolean containsKey(Object key): Devuelve true si el map tiene almacenada la clave (key). En caso contrario devolverá false.boolean containsValue(Object value): Devuelve true si el map tiene almacenada el valor (value). En caso contrario devolverá false.int size(): Devuelve el número de pares clave-valor almacenado en el map.boolean isEmpty(): Devuelve true si el map está vacío, false en cualquier otro caso.void clear(): Vacía el map.Set<K> keySet(): Devuelve el conjunto de claves contenidas en el map.
Map<String, Integer> ages = new HashMap<String, Integer>();
ages.put("Ana", 20);
ages.put("Pedro", 25);
ages.put("María", 21);
ages.put("Ana", 21); // Actualiza la edad de Ana a 21
for (String name : ages.keySet()) {
IO.println(name + ": " + ages.get(name));
}
También es posible instanciar mapas inmutables con Map.of():
Map<String, Integer> ages = Map.of("Ana", 20, "Pedro", 25, "María", 21);
Existen dos formas principales de iterar sobre un mapa mediante un bucle for-each:
El método keySet() devuelve un conjunto con las claves del mapa. Para obtener el valor asociado a cada clave, es necesario llamar a map.get(clave) en cada iteración (lo que realiza una búsqueda adicional):
// Menos eficiente: realiza una búsqueda extra en el mapa con .get() en cada iteración
for (String producto : precios.keySet()) {
Double precio = precios.get(producto);
IO.println(producto + " cuesta " + precio + "€");
}
El método entrySet() devuelve un conjunto de parejas clave-valor representadas por la interfaz interna Map.Entry<K, V>. Permite acceder tanto a la clave (getKey()) como al valor (getValue()) de forma directa sin realizar búsquedas adicionales:
// Recomendado y más eficiente para leer clave y valor simultáneamente
for (Map.Entry<String, Double> entrada : precios.entrySet()) {
IO.println(entrada.getKey() + " cuesta " + entrada.getValue() + "€");
}
El comportamiento interno de las colecciones impone ciertos requisitos sobre las clases de los objetos que se almacenan en ellas.
Las colecciones con el prefijo Hash utilizan una tabla de dispersión (Hash Table) para localizar elementos en tiempo constante $O(1)$.
Para que funcionen correctamente, la clase de los objetos almacenados debe sobrescribir obligatoriamente los métodos equals() y hashCode() de Object:
hashCode(): Determina la casilla donde se guarda o busca el objeto.equals(): Resuelve posibles colisiones para confirmar si dos objetos son exactamente iguales.
Las colecciones con el prefijo Tree mantienen sus elementos ordenados automáticamente. No utilizan hashCode(), sino que requieren que los elementos se puedan comparar entre sí:
Comparable (sobrescribiendo el método compareTo()).Comparator al constructor de la colección.
La clase java.util.Collections (en plural) contiene exclusivamente métodos estáticos de utilidad diseñados para operar sobre listas y colecciones.
Principales métodos estáticos:
Collections.sort(lista): Ordena la lista en orden ascendente (según su orden natural).Collections.reverse(lista): Invierte el orden de los elementos en la lista.Collections.shuffle(lista): Desordena aleatoriamente los elementos de la lista.Collections.max(coleccion) / Collections.min(coleccion): Obtiene el elemento máximo o mínimo según su orden natural.Collections.frequency(coleccion, objeto): Cuenta cuántas veces aparece un elemento específico en la colección.Collections.unmodifiableList(lista): Devuelve una vista envuelta de solo lectura de la lista dada.
List<Integer> notas = new ArrayList<>(); notas.add(8); notas.add(4); notas.add(10); Collections.sort(notas); // La lista ahora es [4, 8, 10] Collections.reverse(notas); // La lista ahora es [10, 8, 4] int notaMaxima = Collections.max(notas); // Devuelve 10
Crea un programa que realice las siguientes acciones:
List.Collections para:Collections.frequency().
Crea un método estático que reciba una lista de cadenas de texto List y realice las siguientes transformaciones:
“INICIO” en el índice $0$ y la palabra “FIN” al final.Dada una lista inicial de direcciones de correo electrónico:
“spam_” usando un bucle for-each tradicional y comprueba/explica en un comentario el error generado.Iterator y su método remove() para eliminar de forma segura todos los correos basura.Crea un programa con una lista con elementos repetidos:
HashSet e imprime el resultado. ¿Qué ocurre con el orden de las palabras?LinkedHashSet e imprime el resultado. Compara la diferencia de orden respecto a HashSet.Desarrolla un programa de gestión de inventario para una tienda:
Map<String, Integer> donde la clave sea el código/nombre del producto y el valor sea el stock disponible.entrySet().
Dado un texto/frase introducido por teclado o almacenado en una cadena (ej. “java es un lenguaje java orientado a objetos y java es popular”):
.split(“ ”).Map<String, Integer> para contar cuántas veces aparece cada palabra en el texto.
Dada la clase Estudiante con las propiedades dni (String) y nombre (String):
equals() y hashCode() tomando el dni como atributo identificativo único.Comparable para ordenar estudiantes alfabéticamente por su nombre.HashSet. Comprueba que no se duplican.TreeSet y se verifique que quedan ordenados alfabéticamente de forma automática.Crea un sistema para clasificar alumnos por asignatura:
Map<String, List> donde la clave sea el nombre de la asignatura (ej. “Programación”, “Bases de Datos”) y el valor sea una lista de nombres de alumnos matriculados.matricularAlumno(Map<String, List> mapa, String asignatura, String alumno) que: