← CC2008

Arrays

Semestre 02, 2026

Contenido de la lección

Qué es un arreglo y cómo se ve en memoria
Declaración, creación y valores por defecto
Índices, longitud y recorridos
Arreglos de distintos tipos
El salto a los arreglos de objetos
Referencias, métodos y errores típicos

El problema de las variables sueltas


int nota1 = 80;
int nota2 = 75;
int nota3 = 90;
int nota4 = 85;
int nota5 = 70;
  • Con cinco notas todavía se aguanta. Con quinientas es imposible de escribir.
  • No hay forma de recorrerlas: no se puede pedir la variable número \(i\).
  • Cada operación nueva obliga a repetir la misma línea cinco veces.

Un nombre para todo el conjunto


int[] notas = {80, 75, 90, 85, 70};
Un solo nombre identifica al conjunto completo.
Un número identifica cada posición, y ese número sí puede ser una variable.

Una estructura para elementos del mismo tipo


int[] notas = {80, 75, 90, 85, 70};
notas
01234
80
75
90
85
70

Las cuatro propiedades de un arreglo

Homogéneo: todos sus elementos son del mismo tipo.
De tamaño fijo: una vez creado no crece ni se reduce.
Indexado: se llega a cada elemento por su posición, contando desde cero.
Sus elementos ocupan posiciones contiguas de memoria, una tras otra.

Declaración y creación en memoria

Paso 1 — declarar
int[] numeros;

Anuncia que existe una variable capaz de apuntar a un arreglo de enteros. Todavía no hay espacio reservado.

Paso 2 — reservar el espacio
numeros = new int[5];

La palabra new aparta memoria para cinco enteros y entrega la referencia a ese bloque.

Paso 3 — la forma común
int[] numeros = new int[5];

El literal de arreglo


int[] notas = {80, 75, 90, 85, 70};
  • El tamaño se deduce de la cantidad de valores escritos.
  • No lleva new porque el compilador lo agrega solo.
  • Sirve únicamente en la línea de la declaración.
01234
80
75
90
85
70

La asignación posterior


int[] numeros;

numeros = {1, 2, 3};
Error de compilación

Fuera de la declaración, las llaves solas no le dicen a Java de qué tipo es el arreglo que se está construyendo.


int[] numeros;

numeros = new int[] {1, 2, 3};
La forma correcta

Al escribir new int[] delante, el tipo queda explícito y la asignación funciona en cualquier punto del programa.

Los valores por defecto


int[] numeros = new int[5];

System.out.println(numeros[0]);
Idea clave

Crear un arreglo no lo deja vacío. Java lo llena con el valor por defecto del tipo, así que se puede leer desde el primer momento.

01234
0
0
0
0
0

El valor por defecto de cada tipo


   tipo                                   valor por defecto

   int, short, long, byte                 0
   double, float                          0.0
   boolean                                false
   char                                   ' '
   cualquier objeto (String, Perro, …)    null
La última fila importa

Un arreglo de objetos arranca lleno de nulos. Esa fila es todo el puente hacia la segunda mitad de la lección.

Asignación y consulta por posición


notas[2] = 90;

System.out.println(notas[2]);
> Ejecutando programa... 90
01234
0
0
90
0
0

Una casilla se comporta como una variable

Se le puede asignar un valor, igual que a cualquier variable de su tipo.
Se puede leer dentro de una operación aritmética o de una condición.
Se puede pasar como argumento a un método.
El índice puede ser una constante, una variable o el resultado de una expresión.

La propiedad de tamaño

01234
80
75
90
85
70
length = 5

System.out.println(notas.length);

notas[notas.length - 1]

Detalles de length

No es un método: se escribe sin paréntesis.
Es de solo lectura. Asignarle un valor es un error de compilación.
La última posición válida siempre es length menos uno.
En String el tamaño se pide con length(), con paréntesis. Son cosas distintas.

El índice fuera de rango


int[] notas = new int[5];

notas[5] = 100;
Por qué no lo detecta el compilador

El índice puede ser cualquier expresión, y su valor solo se conoce al ejecutar. La verificación ocurre en ese momento.

> Ejecutando programa... Exception in thread "main" java.lang.ArrayIndexOutOf BoundsException: Index 5 out of bounds for length 5

Recorrido con el ciclo tradicional


for (int i = 0; i < notas.length; i++) {
    System.out.println(notas[i]);
}
> Ejecutando programa... 80 75 90 85 70
01234
80
75
90
85
70
iiiii

Recorrido con for-each


for (int nota : notas) {
    System.out.println(nota);
}
Cómo se lee

Para cada nota dentro de notas. La variable nota es una copia del elemento, así que asignarle un valor no toca el arreglo.


                       for       for-each

  da el índice          sí            no
  permite escribir      sí            no
  recorre al revés      sí            no
  legibilidad         media          alta

Pregunta de concepto

Pregunta

Un compañero escribe \(i \le notas.length\) en la condición del ciclo. El programa compila sin problema. ¿Qué va a pasar al ejecutarlo?

Respuesta

Las primeras cinco vueltas funcionan. En la sexta el índice vale 5, esa casilla no existe y el programa se detiene con un error de índice fuera de rango.

Población dinámica con Scanner


Scanner teclado = new Scanner(System.in);

int[] notas = new int[5];

for (int i = 0; i < notas.length; i++) {
    System.out.print("Nota " + (i + 1) + ": ");
    notas[i] = teclado.nextInt();
}
Nota 1: 85
85
0
0
0
0
01234
Se imprime i más uno porque las personas cuentan desde uno, aunque el arreglo cuente desde cero.

Algoritmo práctico: el promedio


int suma = 0;

for (int nota : notas) {
    suma += nota;
}

double promedio = (double) suma / notas.length;

System.out.println(promedio);
80
75
90
85
70
suma = 400

400 dividido entre 5 da 80.0


int suma = 0;

for (int nota : notas) {
    suma += nota;
}

double promedio = (double) suma / notas.length;

System.out.println(promedio);
int suma = 0;

El acumulador nace en cero, fuera del ciclo. Si se declarara adentro, volvería a valer cero en cada vuelta y al final quedaría solo la última nota.


int suma = 0;

for (int nota : notas) {
    suma += nota;
}

double promedio = (double) suma / notas.length;

