Cerraste el módulo 4 con una promesa y una confesión. La promesa: los arrays provisionales de BiblioTech se convertirán en colecciones. La confesión: cada vez que el proyecto necesita guardar muchas cosas, recurre a un Material[] catalogo = new Material[10] que hay que copiar entero para añadir un elemento y recorrer completo para encontrar uno. Antes de sustituirlos conviene entenderlos de verdad, porque el array no desaparece: ArrayList es un array por dentro, HashMap es un array de cubetas, ArrayDeque es un array circular, y String es un array de bytes. Todo el Framework de Colecciones que verás en las ocho lecciones siguientes está construido sobre arrays.

Un arreglo (o array, el término que usaremos casi siempre porque es el que verás en todo el código real) es la estructura de datos más primitiva y más rápida de Java: un bloque contiguo de memoria, de tamaño fijo, que guarda elementos del mismo tipo y permite acceder a cualquiera de ellos por su posición en tiempo constante. Esas cuatro palabras —contiguo, fijo, homogéneo, constante— explican a la vez toda su potencia y todas sus limitaciones. En esta lección las verás en detalle, aprenderás por fin el bucle for-each que quedó pendiente en el módulo 2, dominarás la clase de utilidades Arrays y terminarás con una lista clara de las cosas que un array no puede hacer: exactamente la lista de motivos por los que existe el Framework de Colecciones.

Contenido

  1. Qué es realmente un array
  2. Declarar, crear e inicializar
  3. Valores por defecto
  4. Índices, length y los límites del array
  5. Recorrido con for clásico
  6. El bucle for-each
  7. Arrays de primitivos frente a arrays de objetos
  8. Arrays multidimensionales y dentados
  9. La clase Arrays
  10. Copiar arrays
  11. Varargs por dentro es un array
  12. Aplicación: el catálogo de BiblioTech
  13. Las limitaciones que motivan las colecciones
  14. Errores Comunes y Consejos
  15. Ejercicios

  1. Qué es realmente un array

Un array es un bloque contiguo de memoria reservado de una vez, dividido en celdas del mismo tamaño. Cuando escribes:

int[] diasRetraso = new int[5];

la JVM reserva en el heap espacio suficiente para cinco enteros (cinco veces cuatro bytes) más una pequeña cabecera con el tipo y la longitud. Todas las celdas están pegadas una detrás de otra:

flowchart LR
    subgraph heap["Objeto array en el heap"]
        direction LR
        H["cabecera<br/>tipo: int[]<br/>length: 5"]
        C0["[0]<br/>0"]
        C1["[1]<br/>0"]
        C2["[2]<br/>0"]
        C3["[3]<br/>0"]
        C4["[4]<br/>0"]
    end
    ref["diasRetraso<br/>(referencia en la pila)"] --> H

Esa contigüidad es la clave de todo. Para leer diasRetraso[3], la JVM no busca nada: calcula direccion_base + 3 * 4 bytes y lee. Una multiplicación y una suma, siempre el mismo trabajo, dé igual que el array tenga cinco elementos o cinco millones. A eso lo llamamos acceso en tiempo constante, o O(1).

Notación O(): cómo leerla. A lo largo de este módulo compararás estructuras de datos por su coste. La notación O() describe cómo crece el tiempo de una operación cuando crece el número de elementos n, ignorando constantes y detalles de máquina. O(1) significa tiempo constante: da igual que haya 10 o 10 millones de elementos, cuesta lo mismo. O(n) significa lineal: con el doble de elementos, el doble de tiempo (recorrer un array entero). O(log n) significa que doblar los elementos añade solo un paso más (búsqueda binaria: con un millón de elementos, unos 20 pasos). O(n²) significa que doblar los elementos cuadruplica el tiempo (dos bucles anidados; es lo que hace hoy el informePorTipo de BiblioTech). No es una medida de segundos, es una medida de cómo escala.

Y hay una segunda ventaja menos obvia: la localidad de caché. Los procesadores modernos no leen la memoria byte a byte, sino en bloques (líneas de caché de 64 bytes típicamente). Al leer diasRetraso[0], el procesador trae también los siguientes elementos "gratis". Por eso recorrer un array es dramáticamente más rápido que recorrer una estructura cuyos elementos estén dispersos por la memoria: lo verás con números en 05-04, cuando compares ArrayList con LinkedList.

Las tres propiedades que definen un array en Java:

Propiedad Qué significa Consecuencia
Tamaño fijo Se decide al crearlo y no se puede cambiar Para "añadir" hay que crear otro array y copiar
Tipo homogéneo Todos los elementos son del mismo tipo declarado El compilador garantiza qué hay dentro
Es un objeto Vive en el heap; la variable guarda una referencia Se pasa por referencia, admite null, hereda de Object

Ese tercer punto sorprende a mucha gente: un array es un objeto, aunque no exista una clase Array que puedas ver. int[], String[] y Material[] son tipos reales que heredan de Object, por lo que un array tiene hashCode(), toString() y getClass(). Eso tendrá consecuencias importantes en el apartado 9.

  1. Declarar, crear e inicializar

Son tres operaciones distintas y conviene no confundirlas.

Declarar solo crea la variable que guardará la referencia. No hay array todavía:

Material[] catalogo;        // forma recomendada: los corchetes junto al TIPO
Material catalogo2[];       // legal, herencia de C, DESACONSEJADA

Usa siempre la primera forma. La segunda compila, pero oculta información: en Material catalogo[], otro;, catalogo es un array y otro no lo es, lo que es una fuente clásica de confusión.

Crear reserva la memoria con new, y aquí es donde se fija el tamaño para siempre:

Material[] catalogo = new Material[10];      // 10 celdas, todas a null
int[] diasRetraso   = new int[5];            // 5 celdas, todas a 0

El tamaño puede ser una variable calculada en ejecución, lo que sí es flexible:

int cuantos = Integer.parseInt(entrada.nextLine());
Material[] seleccion = new Material[cuantos];

Lo que no puede es cambiar después. seleccion medirá cuantos para siempre.

Inicializar con literal crea y rellena en una sola expresión, y el tamaño se deduce de los elementos:

String[] empleados = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };   // length = 3
double[] tarifas   = { 0.25, 0.10, 0.50 };

Esta forma abreviada solo vale en la declaración. Si quieres asignar un array literal a una variable ya declarada, o pasarlo directamente como argumento, necesitas la forma completa con new:

String[] empleados;
// empleados = { "Marta Ruiz" };                    // ERROR de compilacion
empleados = new String[] { "Marta Ruiz" };          // correcto

registrar(new String[] { "Marta Ruiz", "Diego Alonso" });   // correcto como argumento

  1. Valores por defecto

