Arrays en Java — de las listas de Python a tamaño fijo, índices y referencias

Recuerdo la primera vez que un programa mío en Java se paró de golpe con un mensaje en rojo que decía ArrayIndexOutOfBoundsException. Era un bucle que recorría un array de notas y yo estaba convencido de que el código estaba perfecto: había escrito i <= notas.length. Venía de Python, donde un fallo así me habría dado un IndexError y listo, pero Java me obligó a parar y entender por qué el último índice es length - 1. Tardé un buen rato en darme cuenta de que el problema no era el bucle, sino que no había entendido qué es un array por dentro. Este artículo es para que a ti no te pase.

Vamos a ver qué es un array, cómo se crea, cómo se recorre, por qué dos arrays «iguales» a veces no lo son y qué hace la clase Arrays. Si vienes de Python, iremos comparando con listas y tuplas, porque ahí está la mitad de las confusiones. Y si todavía no has visto cómo funcionan los métodos, te conviene pasar antes por Métodos en Java: lo usaremos al final.

Qué es un array (y en qué se parece a una lista de Python)

Un array es una colección de valores del mismo tipo, guardados uno detrás de otro, con un tamaño fijo que se decide al crearlo. Piensa en una caja de huevos: tiene un número concreto de huecos, todos sirven para lo mismo (huevos), y cada hueco tiene una posición.

Comparado con Python, quédate con estas tres diferencias:

  • Todos los elementos son del mismo tipo. Un int[] solo guarda enteros. En una lista de Python puedes mezclar números y textos; aquí no.
  • El tamaño es fijo. No existe append ni remove. Si necesitas más huecos, tienes que crear otro array más grande. (Para tamaño variable, Java tiene ArrayList, que verás más adelante en colecciones.)
  • Los elementos sí se pueden modificar. En eso se parece a una lista, y no a una tupla.

Un resumen para tener a mano:

Lista de PythonTupla de PythonArray de Java
Tipos mezcladosSíSíNo, un solo tipo
Cambia de tamañoSíNoNo
Se pueden modificar elementosSíNoSí

Declarar y crear un array

En Java hay que distinguir dos pasos que en Python no se ven: declarar la variable (decir qué tipo de array es) y crear el array (reservar los huecos). El tipo va seguido de corchetes:

int[] notas;   // declaración: "notas" será un array de enteros (todavía no existe)

Hay dos formas de crearlo. La primera es darle los valores directamente, entre llaves:

int[] notas = {7, 5, 9, 4, 10};   // 5 huecos, ya con valores

La segunda es indicar solo el tamaño con new, y rellenarlo después:

int[] notas = new int[5];   // 5 huecos, todos valen 0
notas[0] = 7;
notas[1] = 5;

¿Y qué valen los huecos si no los relleno?

Java nunca deja un hueco «sin valor»: al crear el array con new, cada elemento recibe el valor por defecto de su tipo:

Tipo de los elementosValor por defecto
int, long, short, byte0
double, float0.0
booleanfalse
charel carácter nulo ('\u0000')
String y cualquier objetonull («no apunta a nada»)
int[] enteros = new int[3];
boolean[] marcas = new boolean[3];
String[] nombres = new String[3];

System.out.println(enteros[0]);   // 0
System.out.println(marcas[0]);    // false
System.out.println(nombres[0]);   // null

Un array de String recién creado no contiene textos vacíos: contiene null. Si intentas hacer nombres[0].length() antes de asignarle algo, obtendrás un NullPointerException. Es un error clásico.

Acceder a los elementos: índices y length

Igual que en Python, los índices empiezan en 0. En un array de 5 elementos, las posiciones válidas son 0, 1, 2, 3 y 4.

int[] notas = {7, 5, 9, 4, 10};

int primera = notas[0];       // 7
notas[3] = 6;                 // ahora el array es {7, 5, 9, 6, 10}

int cuantas = notas.length;   // 5
int ultima = notas[notas.length - 1];   // 10

Fíjate en que length no lleva paréntesis: en Python escribías len(lista), aquí es notas.length. Es un atributo del array, no un método. (Con los String pasa al revés: texto.length() sí lleva paréntesis. Una de las pequeñas trampas de Java.)

