1 Introducción a las colecciones¶
Introducción¶
Una colección representa un grupo de objetos. Estos objetos son conocidos como elementos. Cuando queremos trabajar con un conjunto de elementos, necesitamos un almacén donde poder guardarlos.
Las colecciones son estructuras de datos con la peculiaridad de que son estructuras dinámicas. Eso quiere decir que pueden aumentar o disminuir su tamaño dependiendo de los elementos que almacenan, lo que suponen una mejora respecto a las estructuras de datos estáticas cuyo tamaño se define en su creación y no se puede alterar en tiempo de ejecución, como por ejemplo, los arrays.
El API de Java nos proporciona en el paquete java.util
el framework de las colecciones, que nos permite utilizar diferentes estructuras de datos para almacenar y recuperar objetos de cualquier clase. Java tiene desde la versión 2 todo un juego de clases e interfaces para guardar colecciones de objetos donde todas las entidades conceptuales están representadas por interfaces y las clases se usan para proveer implementaciones de esas interfaces. Estas clases e interfaces están estructuradas en una jerarquía.
Pero ¿qué podemos almacenar dentro de una colección? Podemos almacenar cualquier objeto que herede de la clase Object
. Pero esto presenta ciertos inconvenientes:
- Podríamos tener una colección con objetos completamente distintos, lo que puede dar lugar a problemas ya que en todo momento deberíamos saber qué tipo de objeto y qué posición de la colección se encuentra el elementos con el que queremos trabajar, de otro modo podríamos tener incongruencias en el código e incluso hacer saltar una excepción.
- Otro inconveniente es que tendríamos que hacer continuos castings para poder trabajar con los elementos de la colección, lo cual resulta tedioso y poco productivo.
Veamos un ejemplo para obtener la suma de los valores almacenados en una lista:
int total = 0;
ArrayList numbers = new ArrayList(); // Creación de la lista
numbers.add(1); // Se añade el elemento 1 a la lista utilizando el método add
numbers.add(2);
numbers.add(3);
for(int i = 0; i < numbers.size(); i++){
// No vemos obligados a hacer cast, dado que numbers.get(i) retorna un Object:
total += (int) numbers.get(i);
}
System.out.printf("Total: %d\n", total);
Como vemos en el ejemplo anterior, nos vemos obligados a hacer explícitamente un cast cuando obtenemos un elemento de la lista, dado que la lista internamente trabaja con elementos de la clase Object
. No hay ningún contrato que permita a la clase ArrayList
saber qué tipo de datos queremos que trabaje.
Además, es posible añadir elementos de distinto tipo a la lista, con el agravante de que más adelante cuando se intenta acceder al elemento y se hace cast sobre él se producirá un error en tiempo de ejecución. Así, si modificamos el ejemplo anterior de la siguiente manera:
int total = 0;
ArrayList numbers = new ArrayList();
numbers.add(1);
numbers.add(2);
numbers.add("Antonio");
for(int i = 0; i < numbers.size(); i++){
// Esta línea lanza una excepción cuando
// se trata de convertir a entero el elemento "Antonio",
total += (int) numbers.get(i);
}
System.out.printf("Total: %d\n", total);
Así pues, para resolver este problema, a partir de la versión 5 de Java empezaron a utilizar los genéricos. Los genéricos nos permiten establecer un tipo con el que vamos a trabajar en esa colección, de esa manera podemos evitar los problemas mencionados anteriormente.
Así el ejemplo anterior podríamos modificarlo de la siguiente manera:
int total = 0;
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
//Esta línea da un error de compilación, dado que el compilador
// detecta que estamos intentado añadir una cadena a la lista de enteros:
numbers.add("Antonio");
for(int i = 0; i < numbers.size(); i++){
// Ya no es necesario hacer un cast explícito, dado que el compilador
// lo hará internamente por nosotros, al haberle informado de que queríamos
// trabajar con una lista de enteros
total += numbers.get(i);
}
System.out.printf("Total: %d\n", total);
Como vemos en el ejemplo anterior, gracias a la información que le suministramos al compilador sobre el tipo de lista con el que queremos trabajar, en este caso Integer
, el compilador es capaz de detectar en tiempo de compilación que no debería ser posible añadir una cadena a la lista, y además nos evita tener que hacer explícitamente el cast a entero cuando obtenemos los elementos de la lista, porque ya lo puede hacer él internamente por nosotros.
Lo que se use en un genérico debe ser un objeto, por lo tanto, los genéricos no funcionan con datos primitivos. Para resolver esta situación, la API de Java incorpora las clases envoltorio (wrapper class) que no son más que dotar a los datos primitivos con un envoltorio que permita tratarlos como objetos. Las clases envoltorios proporcionan métodos de utilidad para la manipulación de datos primitivos (conversiones de / hacia datos primitivos, conversiones a String, etc).