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:
1int numeros[];2String 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:
1int numeros[] = new int[10];2String 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:
1int numeros[] = {1, 2, 3, 4, 5};2String 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.
1int numeros[] = {1, 2, 3, 4, 5};2numeros = 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:
1public 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 16 System.out.println(numeros[1]); // Se muestra por consola el valor 27 System.out.println(numeros[2]); // Se muestra por consola el valor 38 System.out.println(numeros[3]); // Se muestra por consola el valor 49 System.out.println(numeros[4]); // Se muestra por consola el valor 510 }11}
12345
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.
1public 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}
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.
1public 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 17
8 indice = 1;9 System.out.println(numeros[indice]); // Se muestra por consola el valor 210
11 indice = 2;12 System.out.println(numeros[indice]); // Se muestra por consola el valor 313
14 indice = 3;15 System.out.println(numeros[indice]); // Se muestra por consola el valor 416
17 indice = 4;18 System.out.println(numeros[indice]); // Se muestra por consola el valor 519 }20}
12345
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.
1public 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:
1public 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 56 }7}
En este ejemplo, hemos mostrado por consola el valor de la propiedad length
del arreglo numeros
, que es 5
.
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.
1public 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.
12345
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
.
1public 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.
1public 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:
1int 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/Columnas | 0 | 1 | 2 |
---|---|---|---|
0 | 1 | 2 | 3 |
1 | 4 | 5 | 6 |
2 | 7 | 8 | 9 |
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:
1public 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 16 System.out.println(numeros[0][1]); // Se muestra por consola el valor 27 System.out.println(numeros[0][2]); // Se muestra por consola el valor 38 System.out.println(numeros[1][0]); // Se muestra por consola el valor 49 System.out.println(numeros[1][1]); // Se muestra por consola el valor 510 System.out.println(numeros[1][2]); // Se muestra por consola el valor 611 System.out.println(numeros[2][0]); // Se muestra por consola el valor 712 System.out.println(numeros[2][1]); // Se muestra por consola el valor 813 System.out.println(numeros[2][2]); // Se muestra por consola el valor 914 }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.
1public 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.
1public 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.