Y una diferencia importante con Python: los índices negativos no existen. En Python lista[-1] es el último elemento; en Java, notas[-1] da error. Para el último hay que escribir notas[notas.length - 1].

ArrayIndexOutOfBoundsException. Si pides una posición que no existe, el programa se detiene:

notas[5] → Index 5 out of bounds for length 5

Con 5 elementos, el último índice es 4, no 5. La regla: el último índice siempre es length - 1. Es justo lo que me pasó a mí con aquel bucle; lo verás con calma en la sección siguiente.

Recorrer un array

Hay dos formas, igual que en Python: por índices o por valores.

Por índices: el for clásico

int[] notas = {7, 5, 9, 4, 10};

for (int i = 0; i < notas.length; i++) {
    System.out.println("Posición " + i + ": " + notas[i]);
}

Lo importante es la condición: i < notas.length, con menor estricto. La última vuelta ocurre con i = 4 y, al sumar uno, i vale 5 y el bucle se corta. Si escribes i <= notas.length, hay una vuelta de más con i = 5 y aparece la excepción de antes.

El fallo más común al recorrer arrays: escribir <= en lugar de < en la condición del for. El programa compila sin problemas y falla al ejecutarse, justo en la última vuelta.

Por valores: el for-each

Si solo necesitas leer cada valor y no te importa su posición, Java tiene una versión más corta, equivalente a for valor in lista de Python:

int suma = 0;
for (int nota : notas) {      // "para cada nota en notas"
    suma += nota;
}
System.out.println("Suma: " + suma);

Se lee así: «para cada int llamado nota dentro de notas«. Los dos puntos : sustituyen a la palabra in de Python.

¿Cuál uso? Si necesitas el índice (para saber la posición, para recorrer dos arrays a la vez, o para modificar elementos), usa el for clásico. Si solo vas a leer los valores, usa el for-each. Tiene una trampa: en un for-each de int, la variable nota es una copia de cada valor, así que hacer nota = 0 dentro del bucle no cambia el array.

El punto que más confunde: un array es una referencia

Esto es lo que más cuesta al principio y, a la vez, lo que más te va a ahorrar errores si lo entiendes bien. Mira este código y piensa qué crees que imprime:

int[] notas = {7, 5, 9};
int[] copia = notas;      // ¿esto copia el array?
copia[0] = 99;

System.out.println(notas[0]);   // ¿7 o 99?

Imprime 99. Y no es un fallo: es cómo funcionan los arrays. Una variable de tipo int[] no contiene los números: contiene la dirección donde está el array en memoria. Es como la dirección de una casa. Cuando escribes int[] copia = notas;, copias la dirección, no la casa. Ahora hay dos variables que apuntan a la misma casa, y si alguien cambia los muebles, lo ve cualquiera de los dos.

Con los números sueltos no ocurre: int b = a; copia el valor y a partir de ahí son independientes. Los arrays (y todos los objetos) se comportan distinto. Si quieres una copia de verdad, hay que crear otro array:

import java.util.Arrays;

int[] notas = {7, 5, 9};
int[] copia = Arrays.copyOf(notas, notas.length);   // array nuevo e independiente
copia[0] = 99;

System.out.println(notas[0]);   // 7  (el original no cambia)
System.out.println(copia[0]);   // 99

Por qué == no sirve para comparar arrays

La misma idea explica otro tropiezo. El operador == compara direcciones, no contenidos:

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

System.out.println(a == b);                 // false  (son dos arrays distintos)
System.out.println(Arrays.equals(a, b));    // true   (mismo contenido)

Es la misma historia que == contra .equals() con los String: para comparar contenido, se usa un método. Para arrays, Arrays.equals(a, b).

Y si imprimo un array directamente, ¿qué sale?

int[] notas = {7, 5, 9};
System.out.println(notas);    // [I@49476842  (¡no son los números!)