Al crear un array con new, Java rellena todas las celdas con el valor por defecto del tipo. Esto no es un detalle menor: significa que un array recién creado nunca contiene basura, a diferencia de lo que ocurre en C.

Tipo del elemento Valor por defecto
byte, short, int, long 0
float, double 0.0
char '\u0000' (carácter nulo, se imprime como espacio en blanco o cuadrado)
boolean false
Cualquier tipo referencia (String, Material, Integer...) null

La consecuencia práctica más importante es la última fila. Un new Material[10] no contiene diez materiales: contiene diez referencias null. Sigue siendo tu trabajo llenarlas:

Material[] catalogo = new Material[3];
System.out.println(catalogo[0]);              // null
// catalogo[0].getTitulo();                   // NullPointerException

catalogo[0] = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
System.out.println(catalogo[0].getTitulo());  // Java Efectivo

Y también explica la trampa clásica de contar elementos: si creas new Material[10] y solo rellenas tres, catalogo.length vale 10, no 3. El array no sabe cuántas celdas has usado; por eso todo el BiblioTech del módulo 4 arrastra variables auxiliares como int n = 0 para llevar la cuenta a mano. Las colecciones eliminan exactamente ese problema.

  1. Índices, length y los límites del array

Los índices de un array van de 0 a length - 1. Que empiecen en 0 no es un capricho: el índice es literalmente el desplazamiento desde el inicio del bloque. El primer elemento está a distancia cero de la base.

String[] empleados = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };

System.out.println(empleados.length);        // 3
System.out.println(empleados[0]);            // Marta Ruiz     (el primero)
System.out.println(empleados[2]);            // Nuria Vidal    (el ultimo)
System.out.println(empleados[empleados.length - 1]);   // Nuria Vidal, idioma estandar

length es un atributo, no un método. Se escribe array.length, sin paréntesis. Es una de las inconsistencias históricas de Java que más confusión genera, porque para String es un método (texto.length()) y para las colecciones es otro método distinto (lista.size()). Memoriza los tres:

Tipo Cómo se obtiene el tamaño
Array array.lengthatributo, sin paréntesis
String texto.length() — método
Colección (List, Set, Map...) coleccion.size() — método

Si accedes fuera de rango, la JVM lo detecta y lanza ArrayIndexOutOfBoundsException:

System.out.println(empleados[3]);      // ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
System.out.println(empleados[-1]);     // ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3

Esto es una virtud, no un defecto: Java comprueba cada acceso y detiene el programa en el punto exacto del error, en lugar de leer memoria ajena silenciosamente como haría C. El coste de esa comprobación es mínimo y el compilador JIT la elimina cuando puede demostrar que el índice es seguro. Cómo capturar y tratar esa excepción es el tema del módulo 6; por ahora, evítala comprobando los límites antes de acceder.

  1. Recorrido con for clásico

El bucle for que conoces desde 02-02 es la forma completa de recorrer un array, y sigue siendo la correcta cuando necesitas el índice:

double[] multas = { 3.75, 0.0, 12.50, 1.25 };

for (int i = 0; i < multas.length; i++) {
    System.out.printf("Prestamo %d -> multa %.2f EUR%n", i + 1, multas[i]);
}

Tres detalles que evitan la mayoría de los errores de recorrido:

  • i < multas.length, nunca i <= multas.length. El último índice válido es length - 1.
  • Usa multas.length, no una constante literal como 4. Si mañana el array crece, el bucle se adapta solo.
  • Empieza en 0, salvo que tengas un motivo explícito.

El for clásico es imprescindible cuando el recorrido necesita algo más que los valores:

// Recorrido hacia atras
for (int i = multas.length - 1; i >= 0; i--) { ... }

// Recorrido de dos en dos
for (int i = 0; i < multas.length; i += 2) { ... }

// Comparar cada elemento con el siguiente (ojo al limite)
for (int i = 0; i < multas.length - 1; i++) {
    if (multas[i] > multas[i + 1]) { ... }
}

// MODIFICAR el contenido del array
for (int i = 0; i < multas.length; i++) {
    multas[i] = Math.min(multas[i], 20.0);      // aplicar el tope de multa
}

Esa última es la más importante de recordar para el apartado siguiente.

  1. El bucle for-each

En el módulo 2 se pospuso el for-each porque no tenía sentido explicarlo sin algo que recorrer. Ya lo tienes.

El bucle for mejorado o for-each (Java 5) recorre todos los elementos de un array o de cualquier objeto Iterable —lo que incluye todas las colecciones que verás en este módulo— sin gestionar índices:

for (Tipo elemento : coleccionOArray) {
    // usar 'elemento'
}

Se lee literalmente "para cada elemento de tipo Tipo en coleccionOArray". Los dos puntos se pronuncian "en".

Compara las dos versiones del mismo recorrido:

String[] empleados = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };

// for clasico: 3 elementos que pueden fallar (inicio, condicion, incremento)
for (int i = 0; i < empleados.length; i++) {
    System.out.println("Empleado: " + empleados[i]);
}

// for-each: sin indice, sin condicion, sin incremento
for (String nombre : empleados) {
    System.out.println("Empleado: " + nombre);
}

Y con objetos del proyecto, donde brilla de verdad:

Material[] catalogo = {
    new Libro("Java Efectivo",      "Joshua Bloch",  "978-0000000001", 2018),
    new Libro("Patrones de Diseno", "Erich Gamma",   "978-0000000002", 1994),
    new Revista("Java Magazine",    "REV-2024-03",   42, "Mensual"),
    new Dvd("Refactorizacion en vivo", "DVD-0007",   95)
};

for (Material m : catalogo) {
    System.out.println(m.describir());     // polimorfismo de 03-06, sin un solo indice
}

Ventajas

  • Menos código y menos errores. Desaparecen las tres fuentes clásicas de fallo: empezar en 1, terminar en <=, olvidar el i++ (bucle infinito).
  • Imposible salirse de rango. No hay índice que equivocarse: ArrayIndexOutOfBoundsException no puede ocurrir.
  • Expresa la intención. "Recorro todos los elementos, uno por uno, hacia delante." Quien lee tu código lo sabe al instante.
  • Funciona igual con arrays y con colecciones. El mismo bucle sirve para Material[] y para List<Material>, lo que hará indoloro el refactor del apartado 12.

Limitaciones

Y ahora las tres cosas que no puede hacer, que son la razón de que el for clásico siga existiendo:

1. No te da el índice. Si necesitas numerar, comparar con el vecino o escribir en la posición i, necesitas el for clásico (o llevar un contador aparte, que es exactamente lo que el for-each intentaba evitar).

int posicion = 1;
for (Material m : catalogo) {
    System.out.println(posicion + ". " + m.getTitulo());
    posicion++;                                   // funciona, pero delata que querias un for clasico
}

