Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Programación Orientada a Objetos

Colecciones en Java

Estructuras de Datos Eficientes para Almacenar Grupos de Objetos

Universidad Nacional de Rio Negro - Sede Andina

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:

  1. El Framework de Colecciones: Listas, conjuntos, mapas y colas

  2. Iteración: Diferentes formas de recorrer colecciones

  3. Comparación y Ordenamiento: Comparable y Comparator

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 manualmente

Ejemplo 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

InterfaceDescripciónCaracterísticas
List<E>Secuencia ordenadaPermite duplicados, acceso por índice
Set<E>Conjunto sin duplicadosNo permite duplicados
Queue<E>Cola (FIFO)Operaciones de encolar/desencolar
Map<K,V>Diccionario clave-valorClaves únicas, valores asociados

List: Listas Ordenadas

Características de List

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ónComplejidadDescripció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ónUsar
Acceso frecuente por índiceArrayList
Muchas inserciones/eliminaciones al inicioLinkedList
Caso generalArrayList (más eficiente en memoria)

Set: Conjuntos Sin Duplicados

Características de Set

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ónComplejidadDescripció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ónComplejidadDescripció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ónUsar
Máxima velocidad, sin importar ordenHashSet
Elementos ordenados naturalmenteTreeSet
Mantener orden de inserciónLinkedHashSet

Map: Diccionarios Clave-Valor

Características de Map

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"
    }
}
  1. No excepciones genéricas: Una clase que extiende Exception o Throwable no 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:

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 nombre

Comparadores 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 1

Con 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

Iteración

Comparación

equals() y hashCode()

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.