Sale algo como [I@49476842: el tipo del array ([I significa «array de int») y una dirección de memoria. Para ver el contenido, usa Arrays.toString(notas), que da [7, 5, 9].

Imprimir un array con println(array). Si ves algo del estilo [I@6d06d69c, no es un fallo de tu programa: simplemente te falta Arrays.toString(...).

Esta idea de «la variable guarda una dirección» se volverá a ver con clases y objetos: String, y cualquier objeto que crees, funcionan igual. Si lo entiendes aquí con arrays, ese tema ya lo llevas medio hecho.

Arrays y métodos

Lo que acabamos de ver tiene una consecuencia directa si ya conoces los métodos: cuando pasas un array a un método, se pasa la dirección, no una copia. Es decir, el método puede modificar tu array original.

static void subirUnPunto(int[] datos) {
    for (int i = 0; i < datos.length; i++) {
        datos[i]++;                    // modifica el array original
    }
}

public static void main(String[] args) {
    int[] notas = {7, 5, 9};
    subirUnPunto(notas);
    System.out.println(Arrays.toString(notas));   // [8, 6, 10]
}

Con un int suelto esto no pasa (el método recibe una copia del valor). Con un array, sí. Es muy útil, pero también una fuente de errores: si un método «ordena» o «cambia» tu array sin avisar, luego te sorprendes. Una buena costumbre es dejar claro en el nombre del método si modifica o si solo calcula (subirUnPunto modifica; media solo calcula).

Arrays de varias dimensiones

En Python, una tabla se representa con una lista de listas. En Java, con un array de arrays, y se declara con un par de corchetes por dimensión:

int[][] tabla = {
    {10, 15, 20, 30, 40},
    {12, 14, 16, 18},
    {11, 17, 23, 29, 31}
};

System.out.println(tabla[1][3]);   // 18  → fila 1, columna 3

La forma de pensarlo: tabla es un array de 3 elementos, y cada elemento es a su vez un array de enteros. Por eso tabla[1] es la segunda fila entera, y tabla[1][3] es el elemento 3 de esa fila.

También puedes crear una tabla vacía indicando filas y columnas:

int[][] tabla = new int[2][3];   // 2 filas, 3 columnas, todo a 0
tabla[1][2] = 5;
System.out.println(Arrays.deepToString(tabla));   // [[0, 0, 0], [0, 0, 5]]

Fíjate en que para imprimir una tabla se usa deepToString («a fondo»), porque toString normal solo sabría mostrar las direcciones de las filas.

Como cada fila es un array independiente, las filas pueden tener longitudes distintas (en el ejemplo de arriba, 5, 4 y 5 elementos). Por eso, al recorrer, la longitud de cada fila se pide con tabla[i].length y no con una única longitud común.

Recorrer una tabla

Hacen falta dos bucles anidados: el de fuera recorre las filas, el de dentro recorre los elementos de cada fila.

// Por índices
for (int fila = 0; fila < tabla.length; fila++) {
    for (int col = 0; col < tabla[fila].length; col++) {
        System.out.print(tabla[fila][col] + " ");
    }
    System.out.println();   // salto de línea al terminar cada fila
}

// Por valores
for (int[] fila : tabla) {          // cada "fila" es un int[]
    for (int valor : fila) {
        System.out.print(valor + " ");
    }
    System.out.println();
}

En el for-each, la variable exterior debe ser del tipo de un nivel menos: si tabla es int[][], cada fila es un int[]. Es la única regla que hay que recordar para no equivocarse con los tipos.

La clase Arrays: herramientas ya hechas

Java trae una clase llamada Arrays (en el paquete java.util, por eso hay que escribir import java.util.Arrays; arriba del archivo) con operaciones muy usadas que no merece la pena programar a mano. Las principales:

MétodoQué hace
Arrays.toString(a)Devuelve el contenido como texto: "[10, 15, 20]"
Arrays.sort(a)Ordena el array de menor a mayor (modifica el original)
Arrays.binarySearch(a, x)Busca x en un array ya ordenado y devuelve su posición
Arrays.copyOf(a, n)Crea un array nuevo de longitud n con los datos de a
Arrays.copyOfRange(a, desde, hasta)Copia un trozo (el hasta no se incluye)
Arrays.equals(a, b)Compara el contenido de dos arrays
Arrays.fill(a, x)Rellena todo el array con el valor x
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] datos = {80, 70, 60, 50, 40, 30, 20, 10};

        Arrays.sort(datos);
        System.out.println(Arrays.toString(datos));
        // [10, 20, 30, 40, 50, 60, 70, 80]

        System.out.println(Arrays.binarySearch(datos, 30));   // 2  (está en la posición 2)
        System.out.println(Arrays.binarySearch(datos, 35));   // -4 (no está)

        int[] trozo = Arrays.copyOfRange(datos, 2, 5);
        System.out.println(Arrays.toString(trozo));           // [30, 40, 50]

        int[] masGrande = Arrays.copyOf(datos, 10);           // los huecos nuevos se rellenan con 0
        System.out.println(Arrays.toString(masGrande));
        // [10, 20, 30, 40, 50, 60, 70, 80, 0, 0]

        int[] sietes = new int[5];
        Arrays.fill(sietes, 7);
        System.out.println(Arrays.toString(sietes));          // [7, 7, 7, 7, 7]
    }
}