2. No permite modificar la celda del array. Esta es la limitación que más se malinterpreta, así que vale la pena verla con precisión:

int[] numeros = { 1, 2, 3 };

for (int n : numeros) {
    n = n * 10;                    // NO hace nada al array
}
System.out.println(Arrays.toString(numeros));    // [1, 2, 3]

La variable n es una copia del valor de la celda (paso por valor, 03-03). Reasignarla cambia la copia, no el array. Lo mismo ocurre con objetos:

for (Material m : catalogo) {
    m = new Libro("Otro", "Nadie", "REF-X", 2020);    // NO cambia catalogo[i]
}

m es una copia de la referencia. Reasignarla apunta la copia a otro objeto; la celda del array sigue apuntando al original.

Pero sí puedes modificar el objeto al que apunta, porque ahí no reasignas nada, invocas métodos sobre el mismo objeto:

for (Material m : catalogo) {
    m.prestar();                 // SI afecta al objeto real del catalogo
}

La regla, en una línea: el for-each no puede cambiar a qué apunta cada celda, pero sí puede cambiar el estado del objeto apuntado.

3. Solo va hacia delante y de uno en uno. No hay recorrido inverso, ni saltos, ni recorridos parciales.

Cuándo usar cada uno

Situación Bucle recomendado
Leer todos los elementos, en orden for-each
Necesitas el índice (numerar, posición, comparar vecinos) for clásico
Escribir en las celdas (array[i] = ...) for clásico
Recorrer hacia atrás o con saltos for clásico
Recorrer solo una parte for clásico
Recorrer una colección y eliminar mientras recorres Ni uno ni otro: Iterator o removeIf (05-02)

Usa for-each por defecto y baja al for clásico solo cuando necesites lo que el for-each no da. En 05-02 verás que el for-each no es una construcción mágica: el compilador lo traduce a un for clásico cuando recorre un array, y a un Iterator cuando recorre una colección.

  1. Arrays de primitivos frente a arrays de objetos

Aquí está la diferencia estructural que explica la mitad de las sorpresas con arrays: qué guarda realmente cada celda.

En un array de primitivos, la celda contiene el valor:

int[] anios = { 2018, 1994, 1999 };

En un array de objetos, la celda contiene una referencia al objeto, que vive en otro sitio del heap:

Material[] catalogo = new Material[3];
catalogo[0] = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
catalogo[1] = new Revista("Java Magazine", "REV-2024-03", 42, "Mensual");
// catalogo[2] sigue a null
flowchart LR
    subgraph prim["int[] anios — los valores estan DENTRO"]
        direction LR
        P0["[0] 2018"]
        P1["[1] 1994"]
        P2["[2] 1999"]
    end

    subgraph obj["Material[] catalogo — referencias"]
        direction LR
        O0["[0] ref"]
        O1["[1] ref"]
        O2["[2] null"]
    end

    L["Libro<br/>Java Efectivo<br/>978-0000000001"]
    R["Revista<br/>Java Magazine<br/>REV-2024-03"]

    O0 --> L
    O1 --> R

De ese diagrama se derivan cuatro consecuencias que conviene tener muy claras:

  1. Un array de objetos es más pequeño de lo que parece pero cuesta más recorrer. Las celdas solo guardan referencias (4 u 8 bytes), pero seguir cada referencia implica un salto a otra zona del heap, lo que rompe la localidad de caché del apartado 1.
  2. Dos celdas pueden apuntar al mismo objeto (aliasing, 03-02). catalogo[2] = catalogo[0]; no copia el libro: crea un segundo camino hacia él. Modificarlo por una vía se ve por la otra.
  3. null es un valor legítimo en las celdas, y cada acceso a catalogo[i].algo() puede lanzar NullPointerException si esa celda nunca se rellenó.
  4. El array es covariante, y eso tiene una trampa. Material[] acepta cualquier subclase de Material (Libro, Revista, Dvd), lo que es cómodo y es lo que permite el catálogo polimórfico. Pero también permite esto:
Object[] cosas = new Libro[2];              // COMPILA: Libro[] es un Object[]
cosas[0] = new Revista("Java Magazine", "REV-1", 1, "Mensual");   // ArrayStoreException en EJECUCION

El compilador lo acepta porque Libro[] es subtipo de Object[], pero la JVM comprueba el tipo real en cada escritura y lanza ArrayStoreException. Es un agujero de seguridad de tipos que Java 1.0 aceptó por pragmatismo y que los genéricos corrigieron: List<Libro> no es un List<Object>, precisamente para que ese error se detecte al compilar. La teoría completa (invarianza, comodines, borrado de tipos) es la lección 10-01.

  1. Arrays multidimensionales y dentados

En Java no existen los arrays bidimensionales de verdad: lo que existe son arrays cuyos elementos son, a su vez, arrays. Entender eso resuelve todas las dudas de golpe.

int[][] prestamosPorMes = new int[3][12];   // 3 tipos de material x 12 meses

Esto crea un array de 3 elementos, cada uno de los cuales es una referencia a un array de 12 enteros:

flowchart LR
    M["prestamosPorMes<br/>int[][] length=3"]
    F0["[0] → int[12]"]
    F1["[1] → int[12]"]
    F2["[2] → int[12]"]
    M --> F0
    M --> F1
    M --> F2
    F0 --> A0["0 0 0 0 0 0 0 0 0 0 0 0"]
    F1 --> A1["0 0 0 0 0 0 0 0 0 0 0 0"]
    F2 --> A2["0 0 0 0 0 0 0 0 0 0 0 0"]

El acceso lleva dos índices, en orden fila-columna:

prestamosPorMes[0][3] = 12;                  // tipo 0 (libros), mes 3 (abril)
System.out.println(prestamosPorMes.length);         // 3   -> numero de filas
System.out.println(prestamosPorMes[0].length);      // 12  -> longitud de la fila 0

El recorrido natural son dos bucles anidados, uno por dimensión:

String[] tipos = { "Libros", "Revistas", "DVDs" };

for (int t = 0; t < prestamosPorMes.length; t++) {
    int total = 0;
    for (int mes = 0; mes < prestamosPorMes[t].length; mes++) {
        total += prestamosPorMes[t][mes];
    }
    System.out.printf("%-10s %3d prestamos al ano%n", tipos[t], total);
}

O, si no necesitas los índices, con for-each anidado:

for (int[] fila : prestamosPorMes) {
    int total = 0;
    for (int valor : fila) { total += valor; }
    System.out.println("Total de la fila: " + total);
}

Fíjate en el tipo de la variable del bucle externo: int[] fila, no int. Cada elemento de un int[][] es un int[].

También hay inicialización literal, y aquí se ve claramente que son arrays de arrays:

int[][] matriz = {
    { 12,  8, 15 },
    {  3,  7,  2 },
    { 20, 11,  9 }
};

Arrays dentados (jagged)

Como cada fila es un array independiente, las filas pueden tener longitudes distintas. A eso se le llama array dentado o jagged:

String[][] prestamosPorEmpleado = new String[3][];      // 3 filas, sin columnas todavia

prestamosPorEmpleado[0] = new String[] { "Java Efectivo", "Refactorizacion" };  // Marta: 2
prestamosPorEmpleado[1] = new String[] { "Patrones de Diseno" };                // Diego: 1
prestamosPorEmpleado[2] = new String[0];                                        // Nuria: 0

String[] nombres = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };

for (int i = 0; i < prestamosPorEmpleado.length; i++) {
    System.out.print(nombres[i] + ": ");
    if (prestamosPorEmpleado[i].length == 0) {
        System.out.println("(sin prestamos)");
    } else {
        System.out.println(String.join(", ", prestamosPorEmpleado[i]));
    }
}
Marta Ruiz: Java Efectivo, Refactorizacion
Diego Alonso: Patrones de Diseno
Nuria Vidal: (sin prestamos)

Observa la sintaxis new String[3][]: se indica el número de filas y se deja la segunda dimensión vacía, porque cada fila se creará después con su tamaño propio. Si dejas las filas sin crear, valen null, y acceder a prestamosPorEmpleado[0][0] daría NullPointerException.

Esta estructura —"para cada empleado, una lista de títulos de longitud variable"— es justo la que en 05-05 sustituirás por un Map<Empleado, List<Prestamo>>, mucho más expresivo y sin índices numéricos que mantener sincronizados con otro array de nombres.

  1. La clase Arrays

java.util.Arrays es una clase de utilidades con métodos estáticos que resuelven casi todo lo que necesitas hacer con arrays. Es de las primeras cosas que hay que importar en cualquier programa serio:

import java.util.Arrays;

toString y deepToString

Un array es un objeto que no sobrescribe toString(), así que imprimirlo directamente muestra la representación de Object de 03-09 (nombre del tipo, arroba, hash en hexadecimal):

int[] anios = { 2018, 1994, 1999 };
System.out.println(anios);                       // [I@1b6d3586    <- inutil
System.out.println(Arrays.toString(anios));      // [2018, 1994, 1999]

Para arrays de más de una dimensión, toString no basta —imprimiría la referencia de cada fila—, y hace falta deepToString, que baja recursivamente:

int[][] matriz = { { 1, 2 }, { 3, 4 } };
System.out.println(Arrays.toString(matriz));       // [[I@4554617c, [I@74a14482]
System.out.println(Arrays.deepToString(matriz));   // [[1, 2], [3, 4]]

Arrays.toString es tu herramienta número uno para depurar arrays. Úsala en cada traza.

sort y sort con Comparator

Arrays.sort ordena el array en el sitio (in place): modifica el array recibido y no devuelve nada.

double[] multas = { 12.50, 0.0, 3.75, 1.25 };
Arrays.sort(multas);
System.out.println(Arrays.toString(multas));     // [0.0, 1.25, 3.75, 12.5]

Con objetos, hay dos variantes. Sin Comparator, exige que los elementos implementen Comparable —lo que hace Ficha, según viste en 04-07— y usa su orden natural:

Ficha[] fichas = {
    new Ficha("Refactorizacion",    "Martin Fowler", 1999),
    new Ficha("Java Efectivo",      "Joshua Bloch",  2018),
    new Ficha("Patrones de Diseno", "Erich Gamma",   1994)
};
Arrays.sort(fichas);                             // usa el compareTo de Ficha (por titulo)

Y con un Comparator, que es donde se conecta todo el módulo 4:

Arrays.sort(fichas, Comparator.comparingInt(Ficha::anio));                     // por ano
Arrays.sort(fichas, Comparator.comparing(Ficha::autor).thenComparing(Ficha::titulo));
Arrays.sort(fichas, Comparator.comparingInt(Ficha::anio).reversed());          // mas nuevos primero

Hay además una variante que ordena solo un rango, útil cuando el array tiene celdas sin usar al final:

Arrays.sort(catalogo, 0, n, Comparator.comparing(Material::getTitulo));   // solo las n primeras

La teoría completa —el contrato de compareTo, la estabilidad, qué algoritmo usa Java— es la lección de cierre de este módulo, 05-09.

binarySearch

Busca un elemento con búsqueda binaria: mira el elemento central, descarta media mitad y repite. Coste O(log n): en un array de un millón de elementos, unas veinte comprobaciones en lugar de un millón.

int[] referencias = { 101, 205, 307, 412, 588 };     // YA ORDENADO
System.out.println(Arrays.binarySearch(referencias, 307));    // 2  -> indice donde esta
System.out.println(Arrays.binarySearch(referencias, 400));    // -4 -> no esta

Dos advertencias esenciales:

  • El array debe estar ordenado previamente, con el mismo criterio con el que buscas. Sobre un array desordenado el resultado es basura, y no hay ningún aviso.
  • El valor negativo no es "-1 y ya está": es -(punto_de_insercion) - 1. En el ejemplo, -4 significa que 400 iría en el índice 3. Si quieres el punto de inserción: int pos = -resultado - 1;.

fill

Rellena todo el array (o un rango) con el mismo valor:

double[] multas = new double[5];
Arrays.fill(multas, -1.0);                       // marca "sin calcular" en todas las celdas
Arrays.fill(multas, 0, 2, 0.0);                  // solo indices 0 y 1 (el 2 queda fuera)

copyOf y copyOfRange

copyOf crea un array nuevo del tamaño indicado, copiando lo que quepa y rellenando el resto con el valor por defecto:

Material[] catalogo = new Material[3];
// ... se llenan las 3 celdas ...

Material[] ampliado = Arrays.copyOf(catalogo, 6);     // 3 originales + 3 nulls
Material[] recortado = Arrays.copyOf(catalogo, 2);    // solo las 2 primeras

Este es exactamente el truco que BiblioTech lleva usando desde el módulo 3 para "hacer crecer" un array, y también, literalmente, lo que hace ArrayList por dentro (05-03).

copyOfRange copia un tramo, con el límite inferior incluido y el superior excluido:

int[] numeros = { 10, 20, 30, 40, 50 };
int[] centro  = Arrays.copyOfRange(numeros, 1, 4);    // [20, 30, 40]

equals frente a deepEquals

Aquí hay una trampa que cuesta horas a mucha gente. Como un array no sobrescribe equals, comparar dos arrays con == o con .equals() compara referencias, no contenidos:

int[] a = { 1, 2, 3 };
int[] b = { 1, 2, 3 };

