Colecciones en Java
Estructuras de Datos Eficientes para Almacenar Grupos de Objetos
En los capítulos anteriores aprendimos a crear clases, usar herencia y polimorfismo. Ahora abordamos un tema fundamental para cualquier programa real: cómo almacenar y manipular grupos de objetos de manera eficiente.
Este capítulo cubre:
El Framework de Colecciones: Listas, conjuntos, mapas y colas
Iteración: Diferentes formas de recorrer colecciones
Comparación y Ordenamiento:
ComparableyComparator
Para un tratamiento profundo de los genéricos (tipado seguro para colecciones y clases propias), consultá Genéricos en Java.
El Problema con los Arreglos¶
Limitaciones de los Arreglos¶
Los arreglos de Java tienen limitaciones significativas:
// 1. Tamaño fijo: hay que conocerlo al crear
String[] nombres = new String[10];
// 2. No se puede redimensionar
// Si necesitamos 11 elementos, hay que crear uno nuevo y copiar
// 3. Operaciones manuales
// Insertar en el medio requiere desplazar elementos
// 4. No hay métodos de búsqueda ni manipulación
// Hay que implementar todo manualmenteEjemplo del problema:
public class ListaContactos {
private String[] contactos;
private int cantidad;
public ListaContactos(int capacidadMaxima) {
contactos = new String[capacidadMaxima];
cantidad = 0;
}
public void agregar(String contacto) {
if (cantidad >= contactos.length) {
// ¡Problema! Hay que redimensionar manualmente
String[] nuevo = new String[contactos.length * 2];
System.arraycopy(contactos, 0, nuevo, 0, cantidad);
contactos = nuevo;
}
contactos[cantidad++] = contacto;
}
public void eliminar(int indice) {
// Hay que desplazar todos los elementos
for (int i = indice; i < cantidad - 1; i++) {
contactos[i] = contactos[i + 1];
}
contactos[--cantidad] = null;
}
// Mucho código repetitivo para operaciones básicas...
}La Solución: El Framework de Colecciones¶
Java proporciona el Java Collections Framework: un conjunto de interfaces y clases que implementan estructuras de datos comunes con operaciones optimizadas.
import java.util.ArrayList;
import java.util.List;
public class ListaContactos {
private List<String> contactos;
public ListaContactos() {
contactos = new ArrayList<>(); // Se redimensiona automáticamente
}
public void agregar(String contacto) {
contactos.add(contacto); // ¡Una línea!
}
public void eliminar(int indice) {
contactos.remove(indice); // ¡Una línea!
}
public boolean contiene(String contacto) {
return contactos.contains(contacto); // ¡Ya implementado!
}
}Jerarquía de Colecciones¶
Vista General¶
El Framework de Colecciones está organizado en una jerarquía de interfaces:
Interfaces Principales¶
| Interface | Descripción | Características |
|---|---|---|
List<E> | Secuencia ordenada | Permite duplicados, acceso por índice |
Set<E> | Conjunto sin duplicados | No permite duplicados |
Queue<E> | Cola (FIFO) | Operaciones de encolar/desencolar |
Map<K,V> | Diccionario clave-valor | Claves únicas, valores asociados |
List: Listas Ordenadas¶
Características de List¶
Ordenadas: Los elementos mantienen el orden de inserción
Acceso por índice: Se puede acceder a cualquier posición
Permite duplicados: Puede contener el mismo elemento varias veces
Permite null: Puede contener elementos nulos
Implementaciones Principales¶
ArrayList¶
ArrayList es la implementación más usada. Internamente usa un arreglo que se redimensiona automáticamente.
import java.util.ArrayList;
import java.util.List;
public class EjemploArrayList {
public static void main(String[] args) {
// Crear lista
List<String> frutas = new ArrayList<>();
// Agregar elementos
frutas.add("Manzana");
frutas.add("Banana");
frutas.add("Naranja");
frutas.add("Manzana"); // Duplicado permitido
// Acceder por índice
String primera = frutas.get(0); // "Manzana"
// Modificar elemento
frutas.set(1, "Pera"); // Reemplaza "Banana" por "Pera"
// Insertar en posición específica
frutas.add(1, "Uva"); // Inserta "Uva" en posición 1
// Eliminar
frutas.remove("Naranja"); // Por objeto
frutas.remove(0); // Por índice
// Tamaño
int cantidad = frutas.size(); // 3
// Verificar contenido
boolean tienePera = frutas.contains("Pera"); // true
// Obtener índice
int indice = frutas.indexOf("Pera"); // 0
// Verificar si está vacía
boolean vacia = frutas.isEmpty(); // false
// Limpiar
frutas.clear();
}
}Rendimiento de ArrayList:
| Operación | Complejidad | Descripción |
|---|---|---|
get(index) | O(1) | Acceso directo |
add(elemento) | O(1)* | Agrega al final (*amortizado) |
add(index, elemento) | O(n) | Debe desplazar elementos |
remove(index) | O(n) | Debe desplazar elementos |
contains(elemento) | O(n) | Búsqueda lineal |
LinkedList¶
LinkedList implementa una lista doblemente enlazada. Es más eficiente para inserciones/eliminaciones frecuentes.
import java.util.LinkedList;
import java.util.List;
public class EjemploLinkedList {
public static void main(String[] args) {
LinkedList<String> tareas = new LinkedList<>();
// Operaciones de lista
tareas.add("Tarea 1");
tareas.add("Tarea 2");
// Operaciones adicionales de LinkedList
tareas.addFirst("Tarea urgente"); // Al principio
tareas.addLast("Tarea final"); // Al final
String primera = tareas.getFirst(); // "Tarea urgente"
String ultima = tareas.getLast(); // "Tarea final"
tareas.removeFirst(); // Elimina "Tarea urgente"
tareas.removeLast(); // Elimina "Tarea final"
}
}¿Cuándo usar cada una?
| Situación | Usar |
|---|---|
| Acceso frecuente por índice | ArrayList |
| Muchas inserciones/eliminaciones al inicio | LinkedList |
| Caso general | ArrayList (más eficiente en memoria) |
Set: Conjuntos Sin Duplicados¶
Características de Set¶
Sin duplicados: Cada elemento aparece una sola vez
Sin orden garantizado (excepto implementaciones ordenadas)
Un solo null permitido (en la mayoría de implementaciones)
Implementaciones Principales¶
HashSet¶
HashSet es la implementación más rápida. No garantiza orden.
import java.util.HashSet;
import java.util.Set;
public class EjemploHashSet {
public static void main(String[] args) {
Set<String> paises = new HashSet<>();
// Agregar elementos
paises.add("Argentina");
paises.add("Brasil");
paises.add("Chile");
paises.add("Argentina"); // ¡No se agrega! Ya existe
System.out.println(paises.size()); // 3
// Verificar existencia (muy rápido)
boolean tieneArgentina = paises.contains("Argentina"); // true
// Eliminar
paises.remove("Brasil");
// Iterar (orden no garantizado)
for (String pais : paises) {
System.out.println(pais);
}
}
}Rendimiento de HashSet:
| Operación | Complejidad | Descripción |
|---|---|---|
add(elemento) | O(1) | Muy rápido |
remove(elemento) | O(1) | Muy rápido |
contains(elemento) | O(1) | Muy rápido |
TreeSet¶
TreeSet mantiene los elementos ordenados. Internamente usa un árbol rojo-negro.
import java.util.TreeSet;
import java.util.Set;
public class EjemploTreeSet {
public static void main(String[] args) {
Set<Integer> numeros = new TreeSet<>();
numeros.add(5);
numeros.add(2);
numeros.add(8);
numeros.add(1);
numeros.add(9);
// Los elementos están ordenados
for (Integer n : numeros) {
System.out.print(n + " "); // 1 2 5 8 9
}
// Operaciones adicionales de TreeSet
TreeSet<Integer> ts = new TreeSet<>(numeros);
System.out.println(ts.first()); // 1 (menor)
System.out.println(ts.last()); // 9 (mayor)
System.out.println(ts.lower(5)); // 2 (menor que 5)
System.out.println(ts.higher(5)); // 8 (mayor que 5)
System.out.println(ts.floor(6)); // 5 (menor o igual a 6)
System.out.println(ts.ceiling(6)); // 8 (mayor o igual a 6)
}
}Rendimiento de TreeSet:
| Operación | Complejidad | Descripción |
|---|---|---|
add(elemento) | O(log n) | Mantiene orden |
remove(elemento) | O(log n) | Mantiene orden |
contains(elemento) | O(log n) | Búsqueda binaria |
LinkedHashSet¶
LinkedHashSet mantiene el orden de inserción.
import java.util.LinkedHashSet;
import java.util.Set;
public class EjemploLinkedHashSet {
public static void main(String[] args) {
Set<String> colores = new LinkedHashSet<>();
colores.add("Rojo");
colores.add("Verde");
colores.add("Azul");
// Mantiene el orden de inserción
for (String color : colores) {
System.out.println(color); // Rojo, Verde, Azul (en ese orden)
}
}
}¿Cuándo usar cada Set?
| Situación | Usar |
|---|---|
| Máxima velocidad, sin importar orden | HashSet |
| Elementos ordenados naturalmente | TreeSet |
| Mantener orden de inserción | LinkedHashSet |
Map: Diccionarios Clave-Valor¶
Características de Map¶
Asocia claves con valores: Cada clave tiene un valor asociado
Claves únicas: No puede haber claves duplicadas
Valores duplicados: Los valores sí pueden repetirse
Un null como clave (en
HashMap)
Implementaciones Principales¶
HashMap¶
HashMap es la implementación más usada y rápida.
import java.util.HashMap;
import java.util.Map;
public class EjemploHashMap {
public static void main(String[] args) {
Map<String, Integer> edades = new HashMap<>();
// Agregar pares clave-valor
edades.put("Ana", 25);
edades.put("Juan", 30);
edades.put("María", 28);
// Obtener valor por clave
Integer edadAna = edades.get("Ana"); // 25
// Si la clave no existe, retorna null
Integer edadPedro = edades.get("Pedro"); // null
// Obtener con valor por defecto
Integer edadPedro2 = edades.getOrDefault("Pedro", 0); // 0
// Verificar si existe la clave
boolean tieneJuan = edades.containsKey("Juan"); // true
// Verificar si existe el valor
boolean tieneEdad30 = edades.containsValue(30); // true
// Actualizar valor (misma clave)
edades.put("Ana", 26); // Ahora Ana tiene 26
// Eliminar por clave
edades.remove("Juan");
// Tamaño
int cantidad = edades.size(); // 2
// Iterar sobre claves
for (String nombre : edades.keySet()) {
System.out.println(nombre);
}
// Iterar sobre valores
for (Integer edad : edades.values()) {
System.out.println(edad);
}
// Iterar sobre pares clave-valor
for (Map.Entry<String, Integer> entry : edades.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}Métodos útiles adicionales:
// putIfAbsent: solo agrega si la clave no existe
edades.putIfAbsent("Pedro", 22);
// computeIfAbsent: calcula el valor si la clave no existe
Map<String, List<String>> grupos = new HashMap<>();
grupos.computeIfAbsent("A", k -> new ArrayList<>()).add("Estudiante 1");
// merge: combina valores
Map<String, Integer> conteo = new HashMap<>();
conteo.merge("palabra", 1, Integer::sum); // Si existe, suma; si no, pone 1
// getOrDefault ya visto
int edad = edades.getOrDefault("Inexistente", -1);TreeMap¶
TreeMap mantiene las claves ordenadas.
import java.util.TreeMap;
import java.util.Map;
public class EjemploTreeMap {
public static void main(String[] args) {
Map<String, Double> notas = new TreeMap<>();
notas.put("Matemática", 8.5);
notas.put("Historia", 7.0);
notas.put("Física", 9.0);
notas.put("Arte", 8.0);
// Las claves están ordenadas alfabéticamente
for (String materia : notas.keySet()) {
System.out.println(materia + ": " + notas.get(materia));
}
// Arte: 8.0
// Física: 9.0
// Historia: 7.0
// Matemática: 8.5
// Operaciones adicionales de TreeMap
TreeMap<String, Double> tm = new TreeMap<>(notas);
System.out.println(tm.firstKey()); // "Arte"
System.out.println(tm.lastKey()); // "Matemática"
}
}No excepciones genéricas: Una clase que extiende
ExceptionoThrowableno puede ser genérica.
Iteración de Colecciones¶
For-Each (Recomendado)¶
List<String> nombres = List.of("Ana", "Juan", "María");
// For-each: limpio y legible
for (String nombre : nombres) {
System.out.println(nombre);
}Iterator¶
Para cuando necesitás eliminar elementos mientras iterás:
List<Integer> numeros = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
// Eliminar pares usando Iterator
Iterator<Integer> it = numeros.iterator();
while (it.hasNext()) {
Integer n = it.next();
if (n % 2 == 0) {
it.remove(); // ¡Seguro! No lanza ConcurrentModificationException
}
}
System.out.println(numeros); // [1, 3, 5]Comparación y Ordenamiento¶
La Interface Comparable¶
Comparable define el orden natural de una clase:
public class Persona implements Comparable<Persona> {
private String nombre;
private int edad;
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
// Ordenar por edad (orden natural)
@Override
public int compareTo(Persona otra) {
return Integer.compare(this.edad, otra.edad);
// Alternativa: return this.edad - otra.edad;
}
// getters...
public String getNombre() { return nombre; }
public int getEdad() { return edad; }
}
// Uso
List<Persona> personas = new ArrayList<>();
personas.add(new Persona("Ana", 25));
personas.add(new Persona("Juan", 20));
personas.add(new Persona("María", 30));
Collections.sort(personas); // Ordena por edad (orden natural)
// Resultado: Juan(20), Ana(25), María(30)Reglas de compareTo:
Retorna negativo si
this < otroRetorna cero si
this == otroRetorna positivo si
this > otro
La Interface Comparator¶
Comparator define criterios alternativos de ordenamiento:
import java.util.Comparator;
// Comparador por nombre
Comparator<Persona> porNombre = new Comparator<Persona>() {
@Override
public int compare(Persona p1, Persona p2) {
return p1.getNombre().compareTo(p2.getNombre());
}
};
// Con method reference (aún más conciso)
Comparator<Persona> porNombreRef =
Comparator.comparing(Persona::getNombre);
// Uso
Collections.sort(personas, porNombre); // Ordena por nombreComparadores Compuestos¶
Utilizando el :: que es una referncia al método, podemos utilizarlo como argumento.
// Ordenar por edad, y si empatan, por nombre
Comparator<Persona> porEdadYNombre =
Comparator.comparing(Persona::getEdad)
.thenComparing(Persona::getNombre);
// Orden descendente
Comparator<Persona> porEdadDesc =
Comparator.comparing(Persona::getEdad).reversed();
// Manejo de nulls
Comparator<Persona> porNombreNullsFirst =
Comparator.comparing(Persona::getNombre,
Comparator.nullsFirst(String::compareTo));
// Uso
personas.sort(porEdadYNombre);
personas.sort(porEdadDesc);Ordenamiento con TreeSet y TreeMap¶
// TreeSet con orden natural (requiere Comparable)
Set<Persona> personasOrdenadas = new TreeSet<>();
personasOrdenadas.add(new Persona("Ana", 25));
personasOrdenadas.add(new Persona("Juan", 20));
// Quedan ordenadas por edad
// TreeSet con Comparator personalizado
Set<Persona> personasPorNombre = new TreeSet<>(
Comparator.comparing(Persona::getNombre)
);
// TreeMap con Comparator
Map<Persona, String> empleados = new TreeMap<>(
Comparator.comparing(Persona::getEdad).reversed()
);equals() y hashCode()¶
La Importancia de equals() y hashCode()¶
Para que los objetos funcionen correctamente en colecciones, debés implementar equals() y hashCode().
public class Producto {
private String codigo;
private String nombre;
private double precio;
public Producto(String codigo, String nombre, double precio) {
this.codigo = codigo;
this.nombre = nombre;
this.precio = precio;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Producto otro = (Producto) obj;
return Objects.equals(codigo, otro.codigo); // Igualdad por código
}
@Override
public int hashCode() {
return Objects.hash(codigo); // Hash basado en código
}
// getters...
}Sin equals/hashCode Correctos¶
public class ProductoMal {
private String codigo;
// No implementa equals ni hashCode
}
Set<ProductoMal> productos = new HashSet<>();
productos.add(new ProductoMal("ABC"));
productos.add(new ProductoMal("ABC"));
System.out.println(productos.size()); // ¡2! Debería ser 1Con equals/hashCode Correctos¶
Set<Producto> productos = new HashSet<>();
productos.add(new Producto("ABC", "Laptop", 1500));
productos.add(new Producto("ABC", "Laptop diferente", 1600));
System.out.println(productos.size()); // 1 (correcto)Ejemplo Completo: Sistema de Inventario¶
import java.util.*;
public class Producto implements Comparable<Producto> {
private final String codigo;
private String nombre;
private double precio;
private int stock;
public Producto(String codigo, String nombre, double precio, int stock) {
this.codigo = codigo;
this.nombre = nombre;
this.precio = precio;
this.stock = stock;
}
// Getters
public String getCodigo() { return codigo; }
public String getNombre() { return nombre; }
public double getPrecio() { return precio; }
public int getStock() { return stock; }
// Setters
public void setPrecio(double precio) { this.precio = precio; }
public void ajustarStock(int cantidad) { this.stock += cantidad; }
@Override
public int compareTo(Producto otro) {
return this.codigo.compareTo(otro.codigo);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Producto)) return false;
Producto otro = (Producto) obj;
return Objects.equals(codigo, otro.codigo);
}
@Override
public int hashCode() {
return Objects.hash(codigo);
}
@Override
public String toString() {
return String.format("%s: %s ($%.2f) - Stock: %d",
codigo, nombre, precio, stock);
}
}
public class Inventario {
private Map<String, Producto> productos;
private Set<String> categorias;
private Map<String, Set<Producto>> productosPorCategoria;
public Inventario() {
this.productos = new HashMap<>();
this.categorias = new TreeSet<>(); // Ordenadas alfabéticamente
this.productosPorCategoria = new HashMap<>();
}
public void agregarProducto(Producto producto, String categoria) {
productos.put(producto.getCodigo(), producto);
categorias.add(categoria);
productosPorCategoria
.computeIfAbsent(categoria, k -> new HashSet<>())
.add(producto);
}
public Optional<Producto> buscarPorCodigo(String codigo) {
return Optional.ofNullable(productos.get(codigo));
}
public List<Producto> buscarPorNombre(String texto) {
return productos.values().stream()
.filter(p -> p.getNombre().toLowerCase()
.contains(texto.toLowerCase()))
.collect(Collectors.toList());
}
public List<Producto> obtenerConBajoStock(int minimo) {
return productos.values().stream()
.filter(p -> p.getStock() < minimo)
.sorted(Comparator.comparing(Producto::getStock))
.collect(Collectors.toList());
}
public List<Producto> obtenerPorCategoria(String categoria) {
Set<Producto> prods = productosPorCategoria.get(categoria);
if (prods == null) {
return Collections.emptyList();
}
return new ArrayList<>(prods);
}
public double calcularValorTotal() {
return productos.values().stream()
.mapToDouble(p -> p.getPrecio() * p.getStock())
.sum();
}
public Map<String, Long> contarPorCategoria() {
Map<String, Long> conteo = new HashMap<>();
for (Map.Entry<String, Set<Producto>> entry :
productosPorCategoria.entrySet()) {
conteo.put(entry.getKey(), (long) entry.getValue().size());
}
return conteo;
}
public void imprimirReporte() {
System.out.println("=== REPORTE DE INVENTARIO ===\n");
System.out.println("Categorías:");
for (String cat : categorias) {
System.out.println(" - " + cat);
}
System.out.println("\nProductos por categoría:");
for (String cat : categorias) {
System.out.println("\n" + cat + ":");
for (Producto p : obtenerPorCategoria(cat)) {
System.out.println(" " + p);
}
}
System.out.printf("\nValor total del inventario: $%.2f%n",
calcularValorTotal());
List<Producto> bajoStock = obtenerConBajoStock(10);
if (!bajoStock.isEmpty()) {
System.out.println("\n⚠️ Productos con bajo stock:");
bajoStock.forEach(p -> System.out.println(" " + p));
}
}
public static void main(String[] args) {
Inventario inv = new Inventario();
inv.agregarProducto(
new Producto("LAP001", "Laptop HP", 1500.00, 15),
"Electrónica"
);
inv.agregarProducto(
new Producto("LAP002", "Laptop Dell", 1800.00, 8),
"Electrónica"
);
inv.agregarProducto(
new Producto("MOU001", "Mouse Logitech", 45.00, 50),
"Periféricos"
);
inv.agregarProducto(
new Producto("TEC001", "Teclado Mecánico", 120.00, 5),
"Periféricos"
);
inv.agregarProducto(
new Producto("CAB001", "Cable HDMI", 15.00, 100),
"Accesorios"
);
inv.imprimirReporte();
System.out.println("\n--- Búsqueda ---");
inv.buscarPorNombre("laptop").forEach(System.out::println);
}
}Resumen¶
Colecciones¶
List: Secuencia ordenada con duplicados (
ArrayList,LinkedList)Set: Sin duplicados (
HashSet,TreeSet,LinkedHashSet)Map: Pares clave-valor (
HashMap,TreeMap)
Iteración¶
For-each: Limpio y legible para lectura
Iterator: Cuando necesitás modificar durante iteración
Streams: Operaciones funcionales (Java 8+)
Comparación¶
Comparable<T>: Orden natural (implementa la clase)Comparator<T>: Orden alternativo (clase separada o lambda)
equals() y hashCode()¶
Siempre sobreescribir ambos juntos
Usar
Objects.equals()yObjects.hash()para simplificar
Nota: Para un estudio detallado de genéricos (tipado seguro, clases genéricas, bounded types, wildcards), consultá el capítulo dedicado Genéricos en Java.
Ejercicios¶
Próximo paso¶
Para seguir, conviene pasar a el material siguiente, donde el recorrido continúa sobre esta base.