Skip to content

7. Manejo de Arreglos y Colecciones

El manejo eficiente de datos es fundamental en cualquier aplicación. Java ofrece diversas estructuras para almacenar y manipular conjuntos de datos, desde los arreglos tradicionales hasta las colecciones del Framework de Colecciones de Java (JCF). En este capítulo, exploraremos estas estructuras, sus características, ventajas y casos de uso.

7.1 Arreglos (simples y multidimensionales)

Section titled “7.1 Arreglos (simples y multidimensionales)”

Los arreglos son estructuras de datos que permiten almacenar múltiples valores del mismo tipo bajo un solo nombre de variable. En Java, los arreglos son objetos y tienen una longitud fija que se establece al momento de su creación.

Existen varias formas de declarar e inicializar arreglos en Java:

// Declaración (solo crea la referencia)
int[] numeros;
// Asignación (crea el arreglo en memoria)
numeros = new int[5]; // Arreglo de 5 enteros inicializados a 0

Los elementos de un arreglo se acceden mediante índices que comienzan en 0:

int[] numeros = {10, 20, 30, 40, 50};
// Acceso a elementos
int primerNumero = numeros[0]; // 10
int tercerNumero = numeros[2]; // 30
// Modificación de elementos
numeros[1] = 25; // Cambia el segundo elemento a 25
numeros[4] = 55; // Cambia el último elemento a 55
// Error en tiempo de ejecución: ArrayIndexOutOfBoundsException
// numeros[5] = 60; // ¡El índice 5 está fuera de los límites del arreglo!

La propiedad length devuelve el tamaño de un arreglo:

int[] numeros = {10, 20, 30, 40, 50};
int longitud = numeros.length; // 5
// Recorrer un arreglo usando su longitud
for (int i = 0; i < numeros.length; i++) {
System.out.println("Elemento " + i + ": " + numeros[i]);
}

Existen varias formas de recorrer un arreglo en Java:

int[] numeros = {10, 20, 30, 40, 50};
// Usando un bucle for tradicional
for (int i = 0; i < numeros.length; i++) {
System.out.println(numeros[i]);
}

La clase java.util.Arrays proporciona métodos útiles para trabajar con arreglos:

import java.util.Arrays;
int[] numeros = {30, 10, 50, 20, 40};
// Ordenar un arreglo
Arrays.sort(numeros);
System.out.println(Arrays.toString(numeros)); // [10, 20, 30, 40, 50]
// Buscar un elemento (en un arreglo ordenado)
int indice = Arrays.binarySearch(numeros, 30); // 2
// Comparar arreglos
int[] otrosNumeros = {10, 20, 30, 40, 50};
boolean sonIguales = Arrays.equals(numeros, otrosNumeros); // true
// Llenar un arreglo
int[] masNumeros = new int[5];
Arrays.fill(masNumeros, 7);
System.out.println(Arrays.toString(masNumeros)); // [7, 7, 7, 7, 7]
// Copiar un arreglo
int[] copiaNumeros = Arrays.copyOf(numeros, numeros.length);
int[] primerosTres = Arrays.copyOf(numeros, 3); // [10, 20, 30]
int[] subArreglo = Arrays.copyOfRange(numeros, 1, 4); // [20, 30, 40]

Java soporta arreglos multidimensionales, que son arreglos de arreglos. Los más comunes son los arreglos bidimensionales (matrices).

// Declaración
int[][] matriz;
// Asignación
matriz = new int[3][4]; // Matriz de 3 filas y 4 columnas

En Java, cada fila de un arreglo multidimensional puede tener diferente longitud:

// Arreglo irregular (jagged array)
int[][] irregular = new int[3][];
irregular[0] = new int[2]; // Primera fila con 2 columnas
irregular[1] = new int[4]; // Segunda fila con 4 columnas
irregular[2] = new int[3]; // Tercera fila con 3 columnas
// O directamente
int[][] irregular2 = {
{1, 2},
{3, 4, 5, 6},
{7, 8, 9}
};
int[][] matriz = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
// Acceso a elementos
int elemento = matriz[1][2]; // 6 (fila 1, columna 2)
// Modificación de elementos
matriz[0][1] = 20; // Cambia el elemento en la fila 0, columna 1 a 20
int[][] matriz = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
// Usando bucles for anidados
for (int i = 0; i < matriz.length; i++) {
for (int j = 0; j < matriz[i].length; j++) {
System.out.print(matriz[i][j] + " ");
}
System.out.println(); // Nueva línea después de cada fila
}
// Usando bucles for-each anidados
for (int[] fila : matriz) {
for (int elemento : fila) {
System.out.print(elemento + " ");
}
System.out.println();
}

Los arreglos en Java tienen varias limitaciones:

  1. Tamaño fijo: Una vez creado, el tamaño no puede cambiar.
  2. Tipo homogéneo: Todos los elementos deben ser del mismo tipo.
  3. Operaciones limitadas: No proporcionan métodos para insertar o eliminar elementos.
  4. Sin información de tamaño actual: No hay forma de saber cuántos elementos “útiles” contiene un arreglo parcialmente lleno.
  • Cuando se conoce el tamaño exacto de antemano
  • Para estructuras de datos de tamaño fijo (por ejemplo, días de la semana)
  • Para operaciones de alto rendimiento donde la simplicidad es crucial
  • Para representar datos multidimensionales como matrices o tensores
  • Cuando se trabaja con APIs que requieren arreglos

El Framework de Colecciones de Java proporciona implementaciones dinámicas de listas que superan las limitaciones de los arreglos tradicionales. Las dos implementaciones más comunes son ArrayList y LinkedList.

ArrayList es una implementación redimensionable de la interfaz List que utiliza un arreglo dinámico internamente. Es similar a un arreglo tradicional, pero puede crecer o reducirse según sea necesario.

import java.util.ArrayList;
import java.util.List;
// Creación de ArrayList vacío
ArrayList<String> nombres = new ArrayList<>();
// Creación con capacidad inicial (optimización de rendimiento)
ArrayList<Integer> numeros = new ArrayList<>(20);
// Usando la interfaz List (recomendado para mejor diseño)
List<Double> precios = new ArrayList<>();
// Inicialización con elementos
List<String> frutas = new ArrayList<>();
frutas.add("Manzana");
frutas.add("Banana");
frutas.add("Naranja");
// Inicialización usando List.of (Java 9+, crea lista inmutable)
List<String> colores = List.of("Rojo", "Verde", "Azul");
// Convertir a ArrayList (para hacerla mutable)
List<String> coloresMutables = new ArrayList<>(colores);
List<String> frutas = new ArrayList<>();
// Añadir elementos
frutas.add("Manzana"); // Añade al final
frutas.add(1, "Banana"); // Añade en posición específica
// Acceder a elementos
String primeraFruta = frutas.get(0); // "Manzana"
// Modificar elementos
frutas.set(1, "Pera"); // Reemplaza "Banana" con "Pera"
// Eliminar elementos
frutas.remove("Manzana"); // Elimina por objeto
frutas.remove(0); // Elimina por índice
// Verificar si contiene un elemento
boolean contienePera = frutas.contains("Pera");
// Tamaño
int tamaño = frutas.size();
// Verificar si está vacía
boolean estaVacia = frutas.isEmpty();
// Limpiar todos los elementos
frutas.clear();
List<String> frutas = new ArrayList<>();
frutas.add("Manzana");
frutas.add("Banana");
frutas.add("Naranja");
// Usando un bucle for tradicional
for (int i = 0; i < frutas.size(); i++) {
System.out.println(frutas.get(i));
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
List<String> frutas = new ArrayList<>();
frutas.add("Manzana");
frutas.add("Banana");
frutas.add("Naranja");
frutas.add("Pera");
// Ordenar
Collections.sort(frutas); // ["Banana", "Manzana", "Naranja", "Pera"]
// Ordenar en reversa
Collections.reverse(frutas); // ["Pera", "Naranja", "Manzana", "Banana"]
// Mezclar
Collections.shuffle(frutas); // Orden aleatorio
// Convertir a array
String[] frutasArray = frutas.toArray(new String[0]);
// Sublista (vista de la lista original)
List<String> subLista = frutas.subList(1, 3); // Desde índice 1 (inclusive) hasta 3 (exclusive)
// Buscar
int indice = frutas.indexOf("Naranja");
int ultimoIndice = frutas.lastIndexOf("Naranja"); // Útil si hay duplicados
// Añadir todos los elementos de otra colección
List<String> masFrutas = new ArrayList<>();
masFrutas.add("Uva");
masFrutas.add("Kiwi");
frutas.addAll(masFrutas); // Añade al final
frutas.addAll(1, masFrutas); // Añade desde el índice 1

LinkedList es una implementación de lista doblemente enlazada de las interfaces List y Deque. Cada elemento (nodo) contiene una referencia al elemento anterior y siguiente.

import java.util.LinkedList;
import java.util.List;
import java.util.Deque;
// Creación de LinkedList vacía
LinkedList<String> nombres = new LinkedList<>();
// Como implementación de List
List<Integer> numeros = new LinkedList<>();
// Como implementación de Deque (cola doble)
Deque<Double> precios = new LinkedList<>();
// Inicialización con elementos
LinkedList<String> frutas = new LinkedList<>();
frutas.add("Manzana");
frutas.add("Banana");
frutas.add("Naranja");

Además de las operaciones de List, LinkedList proporciona métodos adicionales para operaciones de cola y pila:

LinkedList<String> frutas = new LinkedList<>();
frutas.add("Banana");
frutas.add("Naranja");
// Operaciones de cola (Queue)
frutas.offer("Manzana"); // Añade al final (similar a add)
String primera = frutas.poll(); // Obtiene y elimina el primer elemento
String cabeza = frutas.peek(); // Obtiene sin eliminar el primer elemento
// Operaciones de pila (Stack)
frutas.push("Pera"); // Añade al principio
String superior = frutas.pop(); // Obtiene y elimina el primer elemento
// Operaciones de Deque (cola doble)
frutas.addFirst("Uva"); // Añade al principio
frutas.addLast("Kiwi"); // Añade al final
String primera2 = frutas.removeFirst(); // Elimina y devuelve el primer elemento
String ultima = frutas.removeLast(); // Elimina y devuelve el último elemento
String primeraVista = frutas.getFirst(); // Obtiene sin eliminar el primer elemento
String ultimaVista = frutas.getLast(); // Obtiene sin eliminar el último elemento
CaracterísticaArrayListLinkedList
Estructura internaArreglo dinámicoLista doblemente enlazada
Acceso aleatorioO(1) - ConstanteO(n) - Lineal
Inserción/eliminación al principio/medioO(n) - Requiere desplazar elementosO(1) - Solo actualiza referencias
Inserción/eliminación al finalO(1) amortizadoO(1)
Uso de memoriaMenor (solo almacena elementos)Mayor (almacena elementos y referencias)
IteraciónMás rápidaMás lenta
Funcionalidad adicionalSolo operaciones de listaOperaciones de lista, cola y pila
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
// Ejemplo de uso apropiado de ArrayList
List<Integer> numeros = new ArrayList<>();
// Añadir muchos elementos al final es eficiente
for (int i = 0; i < 100000; i++) {
numeros.add(i);
}
// Acceso aleatorio es eficiente
int numero = numeros.get(50000); // O(1)
// Ejemplo de uso apropiado de LinkedList
LinkedList<String> historial = new LinkedList<>();
// Añadir al principio es eficiente
historial.addFirst("Página 3");
historial.addFirst("Página 2");
historial.addFirst("Página 1");
// Implementar navegación adelante/atrás
String paginaActual = historial.removeFirst(); // "Página 1"
String siguientePagina = historial.removeFirst(); // "Página 2"

Las colecciones basadas en hash utilizan tablas hash para almacenar elementos, lo que permite un acceso, inserción y eliminación muy eficientes. Las dos implementaciones más comunes son HashMap y HashSet.

HashMap implementa la interfaz Map y almacena pares clave-valor, donde cada clave es única. Proporciona acceso en tiempo constante O(1) para operaciones básicas.

import java.util.HashMap;
import java.util.Map;
// Creación de HashMap vacío
HashMap<String, Integer> edades = new HashMap<>();
// Creación con capacidad inicial y factor de carga
HashMap<String, Double> precios = new HashMap<>(100, 0.75f);
// Usando la interfaz Map (recomendado)
Map<Integer, String> empleados = new HashMap<>();
// Inicialización con elementos
Map<String, String> capitales = new HashMap<>();
capitales.put("España", "Madrid");
capitales.put("Francia", "París");
capitales.put("Italia", "Roma");
// Inicialización usando Map.of (Java 9+, crea mapa inmutable)
Map<String, Integer> poblacion = Map.of(
"Madrid", 3223000,
"Barcelona", 1620000,
"Valencia", 791000
);
// Convertir a HashMap (para hacerlo mutable)
Map<String, Integer> poblacionMutable = new HashMap<>(poblacion);
Map<String, String> capitales = new HashMap<>();
// Añadir elementos
capitales.put("España", "Madrid");
capitales.put("Francia", "París");
// Acceder a elementos
String capitalEspaña = capitales.get("España"); // "Madrid"
String capitalAlemania = capitales.get("Alemania"); // null
// Acceder con valor por defecto si no existe
String capitalPortugal = capitales.getOrDefault("Portugal", "Desconocida"); // "Desconocida"
// Verificar si contiene una clave o valor
boolean contieneEspaña = capitales.containsKey("España"); // true
boolean contieneLondres = capitales.containsValue("Londres"); // false
// Modificar elementos
capitales.put("Francia", "París"); // Sobrescribe el valor existente
// Insertar solo si la clave no existe
capitales.putIfAbsent("Italia", "Roma"); // Añade Italia->Roma
capitales.putIfAbsent("España", "Barcelona"); // No hace nada, España ya existe
// Eliminar elementos
capitales.remove("Francia"); // Elimina Francia->París
capitales.remove("Italia", "Milán"); // No elimina nada, el valor no coincide
// Tamaño
int tamaño = capitales.size(); // 1
// Verificar si está vacío
boolean estaVacio = capitales.isEmpty(); // false
// Limpiar todos los elementos
capitales.clear(); // Elimina todos los elementos
Map<String, String> capitales = new HashMap<>();
capitales.put("España", "Madrid");
capitales.put("Francia", "París");
capitales.put("Italia", "Roma");
// Recorrer las claves
for (String pais : capitales.keySet()) {
System.out.println("País: " + pais);
}
Map<String, Integer> poblacion = new HashMap<>();
poblacion.put("Madrid", 3223000);
poblacion.put("Barcelona", 1620000);
poblacion.put("Valencia", 791000);
// Computar un valor basado en la clave actual
poblacion.compute("Madrid", (ciudad, habitantes) -> habitantes + 10000);
// Computar un valor solo si la clave existe
poblacion.computeIfPresent("Barcelona", (ciudad, habitantes) -> habitantes * 2);
// Computar un valor solo si la clave no existe
poblacion.computeIfAbsent("Sevilla", ciudad -> 688000);
// Fusionar valores
poblacion.merge("Valencia", 5000, (valorAntiguo, valorNuevo) -> valorAntiguo + valorNuevo);
// Reemplazar valores
poblacion.replace("Madrid", 3300000);
poblacion.replace("Barcelona", 1620000, 1650000); // Solo reemplaza si el valor actual coincide
// Operaciones en masa
Map<String, Integer> masCiudades = new HashMap<>();
masCiudades.put("Zaragoza", 675000);
masCiudades.put("Málaga", 571000);
// Añadir todos los elementos de otro mapa
poblacion.putAll(masCiudades);

HashSet implementa la interfaz Set y almacena elementos únicos sin duplicados. Utiliza un HashMap internamente para almacenar los elementos como claves (con un objeto ficticio como valor).

import java.util.HashSet;
import java.util.Set;
// Creación de HashSet vacío
HashSet<String> nombres = new HashSet<>();
// Creación con capacidad inicial
HashSet<Integer> numeros = new HashSet<>(100);
// Usando la interfaz Set (recomendado)
Set<String> colores = new HashSet<>();
// Inicialización con elementos
Set<String> frutas = new HashSet<>();
frutas.add("Manzana");
frutas.add("Banana");
frutas.add("Naranja");
// Inicialización usando Set.of (Java 9+, crea conjunto inmutable)
Set<String> dias = Set.of("Lunes", "Martes", "Miércoles", "Jueves", "Viernes");
// Convertir a HashSet (para hacerlo mutable)
Set<String> diasMutables = new HashSet<>(dias);
// Inicialización desde otra colección
import java.util.Arrays;
import java.util.List;
List<String> listaPaises = Arrays.asList("España", "Francia", "Italia", "España");
Set<String> paises = new HashSet<>(listaPaises); // Elimina duplicados automáticamente
Set<String> colores = new HashSet<>();
// Añadir elementos
colores.add("Rojo"); // Devuelve true
colores.add("Verde");
boolean agregado = colores.add("Rojo"); // Devuelve false, ya existe
// Verificar si contiene un elemento
boolean contieneAzul = colores.contains("Azul"); // false
// Eliminar elementos
boolean eliminado = colores.remove("Verde"); // true
boolean eliminado2 = colores.remove("Amarillo"); // false, no existe
// Tamaño
int tamaño = colores.size(); // 1
// Verificar si está vacío
boolean estaVacio = colores.isEmpty(); // false
// Limpiar todos los elementos
colores.clear();
Set<String> colores = new HashSet<>();
colores.add("Rojo");
colores.add("Verde");
colores.add("Azul");
// Usando un bucle for-each
for (String color : colores) {
System.out.println(color);
}
import java.util.HashSet;
import java.util.Set;
Set<String> frutas1 = new HashSet<>();
frutas1.add("Manzana");
frutas1.add("Banana");
frutas1.add("Naranja");
Set<String> frutas2 = new HashSet<>();
frutas2.add("Naranja");
frutas2.add("Pera");
frutas2.add("Uva");
// Unión (modificando frutas1)
Set<String> union = new HashSet<>(frutas1);
union.addAll(frutas2); // [Manzana, Banana, Naranja, Pera, Uva]
// Intersección (elementos comunes)
Set<String> interseccion = new HashSet<>(frutas1);
interseccion.retainAll(frutas2); // [Naranja]
// Diferencia (elementos en frutas1 pero no en frutas2)
Set<String> diferencia = new HashSet<>(frutas1);
diferencia.removeAll(frutas2); // [Manzana, Banana]
// Diferencia simétrica (elementos que están en uno u otro conjunto, pero no en ambos)
Set<String> difSimetrica = new HashSet<>(frutas1);
difSimetrica.addAll(frutas2); // Unión
Set<String> temp = new HashSet<>(frutas1);
temp.retainAll(frutas2); // Intersección
difSimetrica.removeAll(temp); // [Manzana, Banana, Pera, Uva]
CaracterísticaHashMapHashSet
Interfaz implementadaMapSet
AlmacenaPares clave-valorValores únicos
DuplicadosNo permite claves duplicadasNo permite elementos duplicados
Valor nuloPermite una clave nula y múltiples valores nulosPermite un elemento nulo
Implementación internaTabla hashHashMap (usa HashMap internamente)
Uso comúnAlmacenar asociaciones clave-valorAlmacenar conjuntos de elementos únicos

Los genéricos (Generics) permiten crear clases, interfaces y métodos que operan con tipos parametrizados. En el contexto de colecciones, los genéricos proporcionan seguridad de tipos en tiempo de compilación.

  1. Seguridad de tipos: Detecta errores de tipo en tiempo de compilación en lugar de en tiempo de ejecución
  2. Eliminación de castings: No es necesario hacer casting al recuperar elementos
  3. Código más legible: Especifica claramente qué tipos de objetos contiene una colección
  4. Reutilización de código: Permite escribir algoritmos genéricos que funcionan con diferentes tipos
// Sin generics (antes de Java 5)
List listaAntigua = new ArrayList();
listaAntigua.add("Hola");
listaAntigua.add(123); // Permitido, pero no seguro
String texto = (String) listaAntigua.get(0); // Casting necesario
// ClassCastException en tiempo de ejecución si intentamos:
// String numero = (String) listaAntigua.get(1);
// Con generics (Java 5+)
List<String> listaModerna = new ArrayList<>();
listaModerna.add("Hola");
// listaModerna.add(123); // Error de compilación
String texto2 = listaModerna.get(0); // No necesita casting

Los comodines permiten mayor flexibilidad al trabajar con genéricos.

// Lista de tipo desconocido
List<?> listaDesconocida;
// Puede asignarse cualquier tipo de lista
listaDesconocida = new ArrayList<String>();
listaDesconocida = new ArrayList<Integer>();
// Pero solo se puede leer Object
Object obj = listaDesconocida.get(0);
// Y no se puede añadir nada (excepto null)
// listaDesconocida.add("Hola"); // Error de compilación
listaDesconocida.add(null); // Permitido

Comodín con límite superior (? extends T)

Section titled “Comodín con límite superior (? extends T)”
import java.util.ArrayList;
import java.util.List;
// Clase base y subclases
class Animal {}
class Gato extends Animal {}
class Perro extends Animal {}
// Método que acepta listas de Animal o cualquier subclase de Animal
void procesarAnimales(List<? extends Animal> animales) {
// Podemos leer elementos como Animal
for (Animal animal : animales) {
System.out.println(animal);
}
// Pero no podemos añadir elementos
// animales.add(new Animal()); // Error de compilación
// animales.add(new Gato()); // Error de compilación
}
// Uso
List<Animal> listaAnimales = new ArrayList<>();
List<Gato> listaGatos = new ArrayList<>();
List<Perro> listaPerros = new ArrayList<>();
procesarAnimales(listaAnimales); // OK
procesarAnimales(listaGatos); // OK
procesarAnimales(listaPerros); // OK
import java.util.ArrayList;
import java.util.List;
class Animal {}
class Gato extends Animal {}
class GatoSiames extends Gato {}
// Método que acepta listas de Gato o cualquier superclase de Gato
void agregarGatos(List<? super Gato> destino) {
// Podemos añadir Gatos o subclases de Gato
destino.add(new Gato());
destino.add(new GatoSiames());
// Pero no podemos añadir superclases
// destino.add(new Animal()); // Error de compilación
// Y solo podemos leer como Object
Object obj = destino.get(0); // Solo podemos leer como Object
}
// Uso
List<Animal> listaAnimales = new ArrayList<>();
List<Gato> listaGatos = new ArrayList<>();
List<GatoSiames> listaGatosSiameses = new ArrayList<>();
agregarGatos(listaAnimales); // OK
agregarGatos(listaGatos); // OK
// agregarGatos(listaGatosSiameses); // Error, GatoSiames no es superclase de Gato

7.4.4 Principio PECS (Producer Extends, Consumer Super)

Section titled “7.4.4 Principio PECS (Producer Extends, Consumer Super)”

Una regla útil para recordar cuándo usar cada tipo de comodín:

  • Usa <? extends T> cuando solo necesites leer de una colección (la colección actúa como productor)
  • Usa <? super T> cuando solo necesites escribir en una colección (la colección actúa como consumidor)
  • Usa T exacto cuando necesites tanto leer como escribir
import java.util.ArrayList;
import java.util.List;
// Ejemplo del principio PECS
public void copiarElementos(List<? extends Number> origen, List<? super Number> destino) {
for (Number numero : origen) {
destino.add(numero);
}
}
// Uso
List<Integer> enteros = new ArrayList<>();
enteros.add(1);
enteros.add(2);
List<Number> numeros = new ArrayList<>();
List<Object> objetos = new ArrayList<>();
copiarElementos(enteros, numeros); // OK
copiarElementos(enteros, objetos); // OK
// copiarElementos(objetos, numeros); // Error, Object no es subclase de Number

En este capítulo, hemos explorado las principales estructuras para almacenar y manipular conjuntos de datos en Java:

  1. Arreglos: Estructuras de tamaño fijo que ofrecen acceso directo a elementos mediante índices.

  2. ArrayList y LinkedList: Implementaciones dinámicas de la interfaz List que permiten almacenar elementos ordenados con diferentes características de rendimiento.

  3. HashMap y HashSet: Colecciones basadas en tablas hash que ofrecen acceso eficiente a elementos mediante claves o almacenamiento de elementos únicos.

  4. Generics: Mecanismo que proporciona seguridad de tipos en tiempo de compilación y mayor flexibilidad al trabajar con colecciones.

La elección de la estructura de datos adecuada depende de los requisitos específicos de la aplicación, como el tipo de acceso a los datos, la frecuencia de modificaciones y las operaciones más comunes que se realizarán.

🐝