System.out.println(a == b);              // false
System.out.println(a.equals(b));         // false  <- el equals heredado de Object
System.out.println(Arrays.equals(a, b)); // true   <- lo que casi siempre querias

Y para arrays de más de una dimensión, Arrays.equals compara las filas por referencia y también falla; hace falta deepEquals:

int[][] m1 = { { 1, 2 }, { 3, 4 } };
int[][] m2 = { { 1, 2 }, { 3, 4 } };

System.out.println(Arrays.equals(m1, m2));      // false
System.out.println(Arrays.deepEquals(m1, m2));  // true

Lo mismo ocurre con el hash: usa Arrays.hashCode y Arrays.deepHashCode, nunca array.hashCode(), si vas a meter arrays en estructuras basadas en hash (que además es una idea que 05-05 te desaconsejará con argumentos).

asList y su trampa

Arrays.asList crea una List a partir de un array, y es tentador porque parece la conversión obvia. Pero devuelve una vista de tamaño fijo respaldada por el array original, no una lista normal:

String[] nombres = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };
List<String> lista = Arrays.asList(nombres);

System.out.println(lista.get(0));        // Marta Ruiz
lista.set(0, "Marta R.");                // permitido: cambia TAMBIEN nombres[0]
System.out.println(nombres[0]);          // Marta R.

lista.add("Nuevo");                      // UnsupportedOperationException
lista.remove(0);                         // UnsupportedOperationException
Operación sobre Arrays.asList(array) Resultado
get, size, contains, indexOf, recorrer Funciona con normalidad
set(i, valor) Funciona y modifica el array original
add, remove, clear UnsupportedOperationException

Si quieres una lista de verdad, independiente y modificable, envuelve la vista:

List<String> modificable = new ArrayList<>(Arrays.asList(nombres));
modificable.add("Nuevo");                // ahora si

Y una trampa adicional que aparece con primitivos: Arrays.asList es genérico y los genéricos no aceptan tipos primitivos, así que un int[] se interpreta como un único elemento:

int[] numeros = { 1, 2, 3 };
List<int[]> raro = Arrays.asList(numeros);       // lista de UN elemento (el array entero)
System.out.println(raro.size());                 // 1, no 3

Con Integer[] funcionaría como esperas. La causa profunda —los genéricos solo operan sobre tipos referencia— se explica en 10-01.

  1. Copiar arrays

Hay cuatro formas de copiar, y elegir mal es una fuente habitual de errores sutiles.

Material[] original = { libro1, libro2, libro3 };

Material[] a = original;                                       // NO es una copia: es un alias
Material[] b = original.clone();                               // copia superficial
Material[] c = Arrays.copyOf(original, original.length);       // copia superficial
Material[] d = new Material[3];
System.arraycopy(original, 0, d, 0, 3);                        // copia superficial, con control
Forma Qué hace Cuándo usarla
b = a No copia nada. Dos variables, un solo array Nunca, si tu intención era copiar
a.clone() Copia superficial del mismo tamaño Copia rápida idéntica
Arrays.copyOf(a, n) Copia superficial con tamaño nuevo Crecer, recortar, copia defensiva
System.arraycopy(src, iSrc, dst, iDst, n) Copia n elementos de un array a otro existente Insertar, desplazar, control fino

La primera fila es el error de bulto: Material[] a = original; crea un segundo nombre para el mismo array. Escribir a[0] = otro; cambia también original[0]. Es el aliasing de 03-02 aplicado a arrays, y por eso el constructor de Catalogo hace Arrays.copyOf(materiales, materiales.length): copia defensiva (03-07) para que quien construyó el array no pueda alterar el catálogo por la puerta de atrás.

Todas las copias son superficiales (shallow). Copian las referencias, no los objetos:

Material[] copia = original.clone();
copia[0].prestar();                      // afecta AL MISMO Libro que ve 'original'
System.out.println(original[0].estaDisponible());   // false

Para una copia profunda hay que clonar elemento a elemento, y por eso la inmutabilidad de 03-07 es tan cómoda: si los objetos no cambian, la copia superficial es suficiente y no hay riesgo.

System.arraycopy es el más incómodo de los cuatro pero el más potente, porque copia sobre un array que ya existe, en la posición que quieras, y admite solapamiento con el mismo array de origen y destino. Es lo que usa ArrayList.remove(int) para tapar el hueco:

// Eliminar el elemento en la posicion 1 desplazando los siguientes hacia la izquierda
Material[] c = { m0, m1, m2, m3 };
System.arraycopy(c, 2, c, 1, c.length - 2);   // copia [2..3] sobre [1..2]
c[c.length - 1] = null;                       // libera la ultima celda

Es una operación O(n): hay que mover todos los elementos posteriores. Recuerda este detalle, porque es exactamente el coste que pagará ArrayList.remove(0) en 05-03 y el que LinkedList evita en 05-04.

  1. Varargs por dentro es un array

En 03-03 usaste varargs para escribir métodos con un número variable de argumentos:

public static double sumarMultas(double... multas) {
    double total = 0;
    for (double m : multas) { total += m; }      // se recorre como un array... porque LO ES
    return total;
}

El secreto es que double... multas es exactamente double[] multas con azúcar sintáctico en la llamada: el compilador empaqueta los argumentos sueltos en un array antes de invocar el método.

sumarMultas(3.75, 1.25, 12.50);            // el compilador crea new double[]{3.75, 1.25, 12.50}
sumarMultas();                             // crea new double[0]: array VACIO, no null
sumarMultas(new double[] { 3.75, 1.25 });  // tambien vale: le pasas el array directamente

De ahí se derivan tres reglas prácticas:

  • Un parámetro varargs nunca es null si se llama con la sintaxis normal: sin argumentos llega un array de longitud 0. Puedes recorrerlo sin comprobar nada.
  • Solo puede haber uno y debe ir el último: metodo(String etiqueta, double... valores) es válido; al revés, no.
  • Puedes pasarle un array ya construido, lo que resulta muy útil para reenviar argumentos entre métodos.

  1. Aplicación: el catálogo de BiblioTech

Reunamos todo en el catálogo del proyecto, todavía en versión array, y ordenémoslo con los Comparator del módulo 4.

package com.nexussoftware.bibliotech.servicio;

import java.util.Arrays;
import java.util.Comparator;
import com.nexussoftware.bibliotech.dominio.Material;

/** Catalogo de BiblioTech en su ultima version basada en arrays. */
public class CatalogoArray {

    private Material[] materiales;    // array interno
    private int        n;             // cuantas celdas estan realmente ocupadas

    public CatalogoArray(int capacidadInicial) {
        this.materiales = new Material[Math.max(capacidadInicial, 1)];
        this.n = 0;
    }