¿Por qué binarySearch devuelve -4?

Cuando el valor no está, binarySearch devuelve -(punto de inserción) - 1, donde el «punto de inserción» es la posición donde debería estar para mantener el orden. Buscando 35 en [10, 20, 30, 40, ...], le tocaría la posición 3 (entre el 30 y el 40): -3 - 1 = -4. Lo práctico: si el resultado es negativo, no está; si es 0 o más, es su posición.

binarySearch solo funciona si el array ya está ordenado. Si lo usas sobre un array desordenado, no da error, pero el resultado no significa nada. Ordena primero con Arrays.sort.

Otra pega: Arrays.equals compara en un solo nivel. Si los elementos son a su vez arrays (una tabla), se usa Arrays.deepEquals, igual que deepToString para imprimir.

Es habitual preguntarse…

¿Puedo añadir un elemento a un array?

No: el tamaño es fijo. Lo que se hace es crear un array más grande (con Arrays.copyOf(a, a.length + 1)) y colocar el nuevo valor en el último hueco. Si necesitas hacerlo con frecuencia, es la señal de que te conviene un ArrayList.

¿Por qué hay que decir el tamaño al crearlo?

Porque Java reserva un bloque de memoria contiguo de ese tamaño. Es lo que hace que acceder a notas[3] sea tan rápido: sabe exactamente dónde está sin buscar.

¿Qué diferencia hay entre un array vacío y null?

new int[0] es un array real, con cero elementos (su length es 0 y se puede recorrer sin problemas). null significa que la variable no apunta a ningún array: si haces .length sobre ella, se produce un NullPointerException.

¿Puedo crear un array de tamaño variable, por ejemplo new int[n]?

Sí. El tamaño puede ser cualquier expresión, como una variable leída con Scanner. Lo que no puede cambiar es el tamaño después de crearlo. Si n es negativo, falla con NegativeArraySizeException.

Un ejemplo completo

Juntemos todo en un programa pequeño: calcula el máximo y la media de las notas de un grupo con métodos, y luego la media de cada grupo de una tabla. Ve leyendo los comentarios, que explican el porqué de cada decisión.

import java.util.Arrays;

public class Notas {

    // Devuelve la nota más alta. Parte del primer elemento y compara con el resto.
    static int maximo(int[] datos) {
        int max = datos[0];                  // asumimos que el primero es el mayor
        for (int i = 1; i < datos.length; i++) {   // empezamos en 1: el 0 ya lo tenemos
            if (datos[i] > max) {
                max = datos[i];
            }
        }
        return max;
    }

    // Devuelve la media. El (double) evita la división entera: 41 / 6 daría 6, no 6.83
    static double media(int[] datos) {
        int suma = 0;
        for (int n : datos) {                // for-each: solo leemos, no modificamos
            suma += n;
        }
        return (double) suma / datos.length;
    }

    // Este método SÍ modifica el array original (por eso usa el for clásico)
    static void subirUnPunto(int[] datos) {
        for (int i = 0; i < datos.length; i++) {
            datos[i]++;
        }
    }