System.out.println(promedio);
for (int nota : notas) {
    suma += nota;

Solo hace falta leer cada elemento, nunca su posición, así que el for-each es la herramienta correcta. Cada vuelta agrega una nota al total.


int suma = 0;

for (int nota : notas) {
    suma += nota;
}

double promedio = (double) suma / notas.length;

System.out.println(promedio);
(double) suma / notas.length

La conversión es obligatoria. Entre dos enteros Java hace división entera y descarta los decimales, así que un promedio de 80.4 saldría como 80.


int suma = 0;

for (int nota : notas) {
    suma += nota;
}

double promedio = (double) suma / notas.length;

System.out.println(promedio);
System.out.println(promedio);

Con las notas del ejemplo la salida es 80.0. El punto cero confirma que el resultado es un double y que la conversión funcionó.

Estructuras estrictamente homogéneas

int[] edades = {18, 20, 22};
18
20
22
double[] precios = {10.5, 20.7, 15.2};
10.5
20.7
15.2
boolean[] estados = {true, false, true};
true
false
true
String[] nombres = {"Ana", "Luis", "Leo"};
"Ana"
"Luis"
"Leo"

La regla de oro

Regla

Cada arreglo almacena elementos de un solo tipo, y ese tipo se fija al declararlo.

Meter un texto en un arreglo de enteros es un error de compilación, no de ejecución.
Gracias a esa restricción todas las casillas miden lo mismo y llegar a la posición i es una sola operación.

Arreglos como parámetros


public void imprimir(int[] arreglo) {
    for (int valor : arreglo) {
        System.out.println(valor);
    }
}
Los arreglos son objetos

Al pasar un arreglo a un método no se copia su contenido: se pasa la referencia. El parámetro y la variable original son un alias del mismo arreglo.

valores
arreglo
1
2
3

El método modifica el arreglo original


public void modificar(int[] datos) {
    datos[0] = 999;
}

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

modificar(valores);

System.out.println(valores[0]);
012
999
2
3
> Ejecutando programa... 999

Copiar para evitar efectos colaterales


import java.util.Arrays;

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

int[] copia = Arrays.copyOf(original, original.length);
Dos arreglos independientes

La copia vive en su propio espacio de memoria. Cambiarla no toca al original, y por eso sirve para pasarle datos a un método sin arriesgarlos.

original
1
2
3
4
copia
1
2
3
4

El salto conceptual: arreglos de objetos

Antes: variables independientes


Perro perro1 = new Perro("Max", 3);
Perro perro2 = new Perro("Rocky", 5);
Perro perro3 = new Perro("Luna", 2);
🐕
Max
🐕‍🦺
Rocky
🐺
Luna

Ahora: un arreglo unificado


Perro[] perros = new Perro[3];
Cada casilla no guarda un objeto: guarda una referencia a un objeto.

El estado inicial de las referencias


Perro[] perros = new Perro[3];
012
null
null
null
Todavía no existen objetos Perro

Crear el arreglo reserva tres casillas capaces de apuntar a un Perro, pero no construye ningún Perro. Hay que instanciarlos uno por uno.

Instanciación y mapa de referencias


perros[0] = new Perro("Max", 3);
perros[1] = new Perro("Rocky", 5);
perros[2] = new Perro("Luna", 2);
el arreglo
el heap
0
🐕
Max
edad = 3
1
🐕‍🦺
Rocky
edad = 5
2
🐺
Luna
edad = 2

Dos zonas de memoria distintas

El arreglo es un bloque contiguo de tres casillas, todas del mismo tamaño.
Los objetos viven aparte, cada uno con su propio espacio y sus propios atributos.
La casilla solo guarda la dirección del objeto, exactamente como haría una variable Perro suelta.
Dos casillas pueden apuntar al mismo objeto. Es el tema de aliases aplicado a arreglos.

Instanciación dentro de un ciclo


Perro[] perros = new Perro[3];

String[] nombres = {"Max", "Rocky", "Luna"};
int[] edades = {3, 5, 2};

for (int i = 0; i < perros.length; i++) {
    perros[i] = new Perro(nombres[i], edades[i]);
}

Perro[] perros = new Perro[3];

String[] nombres = {"Max", "Rocky", "Luna"};
int[] edades = {3, 5, 2};

for (int i = 0; i < perros.length; i++) {
    perros[i] = new Perro(nombres[i], edades[i]);
}
Perro[] perros = new Perro[3];

Quedan tres casillas listas y las tres apuntan a nada. Este es el punto en el que el arreglo existe pero todavía no sirve.


Perro[] perros = new Perro[3];

String[] nombres = {"Max", "Rocky", "Luna"};
int[] edades = {3, 5, 2};

for (int i = 0; i < perros.length; i++) {
    perros[i] = new Perro(nombres[i], edades[i]);
}
String[] nombres = ...
int[] edades = ...

Dos arreglos de primitivos guardan los datos crudos. Comparten el índice con el arreglo de perros, así que la posición i de los tres se refiere al mismo animal.


Perro[] perros = new Perro[3];

String[] nombres = {"Max", "Rocky", "Luna"};
int[] edades = {3, 5, 2};

for (int i = 0; i < perros.length; i++) {
    perros[i] = new Perro(nombres[i], edades[i]);
}
for (int i = 0; i < perros.length; i++)

Aquí sí hace falta el índice, porque se va a escribir en el arreglo. El for-each no serviría para esta parte.


Perro[] perros = new Perro[3];

String[] nombres = {"Max", "Rocky", "Luna"};
int[] edades = {3, 5, 2};

for (int i = 0; i < perros.length; i++) {
    perros[i] = new Perro(nombres[i], edades[i]);
}
perros[i] = new Perro(...);

Cada vuelta construye un objeto nuevo en el heap y guarda su referencia en la casilla i. Al terminar el ciclo ya no queda ningún nulo.

Ejecución de métodos sobre el arreglo

1. encuentra la referencia guardada en esa posición

arreglo[indice].metodo()

2. ejecuta la acción sobre el objeto apuntado


perros[0].ladrar();
System.out.println(perros[1].getNombre());
perros[2].setEdad(3);

Los tres usos de una casilla

Acción: se le pide al objeto que haga algo.
Consulta: se le pide un dato con un getter.
Modificación: se cambia un atributo con un setter.

Perro primero = perros[0];
primero.ladrar();
Dos nombres, un objeto

La variable primero y la casilla perros[0] apuntan al mismo lugar. Lo que se cambie con una se ve con la otra.

Recorriendo referencias y atributos

Ciclo for tradicional


for (int i = 0; i < perros.length; i++) {
    System.out.println(perros[i].getNombre());
    System.out.println(perros[i].getEdad());
}

Acceso mediante índice explícito.

Ciclo for-each


for (Perro perro : perros) {
    perro.mostrarInformacion();
}

La referencia se extrae directo a una variable temporal.

Lo que comparten

Los dos actúan como puente hacia el mismo objeto en memoria. Ninguno copia al perro.

El for-each sí modifica objetos


for (Perro perro : perros) {
    perro.setEdad(1);
}
Sí cambia el arreglo

La copia es de la referencia, y esa referencia lleva al mismo objeto que guarda la casilla.


for (Perro perro : perros) {
    perro = new Perro("Otro", 0);
}
No cambia el arreglo

Aquí solo se cambia hacia dónde apunta la variable temporal. La casilla del arreglo sigue apuntando al perro original.

La referencia nula en ejecución


Perro[] perros = new Perro[3];

perros[0].ladrar();
null
null
null
> Ejecutando programa... Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Perro.ladrar()" because "perros[0]" is null

Las dos formas de evitarlo


for (int i = 0; i < perros.length; i++) {
    perros[i] = new Perro("Sin nombre", 0);
}
Llenar antes de usar

Se separa la fase de construcción de la fase de uso. Es la opción normal.


for (Perro perro : perros) {
    if (perro != null) {
        perro.ladrar();
    }
}
Verificar antes de usar

Sirve cuando el arreglo puede quedar parcialmente lleno a propósito.

Dos errores que se confunden

ArrayIndexOutOfBoundsException

La casilla no existe. El índice quedó fuera del rango que va de cero a length menos uno.

NullPointerException

La casilla existe pero está vacía. Se pidió un método a una referencia que no apunta a ningún objeto.

El primero solo aparece en arreglos; el segundo aparece en cualquier variable de tipo objeto que no se haya instanciado.

Un registro completo, de principio a fin


Scanner teclado = new Scanner(System.in);

Perro[] perros = new Perro[3];

for (int i = 0; i < perros.length; i++) {
    System.out.print("Nombre del perro " + (i + 1) + ": ");
    String nombre = teclado.nextLine();

    System.out.print("Edad de " + nombre + ": ");
    int edad = teclado.nextInt();
    teclado.nextLine();

    perros[i] = new Perro(nombre, edad);
}

for (Perro perro : perros) {
    perro.mostrarInformacion();
}

Scanner teclado = new Scanner(System.in);

Perro[] perros = new Perro[3];

for (int i = 0; i < perros.length; i++) {
    System.out.print("Nombre del perro " + (i + 1) + ": ");
    String nombre = teclado.nextLine();

    System.out.print("Edad de " + nombre + ": ");
    int edad = teclado.nextInt();
    teclado.nextLine();

    perros[i] = new Perro(nombre, edad);
}

for (Perro perro : perros) {
    perro.mostrarInformacion();
}
Perro[] perros = new Perro[3];

El arreglo se crea vacío de objetos. Desde esta línea hasta que termine el primer ciclo, tocar cualquier casilla haría reventar el programa.


Scanner teclado = new Scanner(System.in);

Perro[] perros = new Perro[3];

for (int i = 0; i < perros.length; i++) {
    System.out.print("Nombre del perro " + (i + 1) + ": ");
    String nombre = teclado.nextLine();

    System.out.print("Edad de " + nombre + ": ");
    int edad = teclado.nextInt();
    teclado.nextLine();

    perros[i] = new Perro(nombre, edad);
}

for (Perro perro : perros) {
    perro.mostrarInformacion();
}
String nombre = teclado.nextLine();

Se pide el nombre mostrando i más uno, para que el usuario vea perro 1 y no perro 0. El dato se guarda en una variable local antes de construir nada.


Scanner teclado = new Scanner(System.in);

Perro[] perros = new Perro[3];

for (int i = 0; i < perros.length; i++) {
    System.out.print("Nombre del perro " + (i + 1) + ": ");
    String nombre = teclado.nextLine();

    System.out.print("Edad de " + nombre + ": ");
    int edad = teclado.nextInt();
    teclado.nextLine();

    perros[i] = new Perro(nombre, edad);
}

for (Perro perro : perros) {
    perro.mostrarInformacion();
}
int edad = teclado.nextInt();
teclado.nextLine();

La llamada suelta a nextLine consume el salto de línea que nextInt deja pendiente. Sin ella, la siguiente lectura de nombre saldría vacía.


Scanner teclado = new Scanner(System.in);

Perro[] perros = new Perro[3];

for (int i = 0; i < perros.length; i++) {
    System.out.print("Nombre del perro " + (i + 1) + ": ");
    String nombre = teclado.nextLine();

    System.out.print("Edad de " + nombre + ": ");
    int edad = teclado.nextInt();
    teclado.nextLine();

    perros[i] = new Perro(nombre, edad);
}

for (Perro perro : perros) {
    perro.mostrarInformacion();
}
perros[i] = new Perro(nombre, edad);

Aquí desaparece el nulo de la casilla i. El objeto se construye en el heap y la casilla pasa a guardar su dirección.


Scanner teclado = new Scanner(System.in);

Perro[] perros = new Perro[3];

for (int i = 0; i < perros.length; i++) {
    System.out.print("Nombre del perro " + (i + 1) + ": ");
    String nombre = teclado.nextLine();

    System.out.print("Edad de " + nombre + ": ");
    int edad = teclado.nextInt();
    teclado.nextLine();

    perros[i] = new Perro(nombre, edad);
}

for (Perro perro : perros) {
    perro.mostrarInformacion();
}
for (Perro perro : perros)

El segundo ciclo solo lee, así que el for-each es suficiente. Separar el llenado del recorrido es lo que garantiza que aquí ya no haya nulos.

Parámetros variables


public double promedio(int... numeros) {
    int suma = 0;

    for (int n : numeros) {
        suma += n;
    }

    return (double) suma / numeros.length;
}

promedio(10, 20, 30);
promedio(1, 2, 3, 4, 5, 6);
Los tres puntos

La sintaxis con puntos suspensivos deja que el método reciba una cantidad distinta de argumentos en cada llamada.

Adentro del método, esos argumentos ya son un arreglo normal: se recorre con for-each y tiene length.

Las reglas de los parámetros variables

Internamente los argumentos se tratan como un arreglo.
Solo puede haber un parámetro variable por método.
Debe ser el último de la lista de parámetros.
Es la opción indicada cuando no se sabe de antemano cuántos argumentos van a llegar.

Arreglos multidimensionales


int[][] matriz = new int[3][4];

matriz[0][0] = 10;

System.out.println(matriz[0][0]);
Un arreglo de arreglos

Cada casilla de la primera dimensión guarda la referencia a otro arreglo. En álgebra lineal a esta estructura se le llama matriz.

012
10
0
0
0
0
0
0
0
0
0
0
0

Los dos índices

El primer índice elige la fila, es decir cuál de los arreglos internos se va a usar.
El segundo índice elige la columna dentro de ese arreglo.
El número de filas se pide con matriz.length y el de columnas con matriz[0].length.
Recorrerla completa pide dos ciclos anidados, uno por dimensión.

Síntesis: primitivos contra objetos


                            arreglo de primitivos      arreglo de objetos

   creación                 int[] n = new int[5];      Perro[] p = new Perro[3];
   cada casilla guarda      el valor                   una referencia
   estado inicial           0, 0.0, false              null
   listo tras new           sí                         no, falta instanciar
   acceso                   n[0]                       p[0].metodo()
   error típico             índice fuera de rango      referencia nula
Lo que comparten

El tamaño es fijo y se consulta con length, el primer índice es cero y el último es length menos uno, y los dos funcionan igual con for y con for-each.

Errores comunes

1Usar menor o igual en la condición
  • En la última vuelta el índice vale length.
  • Esa casilla no existe y el programa se detiene.
2Creer que new crea los objetos
  • En un arreglo de objetos solo reserva casillas.
  • Cada objeto se construye aparte.
3Escribir length con paréntesis
  • En arreglos es un atributo, no un método.
  • En String sí lleva paréntesis, y de ahí viene la confusión.
4Esperar que el arreglo crezca
  • El tamaño se fija al crearlo y no cambia.
  • Para tamaño variable hace falta otra estructura.

Errores comunes

5Reasignar la variable del for-each
  • Solo cambia la variable temporal.
  • La casilla del arreglo queda igual.
6Declarar el acumulador dentro del ciclo
  • Vuelve a cero en cada vuelta.
  • Al final solo queda el último elemento.
7Dividir dos enteros para un promedio
  • Java descarta los decimales.
  • Hace falta convertir a double antes de dividir.
8Recorrer un arreglo a medio llenar
  • Las casillas que faltan siguen en null.
  • Conviene verificar o llenar el arreglo completo.

Buenas prácticas

  • Verificar el tamaño con length antes de acceder a una posición calculada.
  • Instanciar los objetos de un arreglo antes de usarlos, o verificar que no sean nulos.
  • Recordar que pasar un arreglo a un método entrega la referencia, así que modificarlo adentro cambia el original.
  • Copiar el arreglo cuando el método no deba tocar los datos de quien lo llamó.

Conclusiones

Los arreglos agrupan datos del mismo tipo de manera ordenada, bajo un solo nombre y con un índice por posición.
Son objetos y se comportan como tales: se pasan por referencia y su tamaño queda fijo al crearlos.
Guardan tanto valores primitivos como referencias a objetos, y la diferencia entre ambos casos está en el estado inicial.
Son la base de las estructuras más complejas que vienen después, empezando por los arreglos dinámicos.