    /** Anade un material, ampliando el array si esta lleno. */
    public void anadir(Material m) {
        if (m == null) { return; }
        if (n == materiales.length) {
            // el array esta lleno: creamos uno del doble y copiamos
            materiales = Arrays.copyOf(materiales, materiales.length * 2);
        }
        materiales[n] = m;
        n++;
    }

    /** Elimina por referencia, tapando el hueco con System.arraycopy. */
    public boolean eliminar(String referencia) {
        for (int i = 0; i < n; i++) {
            if (materiales[i].getReferencia().equals(referencia)) {
                System.arraycopy(materiales, i + 1, materiales, i, n - i - 1);
                materiales[n - 1] = null;      // evita una fuga de memoria
                n--;
                return true;
            }
        }
        return false;
    }

    /** Devuelve una COPIA con solo las celdas ocupadas: copia defensiva. */
    public Material[] listar() {
        return Arrays.copyOf(materiales, n);
    }

    /** Ordena el catalogo con cualquier criterio, solo el tramo ocupado. */
    public void ordenar(Comparator<Material> criterio) {
        Arrays.sort(materiales, 0, n, criterio);
    }

    public int tamano() { return n; }
}

Y el uso, aplicando los comparadores de 04-06:

CatalogoArray catalogo = new CatalogoArray(4);
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));   // aqui se amplia solo

catalogo.ordenar(Comparator.comparing(Material::getTipo)
                           .thenComparing(Material::getTitulo));

for (Material m : catalogo.listar()) {
    System.out.printf("%-10s %-24s %s%n", m.getTipo(), m.getTitulo(), m.getReferencia());
}
DVD        Refactorizacion en vivo  DVD-0007
Libro      Java Efectivo            978-0000000001
Libro      Patrones de Diseno       978-0000000002
Libro      Refactorizacion          978-0000000003
Revista    Java Magazine            REV-2024-03

Funciona. Y contiene, escritas a mano, tres piezas que en la lección siguiente te dará el JDK hechas: el crecimiento automático, el desplazamiento al borrar y la distinción entre capacidad y tamaño ocupado.

  1. Las limitaciones que motivan las colecciones

Mira CatalogoArray con ojo crítico. Todo lo que tiene de más respecto a un List es infraestructura, no lógica de negocio:

Limitación del array Qué te obliga a escribir Qué hace la colección
Tamaño fijo Llevar un contador n, comprobar n == length, duplicar y copiar add sin más: crece sola
Sin borrado System.arraycopy para tapar el hueco y poner null al final remove
Sin búsqueda por criterio Un bucle con if por cada criterio nuevo contains, indexOf, removeIf
Capacidad ≠ tamaño Recordar que length no es "cuántos hay" y hacer copyOf(a, n) al devolver size() es la verdad
Sin garantía de unicidad Comprobar a mano si un ISBN ya existe, con un bucle O(n) Set (05-06)
Sin acceso por clave Bucle O(n) por referencia; peor, bucles anidados O(n²) para agrupar Map (05-05)
Ninguna semántica Una "cola" es un array más dos índices que mantienes tú Queue, Deque (05-07, 05-08)

Ninguna de esas limitaciones convierte al array en algo obsoleto. Un array sigue siendo la opción correcta cuando:

  • El tamaño es conocido y fijo (una matriz 3×12, un tablero, un buffer de 4096 bytes).
  • Trabajas con primitivos y el rendimiento importa: un int[] de un millón de elementos ocupa 4 MB; un List<Integer> puede ocupar cinco veces más por el autoboxing (05-02).
  • Necesitas máxima velocidad de recorrido y la localidad de caché es determinante.
  • Estás implementando una estructura de datos, como hacen ArrayList, HashMap y ArrayDeque por dentro.

Para todo lo demás —que en una aplicación de gestión es prácticamente todo— la respuesta es el Framework de Colecciones.

Errores Comunes y Consejos

length con paréntesis. array.length() no compila. Es un atributo en arrays, un método en String y size() en colecciones. Ténlo en la cabeza como una tabla de tres filas.

Confundir capacidad con contenido. new Material[10] tiene length == 10 pero cero materiales. Si solo has rellenado tres celdas, recorrer las diez te dará siete NullPointerException. Lleva siempre un contador o recorta con Arrays.copyOf(array, n) antes de devolver.