    public static void main(String[] args) {
        int[] notas = {7, 5, 9, 4, 10, 6};

        System.out.println("Notas: " + Arrays.toString(notas));
        System.out.println("Máxima: " + maximo(notas));
        System.out.println("Media: " + media(notas));

        subirUnPunto(notas);
        System.out.println("Tras subir: " + Arrays.toString(notas));

        // Tres grupos con distinto número de alumnos (filas de distinta longitud)
        int[][] grupos = {
            {7, 5, 9},
            {4, 10},
            {6, 8, 8, 3}
        };
        for (int g = 0; g < grupos.length; g++) {
            // grupos[g] es un int[]: se lo pasamos directamente al método media
            System.out.println("Grupo " + g + " (" + grupos[g].length + " alumnos): " + media(grupos[g]));
        }
    }
}

Salida:

Notas: [7, 5, 9, 4, 10, 6]
Máxima: 10
Media: 6.833333333333333
Tras subir: [8, 6, 10, 5, 11, 7]
Grupo 0 (3 alumnos): 7.0
Grupo 1 (2 alumnos): 7.0
Grupo 2 (4 alumnos): 6.25

Fíjate en lo bien que encajan las piezas: media funciona igual con el array de notas que con grupos[g], porque cada fila de la tabla es un array de enteros. Eso es lo que significa que una tabla sea «un array de arrays».

Visualízalo con Python Tutor

Lo de las referencias se entiende mucho mejor viéndolo que leyéndolo. Entra en pythontutor.com/java.html, pega este código y avanza paso a paso con «Next»:

public class Main {
    public static void main(String[] args) {
        int[] notas = {7, 5, 9};
        int[] alias = notas;          // ¿copia o misma casa?
        alias[0] = 99;

        int[] copia = new int[notas.length];
        for (int i = 0; i < notas.length; i++) {
            copia[i] = notas[i];      // copia real, elemento a elemento
        }
        copia[1] = 0;

        System.out.println(notas[0]);
        System.out.println(copia[1]);
    }
}

Qué observar:

  • Tras int[] notas = {7, 5, 9};, a la derecha aparece un array y la variable notas con una flecha hacia él: esa flecha es la «dirección».
  • Tras int[] alias = notas;, aparece una segunda flecha que apunta al mismo array, no un array nuevo.
  • Al ejecutar alias[0] = 99;, el 7 cambia a 99 en el único array que existe: notas[0] también vale 99.
  • Con new int[notas.length] aparece un segundo array independiente, y el bucle copia los valores uno a uno. Ahora copia[1] = 0 no afecta a notas.

Resumen

  • Un array guarda valores del mismo tipo y tiene tamaño fijo; los elementos sí se pueden modificar.
  • Se crea con llaves ({1, 2, 3}) o con new tipo[tamaño]; sin rellenar, los huecos valen 0, 0.0, false o null según el tipo.
  • Los índices van de 0 a length - 1; length no lleva paréntesis y no hay índices negativos.
  • Se recorre con for clásico (necesitas índice o modificar) o con for-each (solo leer). Condición del for: i < array.length.
  • Una variable de array guarda una referencia: asignar (b = a) no copia, y == no compara contenido. Usa Arrays.copyOf y Arrays.equals.
  • Al pasar un array a un método, el método puede modificarlo.
  • Una tabla es un array de arrays (int[][]), se recorre con dos bucles y sus filas pueden tener distinta longitud.
  • La clase Arrays (toString, sort, binarySearch, copyOf, copyOfRange, equals, fill) evita reinventar la rueda.

Checklist: ¿lo tienes claro?

  • ¿Sabes declarar un array y crearlo de las dos formas (llaves y new)?
  • ¿Sabes decir qué valor tendrá cada hueco si no lo rellenas?
  • ¿Puedes explicar por qué notas[notas.length] falla?
  • ¿Sabes por qué int[] b = a; no hace una copia?
  • ¿Sabes comparar dos arrays por contenido y mostrar uno por pantalla?
  • ¿Sabes recorrer una tabla con dos bucles, por índices y por valores?

Si algún punto te ha dejado dudas, vuelve a su sección antes de seguir. En el siguiente artículo lo pondremos en práctica con programas completos.

Sigue aprendiendoTodos los artículos de Java

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *