Saltearse al contenido

Arreglos

Arreglos

Los arreglos, pertenecientes a la clase Array, son estructuras de datos que nos permiten almacenar secuencias de datos de un mismo tipo.

Es decir, una arreglo representa una colección de datos homogéneos. Por ejemplo, un arreglo de números enteros, un arreglo de cadenas de texto, un arreglo de objetos de una clase específica, etc.

Los arreglos pueden declarse de múltiples maneras, pero siempre deben indicar el tipo de datos que almacenarán, y agregar corchetes ([]) después del tipo de datos. Por ejemplo, todas las siguientes formas de declarar arreglos son válidas:

1
<tipo>[] <nombre>;
2
<tipo> []<nombre>;
3
<tipo> <nombre>[];

Recordemos emplear la última forma de declarar arreglos, ya que es la más legible y utilizada. Por ejemplo:

1
int numeros[];
2
String nombres[];

Si bien hemos declarado los arreglos, aun no podemos utilizarlos, ya que este tipo de estructuras de datos deben ser inicializadas antes de poder utilizarlas.

Este proceso es considerado un tipo de asignación, y consiste en asignarle un espacio de memoria al arreglo, indicando la cantidad de elementos que podrá almacenar. Para ello, utilizamos la palabra reservada new, seguida del tipo de datos del arreglo, y entre corchetes, la cantidad de elementos que podrá almacenar.

1
<nombre> = new <tipo>[<cantidad>];

Este es el proceso de creación que debemos emplear con los tipos de datos no primitivos, y lo veremos continuamente cuando trabajemos con otros tipos de datos no primitivos.

Una vez aclaramos esto, podemos inicializar nuestros arreglos. Para lo cual emplearemos los ejemplo anteriores:

1
int numeros[] = new int[10];
2
String nombres[] = new String[5];

En este caso, hemos inicializado dos arreglos, uno de números enteros, y otro de cadenas de texto. Ambos con una cantidad de elementos específica.

Al inicializar arreglos de esta manera los elementos se inicializan con un valor por defecto, que depende del tipo de datos del arreglo. Por ejemplo, en el caso de los números enteros, el valor por defecto es 0, y en el caso de las cadenas de texto, el valor por defecto es null (más adelante veremos qué significa esto). Sin embargo, podemos inicializar los arreglos con valores específicos, para lo cual debemos indicarlos entre llaves, separados por comas. Por ejemplo:

1
int numeros[] = {1, 2, 3, 4, 5};
2
String nombres[] = {"Juan", "Pedro", "María", "Ana", "Luisa"};

Al inicializar los arreglos, sea con valores por defecto o con valores específicos, no podemos modificar la cantidad de elementos que pueden almacenar sin perder los datos que ya contiene. Es decir, los arreglos tienen un tamaño fijo, y no puede modificarse una vez inicializados.

Si bien podríamos reasignar el arreglo con una nueva cantidad de elementos, esto implicaría perder los datos que ya contiene. Esto se debe a que, al reasignar el arreglo, se crea un nuevo arreglo con la nueva cantidad de elementos, y la información del arreglo anterior se vuelve inaccesible.

1
int numeros[] = {1, 2, 3, 4, 5};
2
numeros = new int[10];

En el ejemplo anterior, hemos inicializado un arreglo de números enteros con 5 elementos, y luego lo hemos reasignado con 10 elementos. Aunque podría parecer que el arreglo ahora puede almacenar otros 5 elementos, además de los 5 que ya contenía, esto no es del todo cierto. En realidad, el arreglo ahora puede almacenar 10 elementos, pero los primeros 5 elementos que contenía ya no están disponibles, y ahora todos los elementos del arreglo tienen el valor por defecto del tipo de datos del arreglo (en este caso, 0).

Internamente, los arreglos almacenan sus elementos en posiciones de memoria contiguas, y cada elemento se almacena en una posición específica.

Estas posiciones se denominan índices, y se utilizan para acceder a los elementos del arreglo. Los índices de los arreglos comienzan en 0, y terminan en n-1, siendo n la cantidad de elementos que puede almacenar el arreglo. Por ejemplo, si tenemos un arreglo de 5 elementos, los índices de los elementos serán 0, 1, 2, 3 y 4.

Acceso a elementos

Como mencionamos anteriormente, los arreglos almacenan sus elementos en posiciones de memoria contiguas, y cada elemento se almacena en una posición específica.

