De todas las clases del Framework de Colecciones, ArrayList es la que más vas a escribir. En cualquier proyecto Java real, entre el 70 % y el 90 % de las colecciones son ArrayList, y con razón: combina el acceso instantáneo por posición de un array con el crecimiento automático de una colección, y su recorrido es el más rápido de todas las implementaciones de List. Es la respuesta por defecto cuando necesitas "una lista de cosas", y solo deberías cambiarla cuando tengas un motivo concreto y medido.
Precisamente porque la usarás tanto, merece la pena entenderla por dentro. En esta lección verás que un ArrayList es literalmente el CatalogoArray que escribiste en 05-01 —un array interno más un contador—, pero escrito por los ingenieros del JDK y afinado durante veinticinco años. Entender ese array interno explica de golpe casi todo: por qué get(i) es instantáneo, por qué remove(0) es caro, por qué existe un constructor que pide la capacidad, por qué add es O(1) "amortizado" aunque a veces copie un millón de elementos, y por qué subList puede sorprenderte. Al terminar, el catálogo de BiblioTech habrá dejado de ser un array para siempre.
Contenido
- Qué es un
ArrayListpor dentro - Capacidad frente a tamaño
- El redimensionado y el coste amortizado
- El constructor con capacidad inicial
- Crear y llenar una lista
- La API completa, método a método
remove(int)frente aremove(Object): la trampa clásicasubListes una vista- Tabla de complejidad de cada operación
- Recorrido y borrado seguro
ArrayListfrente a array- Conversión array ↔ lista
- Listas de listas
equalsyhashCodede una lista- Refactorización de BiblioTech
- Errores Comunes y Consejos
- Ejercicios
- Qué es un
ArrayList por dentro
ArrayList por dentroSi abres el código fuente de java.util.ArrayList en el JDK, lo primero que encuentras es esto (simplificado):
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess {
private static final int DEFAULT_CAPACITY = 10;
transient Object[] elementData; // el array interno donde viven los elementos
private int size; // cuantos elementos hay REALMENTE
}Dos campos. Un array y un entero. Es exactamente el CatalogoArray de 05-01: un Material[] materiales y un int n. La diferencia no está en la idea, sino en que aquí toda la gestión de capacidad, crecimiento y desplazamientos está escrita, probada y optimizada por otros.
flowchart TB
subgraph AL["objeto ArrayList"]
S["size = 4"]
E["elementData →"]
end
subgraph ARR["Object[] elementData (capacidad 10)"]
direction LR
C0["[0]"]
C1["[1]"]
C2["[2]"]
C3["[3]"]
C4["[4] null"]
C5["[5] null"]
C6["[6] null"]
C7["[7] null"]
C8["[8] null"]
C9["[9] null"]
end
E --> ARR
C0 --> L1["Libro<br/>Java Efectivo"]
C1 --> L2["Libro<br/>Patrones de Diseno"]
C2 --> L3["Libro<br/>Refactorizacion"]
C3 --> L4["Revista<br/>Java Magazine"]
De esa estructura se deduce todo lo demás:
get(i)es O(1): es un acceso directoelementData[i], igual que en un array.add(e)al final es O(1): escribir enelementData[size]e incrementarsize... salvo cuando el array se llena.add(0, e)yremove(0)son O(n): hay que desplazar todos los elementos siguientes.contains(e)eindexOf(e)son O(n): hay que recorrer comparando conequals.- Recorrer es rapidísimo: memoria contigua, la mejor localidad de caché posible (05-01).
Fíjate también en implements RandomAccess. Es una interfaz marcadora: no declara ningún método, solo dice "esta lista permite acceso por índice en tiempo constante". Algunos algoritmos del JDK, como Collections.binarySearch, consultan esa marca para elegir estrategia. LinkedList no la lleva.
- Capacidad frente a tamaño
Esta distinción es el origen de la mitad de los malentendidos con ArrayList, y ya la conoces del CatalogoArray:
| Concepto | Qué es | Cómo se consulta |
|---|---|---|
Tamaño (size) |
Cuántos elementos hay realmente | lista.size() |
| Capacidad | Cuántos caben antes de tener que crecer | No se puede consultar desde la API pública |
En el diagrama anterior, el tamaño es 4 y la capacidad es 10: hay seis celdas libres, invisibles desde fuera. Y ahí está la gran mejora respecto a los arrays: la capacidad es un detalle interno que no te concierne. Con Material[] catalogo = new Material[10], catalogo.length valía 10 aunque solo hubieras rellenado 4, y era tu trabajo llevar la cuenta. Con ArrayList, size() dice siempre la verdad.
List<String> nombres = new ArrayList<>();
System.out.println(nombres.size()); // 0 (capacidad interna: 10 en cuanto anadas el primero)
nombres.add("Marta Ruiz");
nombres.add("Diego Alonso");
System.out.println(nombres.size()); // 2Un detalle de eficiencia que introdujo Java 7: un new ArrayList<>() no reserva nada al construirse; apunta a un array compartido vacío y solo reserva las 10 posiciones cuando añades el primer elemento. Así, crear miles de listas que quizá nunca se usen no cuesta memoria.
Existe un método para devolver la capacidad sobrante al sistema:
ArrayList<Material> lista = new ArrayList<>(1000);
// ... solo se llenan 30 ...
lista.trimToSize(); // reduce la capacidad interna a 30Ojo: trimToSize() solo existe en ArrayList, no en la interfaz List, así que para llamarlo tendrías que declarar la variable como ArrayList. Es una de las poquísimas excepciones legítimas a la regla de oro de 05-02, y solo tiene sentido en listas enormes que ya no van a crecer.
- El redimensionado y el coste amortizado
¿Qué pasa cuando añades el elemento número 11 a una lista de capacidad 10? Exactamente lo que hacía tu CatalogoArray:
// dentro de ArrayList, simplificado:
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // capacidad * 1.5
return elementData = Arrays.copyOf(elementData, newCapacity);
}oldCapacity >> 1 es un desplazamiento de bits a la derecha, es decir, dividir entre dos. Así que la nueva capacidad es 1,5 veces la anterior. Y Arrays.copyOf crea un array nuevo y copia todos los elementos: una operación O(n).
flowchart LR
A["Array de capacidad 10<br/>size = 10, LLENO"] --> B["add(elemento 11)"]
B --> C["new Object[15]"]
C --> D["copiar los 10 elementos: O(n)"]
D --> E["elementData apunta al array nuevo"]
E --> F["el array viejo queda para el recolector"]
F --> G["escribir el elemento 11<br/>size = 11"]
La secuencia de capacidades partiendo de una lista vacía es:
| Al añadir el elemento nº | Capacidad antes | Capacidad después | ¿Hubo copia? | Elementos copiados |
|---|---|---|---|---|
| 1 | 0 | 10 | Reserva inicial | 0 |
| 11 | 10 | 15 | Sí | 10 |
| 16 | 15 | 22 | Sí | 15 |
| 23 | 22 | 33 | Sí | 22 |
| 34 | 33 | 49 | Sí | 33 |
| 50 | 49 | 73 | Sí | 49 |
| 74 | 73 | 109 | Sí | 73 |
Por qué add sigue siendo O(1)
Si algunas llamadas a add cuestan O(n), ¿cómo puede la tabla de 05-02 decir que add es O(1)? Porque es O(1) amortizado, y merece la pena entender la idea porque aparece en muchas estructuras de datos.
Amortizado significa coste medio por operación a lo largo de una secuencia larga, no coste de una operación aislada. Haz la cuenta: para llegar a 1000 elementos, el número total de elementos copiados en todos los redimensionados es de aproximadamente 2000 (la suma de una serie geométrica de razón 1,5 converge a unas dos veces el tamaño final). Es decir, unas 2 copias por elemento añadido, de media, sin importar si añades mil o un millón. Dos operaciones por elemento es una constante, y una constante es O(1).
La clave está en que el array crece multiplicativamente (×1,5), no aditivamente. Si creciera de 10 en 10, para llegar a 1000 elementos habría 100 redimensionados que copiarían 10+20+30+...+990 ≈ 50 000 elementos: 50 por elemento añadido, y creciendo. Eso sí sería un problema, y sería O(n) amortizado.
La analogía útil: mudarse de casa es carísimo, pero si solo te mudas cuando duplicas tus pertenencias, el coste medio por objeto que acumulas es constante.
- El constructor con capacidad inicial
Hay tres constructores, y saber cuándo usar el segundo es un detalle de profesional:
List<Material> a = new ArrayList<>(); // capacidad por defecto (10 al llenar)
List<Material> b = new ArrayList<>(500); // capacidad inicial 500
List<Material> c = new ArrayList<>(otraColeccion); // copia con el tamano justo¿Cuándo importa la capacidad inicial? Cuando sabes de antemano, aunque sea aproximadamente, cuántos elementos vas a meter y son muchos.
// Cargar el catalogo completo: sabemos que son unos 5000 materiales
List<Material> catalogo = new ArrayList<>(5000);
for (String linea : lineasDelFichero) {
catalogo.add(convertir(linea));
}Sin la capacidad inicial, llegar a 5000 elementos habría provocado unos 20 redimensionados y unas 10 000 copias de referencias. Con ella, cero. Para 5000 elementos la diferencia es de milisegundos; para varios millones, notable.
¿Cuándo NO importa? Casi siempre. Para listas de decenas o cientos de elementos, new ArrayList<>() es perfecto y añadir un número mágico solo ensucia el código. No caigas en la microoptimización: usa el constructor con capacidad cuando el tamaño sea grande y previsible, y el vacío el resto de las veces.
El tercer constructor es el que usarás para copias defensivas (03-07) y para convertir entre colecciones:
List<Material> copiaIndependiente = new ArrayList<>(catalogo); // copia superficial modificable
List<String> deUnSet = new ArrayList<>(conjuntoISBN);
List<String> deUnaListaFija = new ArrayList<>(List.of("a", "b", "c")); // ahora si es modificable
- Crear y llenar una lista
import java.util.ArrayList;
import java.util.List;
List<Material> catalogo = new ArrayList<>();
catalogo.add(new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018));
catalogo.add(new Libro("Patrones de Diseno", "Erich Gamma", "978-0000000002", 1994));
catalogo.add(new Libro("Refactorizacion", "Martin Fowler", "978-0000000003", 1999));
catalogo.add(new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"));
catalogo.add(new Dvd("Refactorizacion en vivo", "DVD-0007", 95));Y las formas abreviadas cuando ya tienes los elementos:
// Lista MODIFICABLE a partir de elementos sueltos
List<String> empleados = new ArrayList<>(List.of("Marta Ruiz", "Diego Alonso", "Nuria Vidal"));
// Lista INMUTABLE (cuidado: add lanzara UnsupportedOperationException)
List<String> fijos = List.of("Marta Ruiz", "Diego Alonso", "Nuria Vidal");
// Anadir varios de golpe a una lista existente
catalogo.addAll(otrosMateriales);
Collections.addAll(catalogo, material1, material2, material3);El idioma new ArrayList<>(List.of(...)) es el más práctico para inicializar una lista modificable con contenido: dice lo que hace y ocupa una línea.
- La API completa, método a método
Añadir
List<String> l = new ArrayList<>(List.of("A", "B", "C"));
l.add("D"); // [A, B, C, D] al final, O(1) amortizado
l.add(1, "X"); // [A, X, B, C, D] en la posicion 1, O(n): desplaza el resto
l.addAll(List.of("E","F")); // [A, X, B, C, D, E, F]
l.addAll(0, List.of("Z")); // [Z, A, X, B, C, D, E, F]add(int, E) acepta índices de 0 a size() (ambos incluidos: size() significa "al final"). Fuera de ese rango, IndexOutOfBoundsException — cómo tratarla es el módulo 6.
Leer y escribir
String primero = l.get(0); // O(1)
String ultimo = l.get(l.size()-1); // el idioma para el ultimo elemento
String anterior = l.set(1, "Y"); // SUSTITUYE y devuelve lo que habia. O(1)Confundir add(1, "Y") con set(1, "Y") es un error frecuente: el primero inserta (la lista crece), el segundo reemplaza (la lista mantiene su tamaño).
Eliminar
l.remove(0); // por INDICE, devuelve el elemento eliminado
l.remove("B"); // por OBJETO, devuelve boolean. Usa equals
l.removeIf(s -> s.isEmpty()); // por CRITERIO, devuelve boolean
l.clear(); // vacia la listaLos tres primeros son O(n) en el caso general: eliminar en la posición i obliga a desplazar los size - i - 1 elementos posteriores con un System.arraycopy interno, exactamente como hacía tu CatalogoArray. Solo remove(size()-1) es O(1).
Buscar
boolean hay = l.contains("C"); // O(n), usa equals
int pos = l.indexOf("C"); // primera aparicion, o -1 si no esta. O(n)
int ultima = l.lastIndexOf("C"); // ultima aparicioncontains e indexOf dependen por completo de equals. Esto conecta directamente con 03-09: si tu clase no lo sobrescribe, hereda el de Object, que compara referencias, y un objeto "igual pero distinto" no se encontrará nunca:
class MaterialSinEquals { /* no sobrescribe equals */ }
List<Ficha> fichas = new ArrayList<>();
fichas.add(new Ficha("Java Efectivo", "Joshua Bloch", 2018));
// Ficha es un record: equals generado, compara componentes
System.out.println(fichas.contains(new Ficha("Java Efectivo", "Joshua Bloch", 2018))); // true
// Si Ficha fuera una clase normal sin equals:
// System.out.println(...) -> false, aunque los datos sean identicosRegla práctica: si una clase va a vivir en una colección y se va a buscar, necesita equals (y hashCode). En 05-05 verás por qué los dos, siempre juntos.
Transformar y ordenar
l.replaceAll(String::toUpperCase); // aplica una UnaryOperator a cada elemento
l.sort(Comparator.naturalOrder()); // ordena en el sitio
l.sort(Comparator.comparing(Material::getTitulo));
l.forEach(System.out::println); // recorre aplicando un ConsumerLos cuatro llegaron en Java 8 y usan las interfaces funcionales de 04-06. replaceAll es especialmente útil y poco conocido: modifica cada elemento en el sitio, sin crear otra lista.
List<Material> catalogo = ...;
catalogo.sort(Comparator.comparing(Material::getTipo)
.thenComparing(Material::getTitulo));Prefiere lista.sort(comparador) sobre Collections.sort(lista, comparador): es el método propio de la interfaz y no necesita clase de utilidades.
Consultar
remove(int) frente a remove(Object): la trampa clásica
remove(int) frente a remove(Object): la trampa clásicaList tiene dos métodos remove sobrecargados:
Con la mayoría de los tipos no hay ambigüedad, pero con List<Integer> la hay, y produce el bug más famoso del Framework:
List<Integer> numeros = new ArrayList<>(List.of(10, 20, 30, 40));
numeros.remove(1); // que elimina?
System.out.println(numeros); // [10, 30, 40] <- elimino la POSICION 1, no el valor 1La resolución de sobrecarga (03-03) prefiere siempre la coincidencia exacta sin conversiones: 1 es un int, así que llama a remove(int index) sin autoboxing. Si tu intención era eliminar el valor 20, el resultado es correcto por casualidad; si querías eliminar el valor 1 de una lista que empieza por 10, has borrado el elemento equivocado en silencio.
Peor todavía:
List<Integer> ids = new ArrayList<>(List.of(100, 200, 300));
ids.remove(100); // IndexOutOfBoundsException: Index 100 out of bounds for length 3Buscabas el valor 100 y has pedido la posición 100.
Las soluciones
List<Integer> numeros = new ArrayList<>(List.of(10, 20, 30, 40));
numeros.remove(Integer.valueOf(20)); // 1. explicito: por VALOR
numeros.remove((Integer) 30); // 2. casting: por VALOR
numeros.remove(1); // 3. por POSICION (queda claro que es lo que quieres)Integer.valueOf(20) es la forma preferida por legibilidad. Y una regla general muy útil: desconfía de las sobrecargas cuando uses colecciones de Integer. El mismo problema no ocurre con List<String> ni con List<Material>, porque remove("B") no puede confundirse con un índice.
Llamada sobre List<Integer> |
Se resuelve como | Efecto |
|---|---|---|
lista.remove(2) |
remove(int) |
Elimina el elemento en la posición 2 |
lista.remove(Integer.valueOf(2)) |
remove(Object) |
Elimina el valor 2 |
lista.remove((Integer) 2) |
remove(Object) |
Elimina el valor 2 |
lista.indexOf(2) |
Solo hay una versión | Busca el valor 2 (autoboxing) |
lista.contains(2) |
Solo hay una versión | Busca el valor 2 (autoboxing) |
Fíjate en las dos últimas filas: contains e indexOf solo aceptan Object, así que ahí el autoboxing sí ocurre y todo funciona como esperas. La ambigüedad es exclusiva de remove.
subList es una vista
subList es una vistasubList(desde, hasta) devuelve la porción de la lista entre esos índices (desde incluido, hasta excluido). Pero no es una copia: es una vista sobre la lista original, igual que Arrays.asList era una vista sobre el array.
List<String> l = new ArrayList<>(List.of("A", "B", "C", "D", "E"));
List<String> centro = l.subList(1, 4); // [B, C, D]
centro.set(0, "X");
System.out.println(l); // [A, X, C, D, E] <- ha cambiado la ORIGINAL
centro.clear();
System.out.println(l); // [A, E] <- se han borrado de la ORIGINALEse comportamiento es intencionado y muy potente —clear() sobre una sublista es el idioma estándar para eliminar un rango—, pero hay que conocerlo, porque las sorpresas son de dos tipos:
1. Los cambios se propagan en ambos sentidos. Modificar la vista modifica la original y viceversa.
2. Modificar la original estructuralmente invalida la vista. Si añades o quitas elementos en la lista original directamente, cualquier uso posterior de la sublista lanza ConcurrentModificationException, por el mismo mecanismo de modCount que viste en 05-02:
List<String> l = new ArrayList<>(List.of("A", "B", "C", "D", "E"));
List<String> centro = l.subList(1, 4);
l.add("F"); // modificacion estructural de la original
System.out.println(centro); // ConcurrentModificationExceptionSi lo que quieres es una copia independiente, envuélvela:
Usos legítimos de subList tal cual:
// Paginar resultados sin copiar la lista entera
int desde = pagina * porPagina;
int hasta = Math.min(desde + porPagina, resultados.size());
List<Material> paginaActual = resultados.subList(desde, hasta);
// Borrar un rango completo
catalogo.subList(0, 10).clear(); // elimina los 10 primeros
// Ordenar solo un tramo
catalogo.subList(0, 5).sort(Comparator.comparing(Material::getTitulo));
- Tabla de complejidad de cada operación
Esta tabla es la que te permitirá decidir con criterio en 05-04:
| Operación | Complejidad | Por qué |
|---|---|---|
get(i) |
O(1) | Acceso directo elementData[i] |
set(i, e) |
O(1) | Escritura directa |
add(e) (al final) |
O(1) amortizado | Escribir y sumar 1; ocasionalmente copiar todo |
add(0, e) (al principio) |
O(n) | Desplazar todos los elementos a la derecha |
add(i, e) (en medio) |
O(n) | Desplazar los n - i posteriores |
remove(size()-1) (último) |
O(1) | Poner a null y restar 1 |
remove(0) (primero) |
O(n) | Desplazar todos los posteriores a la izquierda |
remove(i) |
O(n) | Desplazar los n - i - 1 posteriores |
remove(Object) |
O(n) | Buscar (O(n)) + desplazar (O(n)) |
contains(e) / indexOf(e) |
O(n) | Recorrer comparando con equals |
size() / isEmpty() |
O(1) | Es un campo, no se cuenta nada |
clear() |
O(n) | Pone todas las celdas a null para no filtrar memoria |
Recorrer con for-each |
O(n), muy rápido | Memoria contigua: localidad de caché óptima |
sort(cmp) |
O(n log n) | TimSort (05-09) |
contains tras sort + binarySearch |
O(log n) | Solo si la mantienes ordenada (05-09) |
El mensaje que hay que llevarse: ArrayList es excelente por los índices y por el final de la lista, y mediocre por el principio y por el medio. Si tu programa hace lista.add(0, x) o lista.remove(0) dentro de un bucle sobre miles de elementos, estás haciendo O(n²) sin darte cuenta, y es el momento de mirar un ArrayDeque (05-07).
- Recorrido y borrado seguro
Las tres formas de recorrer, con su criterio:
// 1. for-each: por defecto, cuando solo lees
for (Material m : catalogo) {
System.out.println(m.describir());
}
// 2. for clasico con indice: cuando necesitas la posicion o escribes con set
for (int i = 0; i < catalogo.size(); i++) {
System.out.printf("%2d. %s%n", i + 1, catalogo.get(i).getTitulo());
}
// 3. forEach con Consumer: cuando ya tienes la accion como referencia a metodo
catalogo.forEach(System.out::println);Un aviso sobre la opción 2: catalogo.get(i) es O(1) en ArrayList, pero será O(n) en LinkedList. Un bucle indexado sobre una LinkedList es O(n²) y es uno de los errores de rendimiento más habituales. El for-each es correcto en ambas.
Y el borrado durante el recorrido, retomando lo de 05-02 con los idiomas concretos:
// CORRECTO Y PREFERIDO: una linea
catalogo.removeIf(m -> !m.estaDisponible());
// CORRECTO: iterador explicito, cuando la logica es compleja
Iterator<Material> it = catalogo.iterator();
while (it.hasNext()) {
Material m = it.next();
if (!m.estaDisponible()) {
registrarBaja(m); // efecto colateral antes de borrar
it.remove();
}
}
// CORRECTO: for clasico HACIA ATRAS, cuando necesitas el indice
for (int i = catalogo.size() - 1; i >= 0; i--) {
if (!catalogo.get(i).estaDisponible()) {
System.out.println("Baja en la posicion " + i);
catalogo.remove(i);
}
}
// INCORRECTO: ConcurrentModificationException
for (Material m : catalogo) {
if (!m.estaDisponible()) { catalogo.remove(m); }
}Un detalle de eficiencia poco conocido: removeIf sobre un ArrayList está implementado en una sola pasada que marca los elementos a eliminar y luego compacta el array de golpe, así que es O(n). Un bucle que llame a remove(Object) n veces sería O(n²). Otra razón para preferir removeIf.
ArrayList frente a array
ArrayList frente a array| Aspecto | Material[] |
List<Material> |
|---|---|---|
| Tamaño | Fijo al crear | Crece y mengua solo |
| Añadir / eliminar | A mano con copyOf / arraycopy |
add / remove |
| Tamaño real | length es la capacidad; hay que llevar un contador |
size() es la verdad |
| Acceso por índice | a[i] — el más rápido posible |
get(i) — O(1), con una llamada de método |
| Primitivos | int[] sin envoltorios |
List<Integer> con autoboxing |
Memoria (1 M de int) |
~4 MB | ~20 MB |
| Recorrido | El más rápido | Muy rápido (mismo array por dentro) |
| Buscar por criterio | Bucle a mano | contains, indexOf, removeIf |
| Ordenar | Arrays.sort |
lista.sort |
| Multidimensional | int[][] natural |
List<List<Integer>>, más verboso |
| Seguridad de tipos | Covariante: ArrayStoreException en ejecución |
Genéricos: error en compilación |
| API disponible | La clase Arrays |
Toda la interfaz List y Collections |
Cuándo el array sigue siendo mejor:
- Primitivos en cantidad.
int[],double[],byte[]. Un buffer de bytes de E/S (módulo 7) siempre esbyte[]. - Tamaño fijo por naturaleza. Una matriz 3×12, un tablero de ajedrez, los 256 valores de una tabla de conversión.
- Máximo rendimiento en cálculo numérico, donde la localidad de caché y la ausencia de envoltorios marcan la diferencia.
- Implementar estructuras de datos, como hacen
ArrayList,HashMapyArrayDequepor dentro. - APIs que lo exigen:
main(String[] args),String.split,toArray.
Para todo lo demás, ArrayList.
- Conversión array ↔ lista
Es una operación cotidiana y cada dirección tiene su trampa.
De lista a array
List<Material> lista = new ArrayList<>(...);
Material[] array = lista.toArray(new Material[0]); // EL IDIOMA CORRECTO
Object[] malo = lista.toArray(); // pierde el tipo: casi nunca utilEl new Material[0] no se desperdicia: le indica al método el tipo de array que debe crear. Como el array pasado es demasiado pequeño, toArray crea internamente uno del tamaño correcto y devuelve ese. Sorprendentemente, new Material[0] suele ser más rápido que new Material[lista.size()] en las JVM modernas, porque evita rellenar de ceros un array que se va a sobrescribir entero. Usa siempre new T[0].
El array resultante es independiente: añadir a la lista después no lo cambia. Pero es una copia superficial: los objetos son los mismos.
De array a lista
Tres formas, con comportamientos muy distintos:
Material[] array = { libro1, libro2, libro3 };
// 1. VISTA de tamano fijo respaldada por el array (05-01)
List<Material> vista = Arrays.asList(array);
vista.set(0, otro); // OK, y modifica array[0]
// vista.add(otro); // UnsupportedOperationException
// 2. Lista MODIFICABLE e INDEPENDIENTE <- lo que quieres casi siempre
List<Material> modificable = new ArrayList<>(Arrays.asList(array));
modificable.add(otro); // OK, y no toca 'array'
// 3. Lista INMUTABLE e independiente (Java 9+)
List<Material> inmutable = List.of(array);
// inmutable.set(0, otro); // UnsupportedOperationException| Forma | Modificable | ¿Refleja cambios del array? | Cuándo usarla |
|---|---|---|---|
Arrays.asList(a) |
Solo set |
Sí, en ambos sentidos | Envolver un array para pasarlo a una API que pide List |
new ArrayList<>(Arrays.asList(a)) |
Totalmente | No | La opción por defecto |
List.of(a) |
No | No | Constantes; rechaza null |
Y las dos trampas que ya conoces de 05-01, que aquí vuelven a morder:
int[] primitivos = { 1, 2, 3 };
List<int[]> mal = Arrays.asList(primitivos); // UNA lista de UN elemento (el array entero)
System.out.println(mal.size()); // 1
Integer[] envueltos = { 1, 2, 3 };
List<Integer> bien = Arrays.asList(envueltos); // 3 elementos, como esperabas
System.out.println(bien.size()); // 3Los genéricos solo funcionan con tipos referencia (10-01), así que un int[] se toma como un único objeto. Para pasar de int[] a List<Integer> sin Streams, el bucle explícito es la vía:
List<Integer> lista = new ArrayList<>(primitivos.length);
for (int n : primitivos) { lista.add(n); } // autoboxing en cada addEn el módulo 10 verás que Arrays.stream(primitivos).boxed().toList() hace lo mismo en una expresión.
- Listas de listas
Como una List puede contener cualquier tipo, puede contener otras listas. Es el equivalente flexible del array bidimensional de 05-01:
List<List<Material>> porTipo = new ArrayList<>();
List<Material> libros = new ArrayList<>();
libros.add(new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018));
libros.add(new Libro("Refactorizacion", "Martin Fowler", "978-0000000003", 1999));
List<Material> revistas = new ArrayList<>();
revistas.add(new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"));
porTipo.add(libros);
porTipo.add(revistas);
porTipo.add(new ArrayList<>()); // los DVD, todavia sin ninguno
// Acceso: dos indices, igual que en un array bidimensional
System.out.println(porTipo.get(0).get(1).getTitulo()); // Refactorizacion
// Recorrido anidado
for (List<Material> grupo : porTipo) {
System.out.println("Grupo de " + grupo.size() + " materiales:");
for (Material m : grupo) {
System.out.println(" " + m.getTitulo());
}
}La ventaja frente a Material[][] es que cada sublista crece por su cuenta, sin dimensionar nada de antemano.
Pero fíjate en la debilidad: el índice 0 significa "libros" por convenio, y ese convenio no está escrito en ninguna parte. Es exactamente el problema que resuelve un Map<String, List<Material>> de 05-05, donde la clave dice explícitamente qué contiene cada grupo.
Y una advertencia sobre la inicialización:
List<List<String>> matriz = new ArrayList<>();
for (int i = 0; i < 3; i++) {
matriz.add(new ArrayList<>()); // cada fila necesita SU PROPIA lista
}
matriz.get(0).add("dato"); // ahora siSi olvidas crear cada sublista, matriz.get(0) devuelve null y el add lanza NullPointerException. Y si añades tres veces la misma lista (List<String> fila = new ArrayList<>(); matriz.add(fila); matriz.add(fila);), las tres filas serán la misma por aliasing (03-02), y escribir en una las cambia todas.
equals y hashCode de una lista
equals y hashCode de una listaAbstractList sí sobrescribe equals y hashCode, a diferencia de los arrays. Dos listas son iguales si tienen los mismos elementos en el mismo orden:
List<String> a = new ArrayList<>(List.of("Marta Ruiz", "Diego Alonso"));
List<String> b = new LinkedList<>(List.of("Marta Ruiz", "Diego Alonso"));
List<String> c = new ArrayList<>(List.of("Diego Alonso", "Marta Ruiz"));
System.out.println(a.equals(b)); // true <- la implementacion NO importa
System.out.println(a.equals(c)); // false <- el ORDEN si importa
System.out.println(a.hashCode() == b.hashCode()); // trueQue a.equals(b) sea true siendo b una LinkedList es deliberado: el contrato de List.equals compara contenido, no clase. Es una diferencia importante con los arrays, donde equals compara referencias y hacía falta Arrays.equals.
El hashCode se calcula así, y explica que dos listas iguales lo tengan igual:
Consecuencia práctica: el hashCode de una lista depende de sus elementos, así que cambia si la lista cambia. Usar una lista mutable como clave de un HashMap o como elemento de un HashSet es una receta para el desastre; lo verás demostrado en 05-05. Si necesitas una lista como clave, usa una inmutable (List.of(...) o List.copyOf(...)).
Y ojo: para que equals de la lista funcione, los elementos deben tener equals correcto. Una List<Ficha> compara bien porque Ficha es un record; una lista de objetos sin equals compararía por referencia elemento a elemento.
- Refactorización de BiblioTech
Es el momento de convertir el catálogo definitivamente. Compara el método buscarPorTipo en sus dos versiones.
Antes: con arrays
/** Version del modulo 4, con Material[] y contador manual. */
public Material[] buscarPorTipo(String tipo) {
Material[] resultado = new Material[n]; // hay que reservar el MAXIMO posible
int encontrados = 0;
for (int i = 0; i < n; i++) { // hasta n, no hasta materiales.length
if (materiales[i] != null && materiales[i].getTipo().equals(tipo)) {
resultado[encontrados] = materiales[i];
encontrados++;
}
}
return Arrays.copyOf(resultado, encontrados); // y recortar al final
}Cuatro problemas en once líneas: reservar de más, llevar un contador aparte, recorrer solo hasta n sin olvidarlo, y recortar al devolver.
Después: con List
/** Version del modulo 5. */
public List<Material> buscarPorTipo(String tipo) {
List<Material> resultado = new ArrayList<>();
for (Material m : materiales) {
if (m.getTipo().equals(tipo)) { resultado.add(m); }
}
return resultado;
}Y con el Predicate de 04-06, un solo método sirve para cualquier criterio:
public List<Material> buscar(Predicate<Material> criterio) {
List<Material> resultado = new ArrayList<>();
for (Material m : materiales) {
if (criterio.test(m)) { resultado.add(m); }
}
return resultado;
}El Catalogo completo
package com.nexussoftware.bibliotech.servicio;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import com.nexussoftware.bibliotech.dominio.Material;
/** Catalogo de BiblioTech sobre ArrayList. */
public class Catalogo {
private final List<Material> materiales;
private final Set<String> referencias = new HashSet<>(); // unicidad en O(1)
public Catalogo() {
this.materiales = new ArrayList<>();
}
/** Capacidad inicial: util solo cuando cargamos un catalogo grande de golpe. */
public Catalogo(int materialesPrevistos) {
this.materiales = new ArrayList<>(Math.max(materialesPrevistos, 10));
}
public boolean anadir(Material m) {
if (m == null) { return false; }
if (!referencias.add(m.getReferencia())) {
return false; // referencia duplicada
}
return materiales.add(m); // siempre true en una List
}
/** Inserta en una posicion concreta. O(n): desplaza el resto. */
public boolean insertar(int posicion, Material m) {
if (m == null || posicion < 0 || posicion > materiales.size()) { return false; }
if (!referencias.add(m.getReferencia())) { return false; }
materiales.add(posicion, m);
return true;
}
public boolean eliminar(String referencia) {
if (!referencias.remove(referencia)) { return false; }
return materiales.removeIf(m -> m.getReferencia().equals(referencia));
}
/** Sustituye el material de una posicion conservando el tamano. */
public Material reemplazar(int posicion, Material nuevo) {
Material anterior = materiales.set(posicion, nuevo); // devuelve el que habia
referencias.remove(anterior.getReferencia());
referencias.add(nuevo.getReferencia());
return anterior;
}
public Material obtener(int posicion) { return materiales.get(posicion); } // O(1)
public boolean contiene(Material m) { return materiales.contains(m); } // O(n), usa equals
public int posicionDe(Material m) { return materiales.indexOf(m); } // O(n)
public List<Material> buscar(Predicate<Material> criterio) {
List<Material> resultado = new ArrayList<>();
for (Material m : materiales) {
if (criterio.test(m)) { resultado.add(m); }
}
return resultado;
}
public int contar(Predicate<Material> criterio) {
int n = 0;
for (Material m : materiales) {
if (criterio.test(m)) { n++; }
}
return n;
}
public void ordenar(Comparator<Material> criterio) { materiales.sort(criterio); }
/** Pagina de resultados usando subList, con los limites bien controlados. */
public List<Material> pagina(int numeroPagina, int porPagina) {
int desde = numeroPagina * porPagina;
if (desde >= materiales.size() || desde < 0) { return List.of(); }
int hasta = Math.min(desde + porPagina, materiales.size());
return new ArrayList<>(materiales.subList(desde, hasta)); // COPIA, no vista
}
/** Copia inmutable: nadie de fuera puede alterar el catalogo. */
public List<Material> listar() { return List.copyOf(materiales); }
/** Cuando una API antigua pide un array. */
public Material[] comoArray() { return materiales.toArray(new Material[0]); }
public int tamano() { return materiales.size(); }
public boolean vacio() { return materiales.isEmpty(); }
public void vaciar() { materiales.clear(); referencias.clear(); }
}Uso completo:
Catalogo catalogo = new Catalogo();
catalogo.anadir(new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018));
catalogo.anadir(new Libro("Patrones de Diseno", "Erich Gamma", "978-0000000002", 1994));
catalogo.anadir(new Libro("Refactorizacion", "Martin Fowler", "978-0000000003", 1999));
catalogo.anadir(new Revista("Java Magazine", "REV-2024-03", 42, "Mensual"));
catalogo.anadir(new Dvd("Refactorizacion en vivo", "DVD-0007", 95));
catalogo.ordenar(Comparator.comparing(Material::getTipo)
.thenComparing(Material::getTitulo));
System.out.println("--- Catalogo (" + catalogo.tamano() + " materiales) ---");
catalogo.listar().forEach(m -> System.out.println(" " + m.describir()));
List<Material> baratos = catalogo.buscar(m -> m.getTarifaDiaria() < 0.30);
System.out.println("Tarifa inferior a 0,30 EUR/dia: " + baratos.size());
System.out.println("Pagina 0 (2 por pagina):");
catalogo.pagina(0, 2).forEach(m -> System.out.println(" " + m.getTitulo()));--- Catalogo (5 materiales) --- [DVD] Refactorizacion en vivo (DVD-0007) [Libro] Java Efectivo - Joshua Bloch (978-0000000001) [Libro] Patrones de Diseno - Erich Gamma (978-0000000002) [Libro] Refactorizacion - Martin Fowler (978-0000000003) [Revista] Java Magazine num. 42 (REV-2024-03) Tarifa inferior a 0,30 EUR/dia: 4 Pagina 0 (2 por pagina): Refactorizacion en vivo Java Efectivo
Comparado con el CatalogoArray de 05-01, ha desaparecido toda la gestión de memoria y han aparecido operaciones que antes eran impensables: paginación, inserción posicional, reemplazo, unicidad garantizada. Y sigue quedando una debilidad, deliberada: eliminar y cualquier búsqueda por referencia recorren la lista entera. Con 5 materiales da igual; con 50 000, no. Ese es el trabajo de 05-05.
Errores Comunes y Consejos
lista.length en lugar de lista.size(). length es de arrays, length() de String, size() de colecciones.
Confundir add(i, e) con set(i, e). El primero inserta y la lista crece; el segundo reemplaza y el tamaño no cambia. Si tu lista crece cuando no debía, mira aquí.
remove(int) en una List<Integer>. lista.remove(2) elimina la posición 2, no el valor 2. Usa remove(Integer.valueOf(2)) para eliminar por valor.
Modificar la lista dentro de un for-each. ConcurrentModificationException. Usa removeIf, Iterator.remove() o recorre hacia atrás con índices.
Bucle indexado sobre una lista que podría no ser ArrayList. for (int i = 0; i < lista.size(); i++) lista.get(i) es O(n) en ArrayList y O(n²) en LinkedList. Si el parámetro es List, recorre con for-each.
lista.add(0, x) o lista.remove(0) dentro de un bucle. Cada llamada desplaza toda la lista: O(n²) en total. Si necesitas trabajar por el principio, usa un ArrayDeque (05-07).
Creer que subList es una copia. Es una vista: modificarla modifica la original, y modificar la original estructuralmente la invalida con ConcurrentModificationException. Si quieres una copia, new ArrayList<>(lista.subList(a, b)).
Esperar que Arrays.asList devuelva una lista modificable. Es de tamaño fijo: set sí, add/remove lanzan UnsupportedOperationException. Usa new ArrayList<>(Arrays.asList(...)).
Arrays.asList sobre un int[]. Devuelve una lista de un elemento. Necesitas Integer[] o un bucle explícito.
Buscar con contains en una clase sin equals. Nunca encontrará nada, aunque los datos coincidan. Repasa 03-09.
Devolver la lista interna desde un getter. Quien llame podrá vaciarla. Devuelve List.copyOf(materiales) o, como mínimo, Collections.unmodifiableList(...) sabiendo que es una vista (05-02).
Consejo: isEmpty() en vez de size() == 0. Se lee mejor y en algunas implementaciones es más rápido.
Consejo: capacidad inicial solo cuando importe. Con miles o millones de elementos previstos, new ArrayList<>(n) ahorra redimensionados. Con veinte, es ruido.
Ejercicios
Ejercicio 1: gestor de préstamos con ArrayList
Escribe GestorPrestamos con un List<Prestamo> interno y estos métodos:
void registrar(Prestamo p).boolean devolver(String referencia, int dia): busca el préstamo por su referencia (PR-0001), invocaregistrarDevolucion(dia)y devuelvetruesi lo encontró.List<Prestamo> vencidos(int diaActual): los que estén vencidos y sin devolver.int purgarDevueltos(): elimina los ya devueltos conremoveIfy devuelve cuántos quitó.List<Prestamo> ultimos(int cuantos): loscuantosúltimos registrados, usandosubListcon los límites bien controlados y devolviendo una copia.Prestamo masAntiguo(): el de menordiaPrestamo, onullsi no hay ninguno.
Ejercicio 2: la trampa de remove documentada
Escribe una clase DemostracionRemove con un método main que:
- Cree un
List<Integer>con los valores 10, 20, 30, 40, 50. - Muestre qué ocurre con
remove(1),remove(Integer.valueOf(20))yremove((Integer) 30), imprimiendo la lista tras cada operación. - Muestre por qué
remove(100)lanzaríaIndexOutOfBoundsException(sin ejecutarlo: coméntalo y explica). - Cree un
List<String>equivalente y explique por qué ahí la ambigüedad no existe. - Escriba un método
boolean eliminarValor(List<Integer> lista, int valor)seguro y reutilizable.
Ejercicio 3: conversiones y sus trampas
Escribe ConversorColecciones con métodos estáticos que demuestren, cada uno con su salida por consola:
void demostrarAsList(): queArrays.asListes una vista de tamaño fijo respaldada por el array (modifica por ambos lados y muestra qué operaciones fallan).void demostrarCopiaIndependiente(): quenew ArrayList<>(Arrays.asList(a))es independiente.void demostrarToArray(): la conversión de vuelta contoArray(new String[0]).void demostrarPrimitivos(): la trampa deArrays.asList(int[])frente aArrays.asList(Integer[]).List<Integer> aLista(int[] primitivos): la conversión correcta con un bucle.
Soluciones
Solución 1
package com.nexussoftware.bibliotech.servicio;
import java.util.ArrayList;
import java.util.List;
import com.nexussoftware.bibliotech.dominio.Prestamo;
public class GestorPrestamos {
private final List<Prestamo> prestamos = new ArrayList<>();
public void registrar(Prestamo p) {
if (p != null) { prestamos.add(p); } // add al final: O(1) amortizado
}
/**
* Busqueda lineal O(n) por referencia. Con muchos prestamos habria que
* indexar por referencia en un Map<String, Prestamo>: eso es 05-05.
*/
public boolean devolver(String referencia, int dia) {
if (referencia == null) { return false; }
for (Prestamo p : prestamos) { // for-each: solo leemos
if (referencia.equals(p.getReferencia())) {
p.registrarDevolucion(dia); // modificamos el OBJETO, no la lista: legal
return true;
}
}
return false;
}
public List<Prestamo> vencidos(int diaActual) {
List<Prestamo> resultado = new ArrayList<>();
for (Prestamo p : prestamos) {
if (!p.estaDevuelto() && p.estaVencido(diaActual)) {
resultado.add(p);
}
}
return resultado; // lista vacia si no hay ninguno, NUNCA null
}
/**
* removeIf hace una sola pasada O(n). Un bucle con remove(Object)
* seria O(n^2) y ademas daria ConcurrentModificationException.
*/
public int purgarDevueltos() {
int antes = prestamos.size();
prestamos.removeIf(Prestamo::estaDevuelto); // referencia a metodo (04-06)
return antes - prestamos.size();
}
public List<Prestamo> ultimos(int cuantos) {
if (cuantos <= 0 || prestamos.isEmpty()) { return List.of(); }
int desde = Math.max(0, prestamos.size() - cuantos); // proteccion del limite inferior
// subList devuelve una VISTA: la copiamos para que sea independiente
return new ArrayList<>(prestamos.subList(desde, prestamos.size()));
}
public Prestamo masAntiguo() {
Prestamo mejor = null;
for (Prestamo p : prestamos) {
if (mejor == null || p.getDiaPrestamo() < mejor.getDiaPrestamo()) {
mejor = p;
}
}
return mejor; // null si la lista esta vacia; documentalo en el javadoc
}
public int tamano() { return prestamos.size(); }
}Puntos de diseño a retener. devolver modifica el objeto dentro de un for-each, lo cual es perfectamente legal: modCount solo cuenta cambios estructurales de la lista. purgarDevueltos usa removeIf en lugar de un bucle con remove, pasando de O(n²) a O(n) y evitando la excepción. ultimos protege el límite inferior con Math.max y copia la vista de subList. Y todos los métodos que devuelven colecciones devuelven una lista vacía en lugar de null.
En 05-09 verás que masAntiguo() se escribe en una línea con Collections.min(prestamos, Comparator.comparingInt(Prestamo::getDiaPrestamo)).
Solución 2
package com.nexussoftware.bibliotech.presentacion;
import java.util.ArrayList;
import java.util.List;
public class DemostracionRemove {
public static void main(String[] args) {
List<Integer> numeros = new ArrayList<>(List.of(10, 20, 30, 40, 50));
System.out.println("Inicial: " + numeros);
// 1) remove(int): el literal 1 es un int -> se elige remove(int index)
// sin autoboxing, porque la sobrecarga prefiere la coincidencia exacta.
numeros.remove(1);
System.out.println("Tras remove(1): " + numeros + " <- borro la POSICION 1");
// 2) remove(Object): Integer.valueOf(30) es un Integer -> remove(Object)
numeros.remove(Integer.valueOf(30));
System.out.println("Tras remove(valueOf(30)): " + numeros + " <- borro el VALOR 30");
// 3) El casting produce el mismo efecto que valueOf, pero se lee peor
numeros.remove((Integer) 40);
System.out.println("Tras remove((Integer) 40): " + numeros + " <- borro el VALOR 40");
// 4) La llamada peligrosa, comentada a proposito:
// numeros.remove(100);
// -> IndexOutOfBoundsException: Index 100 out of bounds for length 2
// El programador queria borrar el VALOR 100 y ha pedido la POSICION 100.
// El error se descubre en ejecucion, no al compilar.
// 5) Con String no hay ambiguedad: "Marta Ruiz" no es un int,
// asi que solo puede resolverse como remove(Object).
List<String> empleados = new ArrayList<>(List.of("Marta Ruiz", "Diego Alonso"));
empleados.remove("Marta Ruiz");
System.out.println("Empleados: " + empleados);
// 6) Metodo seguro y reutilizable
List<Integer> ids = new ArrayList<>(List.of(101, 205, 307));
System.out.println("eliminarValor(205): " + eliminarValor(ids, 205) + " -> " + ids);
System.out.println("eliminarValor(999): " + eliminarValor(ids, 999) + " -> " + ids);
}
/**
* Elimina por VALOR sin ambiguedad posible. El parametro es int por comodidad
* de quien llama, y el envoltorio explicito garantiza que se invoque
* remove(Object) y no remove(int).
*/
public static boolean eliminarValor(List<Integer> lista, int valor) {
return lista.remove(Integer.valueOf(valor));
}
}Inicial: [10, 20, 30, 40, 50] Tras remove(1): [10, 30, 40, 50] <- borro la POSICION 1 Tras remove(valueOf(30)): [10, 40, 50] <- borro el VALOR 30 Tras remove((Integer) 40): [10, 50] <- borro el VALOR 40 Empleados: [Diego Alonso] eliminarValor(205): true -> [101, 307] eliminarValor(999): false -> [101, 307]
La lección de fondo va más allá de remove: cuando dos sobrecargas se distinguen solo por primitivo frente a envoltorio, la que gana es la del primitivo, porque la resolución de sobrecarga prefiere no aplicar boxing. Es la misma regla de 03-03, ahora con consecuencias visibles.
Solución 3
package com.nexussoftware.bibliotech.presentacion;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class ConversorColecciones {
private ConversorColecciones() { }
public static void demostrarAsList() {
System.out.println("--- Arrays.asList es una VISTA de tamano fijo ---");
String[] array = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };
List<String> vista = Arrays.asList(array);
vista.set(0, "Marta R."); // permitido
System.out.println("array[0] tras vista.set: " + array[0]); // Marta R.
array[1] = "Diego A."; // por el otro lado
System.out.println("vista tras array[1]=: " + vista); // Diego A.
// vista.add("Nuevo"); -> UnsupportedOperationException: tamano FIJO
// vista.remove(0); -> UnsupportedOperationException
System.out.println("add y remove lanzarian UnsupportedOperationException");
}
public static void demostrarCopiaIndependiente() {
System.out.println("--- new ArrayList<>(Arrays.asList(a)) es INDEPENDIENTE ---");
String[] array = { "Marta Ruiz", "Diego Alonso" };
List<String> copia = new ArrayList<>(Arrays.asList(array));
copia.add("Nuria Vidal"); // ahora si se puede
copia.set(0, "Marta R.");
System.out.println("copia: " + copia); // [Marta R., Diego Alonso, Nuria Vidal]
System.out.println("array: " + Arrays.toString(array)); // [Marta Ruiz, Diego Alonso]
System.out.println("El array original NO se ha visto afectado");
}
public static void demostrarToArray() {
System.out.println("--- De lista a array ---");
List<String> lista = new ArrayList<>(List.of("Marta Ruiz", "Diego Alonso"));
// new String[0] indica el TIPO. toArray crea internamente uno del tamano justo.
String[] array = lista.toArray(new String[0]);
System.out.println("array: " + Arrays.toString(array) + " (length " + array.length + ")");
lista.add("Nuria Vidal"); // el array ya generado no cambia
System.out.println("tras anadir a la lista, array sigue con " + array.length);
Object[] sinTipo = lista.toArray(); // pierde el tipo: casi nunca util
System.out.println("toArray() sin argumento devuelve Object[]: " + sinTipo.length);
}
public static void demostrarPrimitivos() {
System.out.println("--- La trampa de los primitivos ---");
int[] primitivos = { 1, 2, 3 };
List<int[]> mal = Arrays.asList(primitivos);
System.out.println("Arrays.asList(int[]).size() = " + mal.size()
+ " <- UNA lista de UN elemento: el array entero");
Integer[] envueltos = { 1, 2, 3 };
List<Integer> bien = Arrays.asList(envueltos);
System.out.println("Arrays.asList(Integer[]).size() = " + bien.size()
+ " <- tres elementos, como esperabas");
System.out.println("Causa: los genericos solo admiten tipos referencia (10-01)");
}
/** Conversion correcta de int[] a List<Integer>. */
public static List<Integer> aLista(int[] primitivos) {
if (primitivos == null) { return List.of(); }
// capacidad inicial exacta: sabemos cuantos van a entrar
List<Integer> lista = new ArrayList<>(primitivos.length);
for (int n : primitivos) {
lista.add(n); // autoboxing: int -> Integer en cada add
}
return lista;
}
public static void main(String[] args) {
demostrarAsList();
demostrarCopiaIndependiente();
demostrarToArray();
demostrarPrimitivos();
System.out.println("aLista(new int[]{7,8,9}) = " + aLista(new int[] { 7, 8, 9 }));
}
}La conclusión operativa del ejercicio cabe en una regla: Arrays.asList solo para envolver un array de solo lectura que hay que pasar a una API que pide List; new ArrayList<>(Arrays.asList(...)) para todo lo demás; toArray(new T[0]) para la vuelta. Y con primitivos, siempre un bucle o —a partir del módulo 10— Arrays.stream(...).boxed().
Conclusión
Ya conoces por dentro la colección que más vas a usar. Sabes que un ArrayList es un array interno más un int size: exactamente el CatalogoArray que escribiste tú, pero afinado por el JDK. De esa estructura se deriva todo su perfil de rendimiento: get y set en O(1), add al final en O(1) amortizado, y O(n) para insertar o eliminar en cualquier otro sitio porque hay que desplazar el resto con un System.arraycopy interno.
Distingues capacidad de tamaño, y sabes que la capacidad es un detalle interno que no puedes consultar y que casi nunca debes gestionar. Entiendes el redimensionado: el array crece a ×1,5 con una copia O(n), y precisamente porque crece de forma multiplicativa el coste medio por elemento se mantiene constante — eso es el coste amortizado, una idea que reaparecerá con HashMap y ArrayDeque. Y sabes cuándo el constructor con capacidad inicial merece la pena: con miles o millones de elementos previstos, no con veinte.
Dominas la API completa: add en sus dos formas, get, set frente a add(i, e), remove en sus tres variantes, indexOf, contains y su dependencia total de equals —el hilo que viene de 03-09—, sort, replaceAll, removeIf y forEach con las interfaces funcionales de 04-06. Conoces la trampa de remove(int) frente a remove(Object) en una List<Integer> y las tres formas de desactivarla, y sabes que subList es una vista: modificarla cambia la original, modificar la original la invalida, y para una copia hay que envolverla en un new ArrayList<>(...).
Sabes recorrer y borrar con seguridad —removeIf por defecto, Iterator.remove() para lógica compleja, for hacia atrás si necesitas el índice—, tienes la tabla que compara ArrayList con array y los cinco casos en que el array sigue ganando, y manejas las conversiones en ambos sentidos con las trampas de Arrays.asList y de los primitivos. Conoces las listas de listas y su debilidad —el índice que significa algo por convenio no escrito— y sabes que dos listas son iguales si tienen los mismos elementos en el mismo orden, sea cual sea su implementación, con la advertencia de que su hashCode cambia cuando cambia su contenido.
El Catalogo de BiblioTech es ya una clase profesional: alta con control de duplicados, inserción posicional, reemplazo, baja, búsqueda por cualquier Predicate, ordenación por cualquier Comparator, paginación con subList y vista inmutable hacia el exterior. Cero líneas de gestión de memoria.
En la lección siguiente, LinkedList, verás la otra implementación de List: una cadena de nodos enlazados en ambos sentidos, sin array y sin capacidad. Entenderás qué implica eso para la memoria y para la caché del procesador, y sobre todo desmontarás el mito que casi todo el mundo repite mal: que "LinkedList inserta en O(1)". Es cierto solo si ya tienes la posición, y llegar a ella cuesta O(n). Verás la comparación honesta operación por operación, por qué en la práctica ArrayList gana casi siempre, por qué LinkedList sobrevive sobre todo como Deque, cómo insertar correctamente mientras recorres con un ListIterator, y el único caso de BiblioTech en el que LinkedList es realmente la elección acertada: la cola de reservas que se consume por el principio y crece por el final.
Curso de Programación en Java
Módulo 1: Introducción a Java
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
