Colecciones en Java: List, Set y Map
Introducción
Las colecciones son estructuras de datos que nos permiten almacenar y manipular grupos de objetos de forma eficiente en Java. A diferencia de los arrays, que tienen un tamaño fijo, las colecciones son dinámicas y ofrecen numerosos métodos para realizar operaciones comunes como agregar, eliminar y buscar elementos. El framework de colecciones de Java proporciona interfaces y clases que implementan distintos tipos de colecciones, cada una con características específicas que la hacen adecuada para diferentes necesidades.
En este artículo, exploraremos las tres interfaces principales de colecciones en Java: List, Set y Map. Aprenderemos cómo funcionan, cuándo utilizar cada una y cómo implementarlas en nuestros programas para resolver problemas prácticos de manera elegante y eficiente.
List: colecciones ordenadas con elementos duplicados
Las listas son colecciones ordenadas que permiten elementos duplicados. Mantienen el orden de inserción (a menos que se especifique lo contrario) y permiten acceder a los elementos mediante un índice, similar a los arrays.
Principales implementaciones de List
Java ofrece varias implementaciones de la interfaz List
, cada una con características específicas:
- ArrayList: Implementación basada en arrays dinámicos, ideal para acceso aleatorio.
- LinkedList: Implementación basada en listas doblemente enlazadas, eficiente para inserciones y eliminaciones.
- Vector: Similar a ArrayList pero sincronizado (thread-safe).
Ejemplo básico de ArrayList
import java.util.ArrayList;
import java.util.List;
public class EjemploArrayList {
public static void main(String[] args) {
// Creación de un ArrayList de cadenas
List<String> frutas = new ArrayList<>();
// Añadir elementos
frutas.add("Manzana");
frutas.add("Plátano");
frutas.add("Naranja");
frutas.add("Manzana"); // Se permite duplicados
// Mostrar todos los elementos
System.out.println("Lista de frutas: " + frutas);
// Acceder a un elemento por su índice
System.out.println("Fruta en posición 1: " + frutas.get(1));
// Tamaño de la lista
System.out.println("Número de frutas: " + frutas.size());
// Comprobar si contiene un elemento
System.out.println("¿Contiene Naranja? " + frutas.contains("Naranja"));
// Eliminar un elemento
frutas.remove("Plátano");
System.out.println("Lista después de eliminar: " + frutas);
// Recorrer la lista con bucle for-each
System.out.println("Recorriendo la lista:");
for (String fruta : frutas) {
System.out.println("- " + fruta);
}
}
}
Ejemplo de LinkedList
import java.util.LinkedList;
public class EjemploLinkedList {
public static void main(String[] args) {
LinkedList<Integer> numeros = new LinkedList<>();
// Añadir elementos al final
numeros.add(10);
numeros.add(20);
// Añadir al principio
numeros.addFirst(5);
// Añadir al final (igual que add)
numeros.addLast(30);
System.out.println("Lista enlazada: " + numeros);
// Obtener primer y último elemento
System.out.println("Primer elemento: " + numeros.getFirst());
System.out.println("Último elemento: " + numeros.getLast());
// Eliminar primer y último elemento
numeros.removeFirst();
numeros.removeLast();
System.out.println("Después de eliminar: " + numeros);
}
}
Set: colecciones sin elementos duplicados
Los conjuntos (Sets) son colecciones que no permiten elementos duplicados. A diferencia de las listas, los sets no garantizan un orden específico (excepto algunas implementaciones).
Principales implementaciones de Set
- HashSet: Implementación basada en tablas hash, muy eficiente para operaciones básicas.
- LinkedHashSet: Mantiene el orden de inserción, combinando tablas hash con listas enlazadas.
- TreeSet: Implementación basada en árboles, mantiene los elementos ordenados.
Ejemplo de HashSet
import java.util.HashSet;
import java.util.Set;
public class EjemploHashSet {
public static void main(String[] args) {
// Crear un HashSet de cadenas
Set<String> lenguajes = new HashSet<>();
// Añadir elementos
lenguajes.add("Java");
lenguajes.add("Python");
lenguajes.add("JavaScript");
lenguajes.add("Java"); // Intento de duplicado (será ignorado)
// Mostrar el conjunto
System.out.println("Lenguajes de programación: " + lenguajes);
// Comprobar si contiene un elemento
System.out.println("¿Contiene Java? " + lenguajes.contains("Java"));
// Tamaño del conjunto
System.out.println("Número de lenguajes: " + lenguajes.size());
// Eliminar un elemento
lenguajes.remove("Python");
System.out.println("Después de eliminar Python: " + lenguajes);
// Recorrer el conjunto
System.out.println("Recorriendo el conjunto:");
for (String lenguaje : lenguajes) {
System.out.println("- " + lenguaje);
}
}
}
Ejemplo de TreeSet
import java.util.TreeSet;
public class EjemploTreeSet {
public static void main(String[] args) {
TreeSet<Integer> numeros = new TreeSet<>();
// Añadir elementos en cualquier orden
numeros.add(10);
numeros.add(5);
numeros.add(20);
numeros.add(7);
// TreeSet mantiene los elementos ordenados
System.out.println("Números ordenados: " + numeros);
// Obtener el primer y último elemento
System.out.println("Elemento más pequeño: " + numeros.first());
System.out.println("Elemento más grande: " + numeros.last());
// Obtener subconjuntos
System.out.println("Números menores que 10: " + numeros.headSet(10));
System.out.println("Números desde 7 hasta 20: " + numeros.subSet(7, true, 20, true));
}
}
Map: colecciones de pares clave-valor
Los mapas son colecciones que almacenan pares clave-valor, donde cada clave es única. No extienden de la interfaz Collection, pero se consideran parte del framework de colecciones de Java.
Principales implementaciones de Map
- HashMap: Implementación basada en tablas hash, muy eficiente.
- LinkedHashMap: Mantiene el orden de inserción de las claves.
- TreeMap: Mantiene las claves ordenadas según su orden natural o un comparador.
Ejemplo de HashMap
import java.util.HashMap;
import java.util.Map;
public class EjemploHashMap {
public static void main(String[] args) {
// Crear un HashMap que asocia nombres con edades
Map<String, Integer> edades = new HashMap<>();
// Añadir pares clave-valor
edades.put("Carlos", 25);
edades.put("Ana", 22);
edades.put("Miguel", 30);
// Mostrar el mapa
System.out.println("Mapa de edades: " + edades);
// Obtener un valor a partir de su clave
System.out.println("Edad de Ana: " + edades.get("Ana"));
// Si la clave no existe, devuelve null
System.out.println("Edad de Juan: " + edades.get("Juan"));
// Comprobar si existe una clave
System.out.println("¿Existe Miguel? " + edades.containsKey("Miguel"));
// Comprobar si existe un valor
System.out.println("¿Hay alguien con 30 años? " + edades.containsValue(30));
// Recorrer el mapa
System.out.println("Recorriendo el mapa:");
for (Map.Entry<String, Integer> entrada : edades.entrySet()) {
System.out.println(entrada.getKey() + " tiene " + entrada.getValue() + " años");
}
// Otra forma de recorrer solo las claves
System.out.println("Nombres en el mapa:");
for (String nombre : edades.keySet()) {
System.out.println("- " + nombre);
}
}
}
Ejemplo de TreeMap
import java.util.TreeMap;
public class EjemploTreeMap {
public static void main(String[] args) {
TreeMap<String, String> capitales = new TreeMap<>();
// Añadir pares clave-valor
capitales.put("España", "Madrid");
capitales.put("Francia", "París");
capitales.put("Italia", "Roma");
capitales.put("Alemania", "Berlín");
// TreeMap mantiene las claves ordenadas
System.out.println("Capitales ordenadas por país: " + capitales);
// Obtener la primera y última entrada
System.out.println("Primera entrada: " + capitales.firstEntry());
System.out.println("Última entrada: " + capitales.lastEntry());
// Obtener subconjuntos
System.out.println("Países desde 'A' hasta 'F': " +
capitales.subMap("A", true, "F", true));
}
}
Operaciones comunes con colecciones
Java proporciona la clase Collections
que contiene métodos estáticos para realizar operaciones comunes en colecciones:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class OperacionesColecciones {
public static void main(String[] args) {
List<Integer> numeros = new ArrayList<>();
numeros.add(5);
numeros.add(2);
numeros.add(8);
numeros.add(1);
System.out.println("Lista original: " + numeros);
// Ordenar una lista
Collections.sort(numeros);
System.out.println("Lista ordenada: " + numeros);
// Invertir una lista
Collections.reverse(numeros);
System.out.println("Lista invertida: " + numeros);
// Barajar elementos (aleatorizar)
Collections.shuffle(numeros);
System.out.println("Lista barajada: " + numeros);
// Valor máximo y mínimo
System.out.println("Valor máximo: " + Collections.max(numeros));
System.out.println("Valor mínimo: " + Collections.min(numeros));
}
}
Cuándo usar cada tipo de colección
Para elegir el tipo de colección adecuado, considera estas directrices:
-
Usa List cuando:
- Necesites mantener el orden de los elementos
- Necesites acceder a elementos por su posición
- Permites elementos duplicados
-
Usa Set cuando:
- No quieras elementos duplicados
- No te importe el orden (HashSet) o necesites un orden específico (TreeSet, LinkedHashSet)
- Necesites operaciones de conjunto (unión, intersección, etc.)
-
Usa Map cuando:
- Necesites asociar valores con claves únicas
- Necesites búsquedas rápidas por clave
- Quieras organizar datos en formato clave-valor
Ejemplo práctico: agenda de contactos
Veamos un ejemplo que integra diferentes tipos de colecciones para implementar una agenda de contactos:
import java.util.*;
public class AgendaContactos {
public static void main(String[] args) {
// Mapa para almacenar contactos (nombre -> teléfono)
Map<String, String> contactos = new HashMap<>();
// Lista para mantener el historial de llamadas
List<String> historialLlamadas = new ArrayList<>();
// Conjunto para categorías sin duplicados
Set<String> categorias = new HashSet<>();
// Añadir contactos
contactos.put("María", "666111222");
contactos.put("Juan", "655333444");
contactos.put("Pablo", "644555666");
// Añadir categorías
categorias.add("Amigos");
categorias.add("Trabajo");
categorias.add("Familia");
categorias.add("Amigos"); // Duplicado, será ignorado
// Simular algunas llamadas
simularLlamada("María", historialLlamadas);
simularLlamada("Juan", historialLlamadas);
simularLlamada("María", historialLlamadas);
// Mostrar información
System.out.println("--- AGENDA DE CONTACTOS ---");
System.out.println("Contactos: " + contactos);
System.out.println("Categorías: " + categorias);
System.out.println("Historial de llamadas: " + historialLlamadas);
// Buscar un contacto
String nombre = "María";
if (contactos.containsKey(nombre)) {
System.out.println("\nTeléfono de " + nombre + ": " + contactos.get(nombre));
} else {
System.out.println("\nContacto no encontrado");
}
// Contar llamadas por contacto
Map<String, Integer> frecuenciaLlamadas = new HashMap<>();
for (String contacto : historialLlamadas) {
frecuenciaLlamadas.put(contacto,
frecuenciaLlamadas.getOrDefault(contacto, 0) + 1);
}
System.out.println("\nFrecuencia de llamadas:");
for (Map.Entry<String, Integer> entrada : frecuenciaLlamadas.entrySet()) {
System.out.println(entrada.getKey() + ": " + entrada.getValue() + " llamadas");
}
}
// Método para simular una llamada
private static void simularLlamada(String contacto, List<String> historial) {
historial.add(contacto);
System.out.println("Llamando a " + contacto + "...");
}
}
Resumen
Las colecciones en Java proporcionan estructuras de datos flexibles y potentes para almacenar y manipular grupos de objetos. En este artículo hemos explorado las tres interfaces principales:
- List: Colecciones ordenadas que permiten duplicados, ideales cuando el orden es importante.
- Set: Colecciones que no permiten elementos duplicados, perfectas para representar conjuntos matemáticos.
- Map: Estructuras clave-valor donde cada clave es única, excelentes para búsquedas y asociaciones.
Dominar el framework de colecciones es esencial para desarrollar aplicaciones Java eficientes, ya que te permite elegir la estructura de datos más adecuada para cada problema. En el siguiente artículo, exploraremos cómo trabajar con fechas y horas en Java usando el paquete java.time, lo que complementará tu conocimiento sobre las API estándar de Java más utilizadas.