Estas posiciones se denominan índices, y se utilizan para acceder a los elementos del arreglo, mediante la sintaxis arreglo[índice]. Es decir, podemos acceder a un elemento del arreglo indicando el índice del elemento que queremos acceder. Por ejemplo:

1
public class EjemploArrayIndices {
2
public static void main(String[] args) {
3
int numeros[] = {1, 2, 3, 4, 5};
4
5
System.out.println(numeros[0]); // Se muestra por consola el valor 1
6
System.out.println(numeros[1]); // Se muestra por consola el valor 2
7
System.out.println(numeros[2]); // Se muestra por consola el valor 3
8
System.out.println(numeros[3]); // Se muestra por consola el valor 4
9
System.out.println(numeros[4]); // Se muestra por consola el valor 5
10
}
11
}
Ventana de terminal
1
2
3
4
5

Si intentamos acceder a un elemento en una posición que no existe, se producirá un error en tiempo de ejecución indicando que el valor del índice esta fuera de rango.

1
public class EjemploArrayFueraDeRango {
2
public static void main(String[] args) {
3
int numeros[] = {1, 2, 3, 4, 5};
4
5
System.out.println(numeros[5]);
6
}
7
}
Ventana de terminal
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
at EjemploArrayOutOfBounds.main(main.java:5)

En ocasiones nos resultará útil emplear variables para acceder a los elementos de un arreglo en lugar de valores literales, como en el ejemplo anterior. Para ello, podemos emplear variables de tipo int para almacenar los índices, y luego utilizar estas variables para acceder a los elementos del arreglo.

1
public class EjemploArrayIndicesVariables {
2
public static void main(String[] args) {
3
int numeros[] = {1, 2, 3, 4, 5};
4
5
int indice = 0;
6
System.out.println(numeros[indice]); // Se muestra por consola el valor 1
7
8
indice = 1;
9
System.out.println(numeros[indice]); // Se muestra por consola el valor 2
10
11
indice = 2;
12
System.out.println(numeros[indice]); // Se muestra por consola el valor 3
13
14
indice = 3;
15
System.out.println(numeros[indice]); // Se muestra por consola el valor 4
16
17
indice = 4;
18
System.out.println(numeros[indice]); // Se muestra por consola el valor 5
19
}
20
}
Ventana de terminal
1
2
3
4
5

Luego veremos cómo emplear estructuras de control para recorrer arreglos de manera más eficiente.

Modificación de elementos

Los elementos de los arreglos pueden ser modificados mediante la sintaxis arreglo[índice] = valor. Es decir, podemos asignar un nuevo valor a un elemento del arreglo, indicando el índice del elemento que queremos modificar, y el nuevo valor que queremos asignarle.

1
public class EjemploArrayModificacion {
2
public static void main(String[] args) {
3
int numeros[] = {1, 2, 3, 4, 5};
4
5
numeros[2] = 10;
6
}
7
}

En este ejemplo, hemos modificado el valor del elemento en la posición 2 del arreglo numeros, asignándole el valor 10. Por lo tanto, el arreglo numeros ahora contiene los valores 1, 2, 10, 4 y 5, en ese orden.

Longitud

Como podemos notar, conocer la cantidad de elementos que puede almacenar un arreglo es muy importante, ya que nos permite saber cuáles son los índices válidos para acceder a sus elementos.

Si bien podemos conocer la cantidad de elementos que puede almacenar un arreglo al momento de inicializarlo, no es práctico tener que recordar esta cantidad cada vez que queramos acceder a sus elementos.

Una manera de solucionar esto sería almacenar la cantidad de elementos en una variable, y luego utilizar esta variable para acceder a los elementos del arreglo. Sin embargo, esto no es necesario, ya que Java nos provee una propiedad que nos permite conocer la cantidad de elementos que puede almacenar un arreglo. Esta propiedad se denomina length, y se encuentra ligada al arreglo al momento de inicializarlo.

Para emplear esta propiedad (o atributo), debemos indicar el nombre del arreglo, seguido de un punto (.), y luego el nombre de la propiedad. Por ejemplo:

1
public class EjemploArrayLongitud {
2
public static void main(String[] args) {
3
int numeros[] = {1, 2, 3, 4, 5};
4
5
System.out.println(numeros.length); // Se muestra por consola el valor 5
6
}
7
}

En este ejemplo, hemos mostrado por consola el valor de la propiedad length del arreglo numeros, que es 5.

Ventana de terminal
5

Con esto será más simple acceder a los elementos de un arreglo si necesitamos recorrerlo, ya que podemos emplear la propiedad length para conocer la cantidad de elementos que puede almacenar el arreglo, y luego emplear un ciclo for para recorrerlo.

1
public class EjemploArrayLongitud {
2
public static void main(String[] args) {
3
int numeros[] = {1, 2, 3, 4, 5};
4
5
for (int i = 0; i < numeros.length; i++) {
6
System.out.println(numeros[i]);
7
}
8
}
9
}

Al emplear la propiedad length del arreglo numeros como condición de nuestro ciclo for, nos aseguramos de que el ciclo se ejecute la cantidad de veces necesarias para recorrer todos los elementos del arreglo, sin importar cuántos elementos pueda almacenar. En este ejemplo, el ciclo se ejecutará 5 veces, ya que el arreglo numeros puede almacenar 5 elementos.

Ventana de terminal
1
2
3
4
5

Recorridos de un arreglo

Como mencionamos anteriormente, los arreglos almacenan sus elementos en posiciones de memoria contiguas, y cada elemento se almacena en una posición específica. Por lo tanto, podemos recorrer los elementos de un arreglo mediante un ciclo for, tal y como vimos en un ejemplo previo.

No obstante, no es la única manera de recorrer un arreglo. También podemos recorrerlo mediante un ciclo while, o mediante un ciclo do-while. El ejemplo que vemos a continuación es equivalente al ejemplo anterior, pero empleando un ciclo while en lugar de un ciclo for.

1
public class EjemploArrayRecorridos {
2
public static void main(String[] args) {
3
int numeros[] = {1, 2, 3, 4, 5};
4
5
int i = 0;
6
while (i < numeros.length) {
7
System.out.println(numeros[i]);
8
i++;
9
}
10
}
11
}

Este tipo de operación es tan común que Java nos provee una estructura de control específica para recorrer colecciones de datos, como los arreglos. Hablamos claro de la estructura de control for-each, que nos permite recorrer los elementos de un arreglo de manera más simple e intuitiva.

1
public class EjemploArrayRecorridos {
2
public static void main(String[] args) {
3
int numeros[] = {1, 2, 3, 4, 5};
4
5
for (int numero : numeros) {
6
System.out.println(numero);
7
}
8
}
9
}

Un ciclo for-each abstrae la lógica del recorrido convencional de un arreglo, simplificando la sintaixs de la estructura de control for que usamos hasta el momento.

En este caso, la variable numero representa cada uno de los elementos del arreglo numeros para cada iteración del ciclo. Es decir, en la primera iteración, la variable numero representa el primer elemento del arreglo numeros (el de posición 0), en la segunda iteración representa el segundo elemento del arreglo numeros (el de posición 1), y así sucesivamente hasta recorrer todos los elementos del arreglo.

Así mismo, la variable numero es de tipo int, ya que el arreglo numeros es de tipo int[]. Por lo tanto, podemos emplear la variable numero para acceder a los elementos del arreglo numeros de la misma manera que lo hacíamos con el ciclo for convencional.

Arreglos bidimensionales

Los arreglos que hemos visto hasta el momento son conocidos también como arreglos unidimensionales, ya que representan una secuencia de datos de un mismo tipo.

Sin embargo, Java permite declarar arreglos de más de una dimensión. En este tipo de arreglos cada elemento es a su vez un arreglo, y se denominan arreglos multidimensionales. Particularmente, nos centraremos en los arreglos bidimensionales, que son arreglos de dos dimensiones.

Este tipo de arreglos puede interpretarse como una tabla, donde el arreglo se divide en filas y columnas, y cada elemento se encuentra en una posición específica de la tabla.

Para declarar un arreglo de dos dimensiones basta con agregar un par de corchetes ([]) adicionales después del tipo de datos del arreglo. Por ejemplo:

1
<tipo>[][] <nombre>;
2
<tipo> <nombre>[][];

Para inicializar un arreglo bidimensional, debemos indicar la cantidad de filas y columnas que tendrá el arreglo. Al igual que un arreglo unidimensional, el tamaño del arreglo es fijo, y no puede modificarse una vez inicializado.

La sintaixs para inicializar un arreglo bidimensional es la siguiente:

1
<nombre> = new <tipo>[<filas>][<columnas>];

Y, una vez más, podemos inicializar los arreglos con valores específicos, para lo cual debemos indicarlos entre llaves, separados por comas. Por ejemplo:

1
int numeros[][] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};

En este ejemplo, hemos inicializado un arreglo bidimensional de 3 filas y 3 columnas, y hemos asignado valores específicos a cada uno de sus elementos. Gráficamente, podemos representar este arreglo de la siguiente manera:

Filas/Columnas012
0123
1456
2789

Como podemos ver, cada elemento del arreglo bidimensional es a su vez un arreglo unidimensional, y cada elemento de este arreglo unidimensional representa una columna de la tabla que venimos describiendo.

Manteniendo su parecido con los arreglo unidimensionales, los arreglos bidimensionales también pueden ser accedidos mediante índices. Esta vez necesitaremos dos índices, uno para la fila y otro para la columna. Por ejemplo:

1
public class EjemploArrayBidimensionalIndices {
2
public static void main(String[] args) {
3
int numeros[][] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
4
5
System.out.println(numeros[0][0]); // Se muestra por consola el valor 1
6
System.out.println(numeros[0][1]); // Se muestra por consola el valor 2
7
System.out.println(numeros[0][2]); // Se muestra por consola el valor 3
8
System.out.println(numeros[1][0]); // Se muestra por consola el valor 4
9
System.out.println(numeros[1][1]); // Se muestra por consola el valor 5
10
System.out.println(numeros[1][2]); // Se muestra por consola el valor 6
11
System.out.println(numeros[2][0]); // Se muestra por consola el valor 7
12
System.out.println(numeros[2][1]); // Se muestra por consola el valor 8
13
System.out.println(numeros[2][2]); // Se muestra por consola el valor 9
14
}
15
}

Claramente esta no es la manera más eficiente de acceder a los elementos de un arreglo bidimensional, ya que necesitamos indicar dos índices para acceder a cada elemento. Convencionalmente, los arreglos bidimensionales se recorren mediante dos ciclos for anidados, uno para recorrer las filas y otro para recorrer las columnas.

1
public class EjemploArrayBidimensionalRecorridos {
2
public static void main(String[] args) {
3
int numeros[][] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
4
5
for (int i = 0; i < numeros.length; i++) {
6
for (int j = 0; j < numeros[i].length; j++) {
7
System.out.println(numeros[i][j]);
8
}
9
}
10
}
11
}

Analizando más en detalle el ejemplo anterior, vemos que el código mostrará los elementos de arreglo bidimensional tomando como referencia la fila i-ésima y la columna j-ésima. Esto nos indica que el primer ciclo for recorre las filas del arreglo bidimensional, y el segundo ciclo for recorre las columnas del arreglo bidimensional.

En general, veremos la necesidad de emplear dos ciclos anidados (ya sean for, while o do-while) para recorrer arreglos bidimensionales, independientemente de la operatoria que realicemos con ellos. Incluso, en ocasiones necesitaremos emplear más de dos ciclos anidados para recorrer arreglos de más de dos dimensiones.

Por supuesto, podemos emplear la estructura de control for-each para volver más legible el recorrido de esta compleja estructura de datos.

1
public class EjemploArrayBidimensionalForEach{
2
public static void main(String[] args) {
3
int numeros[][] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
4
5
for (int[] fila : numeros) {
6
for (int numero : fila) {
7
System.out.println(numero);
8
}
9
}
10
}
11
}

En este ejemplo, la variable fila es un arreglo unidimensional que representa cada una de las filas del arreglo bidimensional numeros, y la variable numero representa cada uno de los elementos de la fila para cada iteración del ciclo.

Debemos mencionar que emplear arreglos unidimensionales es costoso en términos de memoria. Después de todo, reservamos espacio en memoria para almacenar una cantidad de elementos que no conocemos, y que puede ser muy grande. Además, por cada dimensión que agregamos a un arreglo, la cantidad de memoria que necesitamos reservar se multiplica por la cantidad de elementos que puede almacenar cada dimensión.

Por lo tanto, debemos tener cuidado al emplear arreglos, y solo hacerlo cuando sea necesario.

En este curso no abordaremos problemas que requieran emplear estructuras de más de dos dimensiones, además veremos alternativas a este tipo de estructuras de datos.