Imprimir un array directamente. System.out.println(array) muestra [I@1b6d3586. Usa Arrays.toString(array) y, con más de una dimensión, Arrays.deepToString(array).

Comparar arrays con equals o ==. Ambos comparan referencias. Usa Arrays.equals y, con más dimensiones, Arrays.deepEquals.

Creer que el for-each modifica el array. for (int n : numeros) { n = 0; } no cambia nada: n es una copia. Si escribes en las celdas, usa for clásico. Si solo llamas a métodos del objeto, el for-each va perfecto.

Asignar en vez de copiar. Material[] copia = original; no copia; crea un alias. Usa original.clone() o Arrays.copyOf(original, original.length). Y recuerda que ambas son superficiales: los objetos apuntados se comparten.

Arrays.asList como si fuera una lista normal. Es una vista de tamaño fijo respaldada por el array: set sí, add/remove lanzan UnsupportedOperationException. Si necesitas modificarla, envuélvela en new ArrayList<>(...).

binarySearch sobre un array desordenado. Devuelve resultados sin sentido, sin ningún aviso. Ordena primero, con el mismo criterio con el que buscas. Y recuerda que el negativo devuelto es -(punto de inserción) - 1, no un simple "no está".

Olvidar poner a null la celda liberada al borrar. En eliminar, después del arraycopy, la última celda sigue apuntando al objeto que ya no forma parte del catálogo. Mientras el array viva, ese objeto no se puede recolectar: es una fuga de memoria silenciosa. ArrayList hace exactamente ese elementData[--size] = null por el mismo motivo.

Consejo de estilo: for-each por defecto. Si el bucle no necesita el índice, escribe for (Material m : catalogo). Es más corto, más claro e imposible de romper por los límites. Baja al for clásico solo cuando el índice sea realmente necesario.

Ejercicios

Ejercicio 1: estadísticas del catálogo

Escribe una clase EstadisticasCatalogo con métodos estáticos que reciban un Material[]:

  • int contarPorTipo(Material[] catalogo, String tipo): cuántos materiales de ese tipo hay.
  • Material masCaro(Material[] catalogo): el de mayor tarifa diaria, o null si el array está vacío o es null.
  • double tarifaMedia(Material[] catalogo): media de las tarifas diarias, 0.0 si no hay elementos.
  • String[] titulos(Material[] catalogo): un array nuevo solo con los títulos.

Todos deben ignorar las celdas a null y usar for-each cuando sea posible.

Ejercicio 2: informe mensual con array bidimensional

Crea InformeMensual que gestione un int[][] prestamos de 3 filas (Libro, Revista, DVD) por 12 columnas (meses). Implementa:

  • void registrar(int tipo, int mes): incrementa la celda correspondiente.
  • int totalPorTipo(int tipo) y int totalPorMes(int mes).
  • int mesMasActivo(): el índice del mes con más préstamos totales.
  • String tabla(): una tabla formateada con printf que muestre filas, columnas y totales.

Ejercicio 3: caja de herramientas con Arrays

Escribe UtilCatalogo con métodos estáticos que apliquen la clase Arrays:

  • Material[] anadir(Material[] catalogo, Material nuevo): devuelve un array nuevo con un elemento más (sin modificar el original).
  • Material[] eliminarEn(Material[] catalogo, int indice): devuelve un array nuevo sin ese elemento.
  • Material[] ordenadoPorTitulo(Material[] catalogo): devuelve una copia ordenada, dejando intacto el original.
  • int buscarPorReferencia(Material[] catalogo, String referencia): usa Arrays.sort + Arrays.binarySearch sobre una copia y explica en un comentario por qué el índice devuelto no sirve para el array original.

Soluciones

Solución 1

package com.nexussoftware.bibliotech.servicio;

import com.nexussoftware.bibliotech.dominio.Material;

public final class EstadisticasCatalogo {

    private EstadisticasCatalogo() { }     // clase de utilidades: no se instancia

    public static int contarPorTipo(Material[] catalogo, String tipo) {
        if (catalogo == null || tipo == null) { return 0; }
        int n = 0;
        for (Material m : catalogo) {                 // for-each: no necesitamos el indice
            if (m != null && m.getTipo().equals(tipo)) {   // equals, no ==, para cadenas (01-05)
                n++;
            }
        }
        return n;
    }

    public static Material masCaro(Material[] catalogo) {
        if (catalogo == null) { return null; }
        Material mejor = null;
        for (Material m : catalogo) {
            if (m == null) { continue; }
            // la primera vez 'mejor' es null: hay que tratarlo aparte
            if (mejor == null || m.getTarifaDiaria() > mejor.getTarifaDiaria()) {
                mejor = m;
            }
        }
        return mejor;
    }

    public static double tarifaMedia(Material[] catalogo) {
        if (catalogo == null) { return 0.0; }
        double suma = 0.0;
        int    n    = 0;                      // contamos SOLO los no nulos
        for (Material m : catalogo) {
            if (m != null) { suma += m.getTarifaDiaria(); n++; }
        }
        return (n == 0) ? 0.0 : suma / n;     // proteccion contra la division por cero
    }

    public static String[] titulos(Material[] catalogo) {
        if (catalogo == null) { return new String[0]; }   // nunca devuelvas null: array vacio
        String[] resultado = new String[catalogo.length]; // tamano MAXIMO posible
        int n = 0;
        for (Material m : catalogo) {
            if (m != null) { resultado[n++] = m.getTitulo(); }
        }
        // recortamos a lo realmente usado: el patron que ArrayList hara innecesario
        return java.util.Arrays.copyOf(resultado, n);
    }
}

Los cuatro métodos comparten el mismo esqueleto defensivo: comprobar null en el array, saltar los elementos null y devolver un valor neutro (0, null documentado, array vacío) cuando no hay datos. El detalle importante de titulos es el patrón "crear del tamaño máximo, contar, recortar": es la única forma de devolver un array del tamaño exacto cuando no sabes de antemano cuántos elementos habrá. Es también, exactamente, lo que hace ArrayList.toArray() por ti.

Solución 2

package com.nexussoftware.bibliotech.presentacion;

public class InformeMensual {

    private static final String[] TIPOS = { "Libros", "Revistas", "DVDs" };
    private static final String[] MESES = { "Ene", "Feb", "Mar", "Abr", "May", "Jun",
                                            "Jul", "Ago", "Sep", "Oct", "Nov", "Dic" };

    private final int[][] prestamos = new int[TIPOS.length][MESES.length];   // 3 x 12, todo a 0

    public void registrar(int tipo, int mes) {
        // comprobamos limites a mano: el modulo 6 ensenara a senalarlo con una excepcion
        if (tipo < 0 || tipo >= TIPOS.length || mes < 0 || mes >= MESES.length) {
            System.out.println("AVISO: indice fuera de rango, registro ignorado");
            return;
        }
        prestamos[tipo][mes]++;
    }

    public int totalPorTipo(int tipo) {
        int total = 0;
        for (int valor : prestamos[tipo]) {    // recorremos una FILA entera: es un int[]
            total += valor;
        }
        return total;
    }

    public int totalPorMes(int mes) {
        int total = 0;
        for (int[] fila : prestamos) {         // recorremos las filas y tomamos una columna
            total += fila[mes];
        }
        return total;
    }

    public int mesMasActivo() {
        int mejorMes = 0;
        int mejorTotal = totalPorMes(0);
        for (int mes = 1; mes < MESES.length; mes++) {   // for clasico: necesitamos el indice
            int total = totalPorMes(mes);
            if (total > mejorTotal) { mejorTotal = total; mejorMes = mes; }
        }
        return mejorMes;
    }

    public String tabla() {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("%-10s", ""));
        for (String mes : MESES) { sb.append(String.format("%5s", mes)); }
        sb.append(String.format("%8s%n", "TOTAL"));

        for (int t = 0; t < TIPOS.length; t++) {
            sb.append(String.format("%-10s", TIPOS[t]));
            for (int valor : prestamos[t]) { sb.append(String.format("%5d", valor)); }
            sb.append(String.format("%8d%n", totalPorTipo(t)));
        }

        sb.append(String.format("%-10s", "TOTAL"));
        for (int mes = 0; mes < MESES.length; mes++) {
            sb.append(String.format("%5d", totalPorMes(mes)));
        }
        sb.append(String.format("%8d%n", totalGeneral()));
        sb.append("Mes mas activo: ").append(MESES[mesMasActivo()]).append('\n');
        return sb.toString();
    }

    private int totalGeneral() {
        int total = 0;
        for (int[] fila : prestamos) {
            for (int valor : fila) { total += valor; }
        }
        return total;
    }
}

Observa la alternancia deliberada entre los dos bucles. totalPorTipo recorre una fila con for-each porque no necesita saber qué mes es cada valor. totalPorMes recorre las filas con for-each pero indexa la columna, porque el mes viene dado. Y mesMasActivo necesita el for clásico porque lo que devuelve es el índice. Cada bucle usa la herramienta que le corresponde.

Solución 3

package com.nexussoftware.bibliotech.servicio;

import java.util.Arrays;
import java.util.Comparator;
import com.nexussoftware.bibliotech.dominio.Material;

public final class UtilCatalogo {

    private UtilCatalogo() { }

    /** Devuelve un array NUEVO con un elemento mas. El original no se toca. */
    public static Material[] anadir(Material[] catalogo, Material nuevo) {
        if (catalogo == null) { return new Material[] { nuevo }; }
        // copyOf con length+1 crea el array ampliado con la ultima celda a null
        Material[] ampliado = Arrays.copyOf(catalogo, catalogo.length + 1);
        ampliado[catalogo.length] = nuevo;
        return ampliado;
    }

    /** Devuelve un array NUEVO sin el elemento de esa posicion. */
    public static Material[] eliminarEn(Material[] catalogo, int indice) {
        if (catalogo == null || indice < 0 || indice >= catalogo.length) {
            return catalogo;                       // nada que hacer
        }
        Material[] resultado = new Material[catalogo.length - 1];
        // dos copias: el tramo anterior al indice y el posterior
        System.arraycopy(catalogo, 0, resultado, 0, indice);
        System.arraycopy(catalogo, indice + 1, resultado, indice,
                         catalogo.length - indice - 1);
        return resultado;
    }

    /** Copia ordenada por titulo. El original conserva su orden. */
    public static Material[] ordenadoPorTitulo(Material[] catalogo) {
        if (catalogo == null) { return new Material[0]; }
        Material[] copia = catalogo.clone();       // clone: copia superficial del mismo tamano
        Arrays.sort(copia, Comparator.comparing(Material::getTitulo));
        return copia;                              // los Material son los MISMOS objetos
    }

    /**
     * Busca por referencia con busqueda binaria sobre una copia ordenada.
     *
     * IMPORTANTE: el indice devuelto por binarySearch es la posicion en la COPIA
     * ORDENADA, no en el array original. Por eso no lo devolvemos: recuperamos el
     * material encontrado y buscamos SU posicion real en el original. Confundir
     * ambos indices es uno de los errores mas sutiles al trabajar con arrays.
     */
    public static int buscarPorReferencia(Material[] catalogo, String referencia) {
        if (catalogo == null || referencia == null) { return -1; }

        Comparator<Material> porReferencia = Comparator.comparing(Material::getReferencia);
        Material[] copia = catalogo.clone();
        Arrays.sort(copia, porReferencia);

        // binarySearch necesita un "elemento sonda" con la referencia buscada
        Material sonda = new Libro("", "", referencia, 2000);
        int posEnCopia = Arrays.binarySearch(copia, sonda, porReferencia);
        if (posEnCopia < 0) { return -1; }          // negativo = punto de insercion, no esta

        Material encontrado = copia[posEnCopia];
        for (int i = 0; i < catalogo.length; i++) {  // traducimos a la posicion real
            if (catalogo[i] == encontrado) { return i; }
        }
        return -1;
    }
}

Este ejercicio deja al descubierto lo incómodo que resulta el array para operaciones cotidianas. anadir construye un array entero para meter un elemento: O(n) cada vez. eliminarEn necesita dos arraycopy. Y buscarPorReferencia cuesta más que la búsqueda lineal que pretendía evitar, porque ordenar la copia es O(n log n) y luego hay que traducir el índice. La conclusión honesta es que para buscar por clave, el array es la estructura equivocada: lo correcto es un Map<String, Material>, con búsqueda O(1) y sin copias ni sondas. Es la lección 05-05.

Conclusión

Ya dominas la estructura sobre la que está construido todo lo demás. Sabes que un array es un bloque contiguo de memoria de tamaño fijo y tipo homogéneo, que es un objeto del heap aunque no tenga una clase visible, y que su contigüidad le da acceso O(1) por índice y una localidad de caché que ninguna otra estructura iguala. Distingues declarar, crear con new e inicializar con literal; conoces los valores por defecto y sabes que new Material[10] contiene diez null, no diez materiales; y tienes claro que length es un atributo, que los índices van de 0 a length - 1 y que salirse lanza ArrayIndexOutOfBoundsException —una excepción que el módulo 6 te enseñará a tratar.

Has aprendido por fin el for-each que quedó pospuesto en el módulo 2: su sintaxis for (Tipo e : origen), sus ventajas —menos código, imposible salirse de rango, misma forma para arrays y colecciones— y sus tres limitaciones exactas: no da el índice, no puede reasignar la celda (aunque sí modificar el objeto apuntado) y solo va hacia delante. Y tienes el criterio: for-each por defecto, for clásico cuando el índice sea imprescindible.

Sabes qué guarda realmente cada celda —valores en los arrays de primitivos, referencias en los de objetos—, con todo lo que se deriva: aliasing, null, peor localidad y la covarianza que puede lanzar ArrayStoreException en ejecución, el agujero que los genéricos de 10-01 vinieron a tapar. Manejas arrays multidimensionales entendiéndolos como arrays de arrays, incluidos los dentados con filas de longitudes distintas. Y tienes la clase Arrays en las manos: toString/deepToString para depurar, sort con y sin Comparator, binarySearch con sus dos advertencias, fill, copyOf, copyOfRange, equals/deepEquals frente al equals heredado que compara referencias, y asList con su trampa de vista de tamaño fijo. Sabes copiar de cuatro formas, y que todas son superficiales. Y sabes que un parámetro varargs es literalmente un array, nunca null, siempre el último.

BiblioTech tiene ahora un CatalogoArray que crece solo, borra tapando el hueco, devuelve copias defensivas y se ordena con cualquier Comparator del módulo 4. Funciona. Y su código es, en tres cuartas partes, infraestructura que no debería estar ahí: un contador n paralelo al length, un Arrays.copyOf para duplicar la capacidad, un System.arraycopy para desplazar, un null manual para no filtrar memoria. Nada de eso habla de bibliotecas ni de préstamos. Además, sigue sin poder garantizar que no haya ISBN duplicados sin un bucle O(n), y sin poder buscar por referencia en menos de O(n) ni agrupar préstamos por empleado sin bucles anidados O(n²).

En la lección siguiente, El Framework de Colecciones, verás el mapa completo de las estructuras que el JDK ya trae resueltas: la jerarquía IterableCollectionList/Set/Queue, el Map que va aparte y por qué, la regla de oro «declara por la interfaz, instancia la implementación», la tabla maestra que compara las diez implementaciones que usarás el resto de tu vida profesional, cómo funciona por dentro el for-each que acabas de aprender —con el Iterator que hay debajo y la temida ConcurrentModificationException— y un árbol de decisión para elegir la colección correcta a la primera. A partir de ahí, cada lección del módulo desarrollará una fila de esa tabla, y el CatalogoArray que acabas de escribir se quedará en la mitad de líneas.

Curso de Programación en Java

Módulo 1: Introducción a Java

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2026. Todos los derechos reservados