Skip to content

UT 7. Colecciones

portada

Cuando el volumen de datos a manejar por una aplicación es elevado, no basta con utilizar variables.

Manejar los datos de, por ejemplo, un único pedido en una aplicación puede ser relativamente sencillo, pues un pedido está compuesto por una serie de datos y eso simplemente se traduce en varias variables. Pero, ¿qué ocurre cuando en una aplicación tenemos que gestionar varios pedidos a la vez? Lo mismo ocurre en otros casos.

Para poder realizar ciertas aplicaciones se necesita poder manejar datos que van más allá de meros datos simples (números y letras). A veces, los datos que tiene que manejar la aplicación son datos compuestos, es decir, datos que están compuestos a su vez de varios datos más simples. Por ejemplo, un pedido está compuesto por varios datos, los datos podrían ser el cliente que hace el pedido, la dirección de entrega, la fecha requerida de entrega y los artículos del pedido.

Los datos compuestos son un tipo de estructura de datos, y en realidad ya los has manejado. Las clases son un ejemplo de estructuras de datos que permiten almacenar datos compuestos, y el objeto en sí, la instancia de una clase, sería el dato compuesto. Pero, a veces, los datos tienen estructuras aún más complejas, y son necesarias soluciones adicionales.

Esas soluciones consisten básicamente en la capacidad de poder manejar varios datos del mismo o diferente tipo de forma dinámica y flexible.

1. Estructuras de almacenamiento

¿Cómo almacenarías en memoria un listado de números del que tienes que extraer el valor máximo?

Seguro que te resultaría fácil; pero, ¿y si el listado de números no tiene un tamaño fijo, sino que puede variar en tamaño de forma dinámica? Entonces la cosa se complica.

Un listado de números que aumenta o decrece en tamaño es una de las cosas que aprenderás a utilizar aquí, utilizando estructuras de datos.

Pasaremos por alto las clases y los objetos, pues ya los has visto con anterioridad, pero debes saber que las clases en sí mismas son la evolución de un tipo de estructuras de datos conocidas como datos compuestos (también llamadas registros). Las clases, además de aportar la ventaja de agrupar datos relacionados entre sí en una misma estructura (característica aportada por los datos compuestos), permiten agregar métodos que manejen dichos datos, ofreciendo una herramienta de programación sin igual. Pero todo esto ya lo sabías.

Las estructuras de almacenamiento, en general, se pueden clasificar de varias formas, atendiendo a:

Si pueden almacenar datos de diferente tipo o no
con capacidad de almacenar varios datos del mismo tipo: varios números, varios caracteres, etc. los arrays, las listas, los conjuntos, las cadenas de caracteres
con capacidad de almacenar varios datos de distinto tipo: números, fechas, cadenas de caracteres, etc. las clases
En función de si pueden o no cambiar de tamaño de forma dinámica
cuyo tamaño se establece en el momento de la creación o definición y su tamaño no puede variar después. los arrays, las matrices (arrays multidimensionales)
cuyo tamaño es variable (conocidas como estructuras dinámicas). Su tamaño crece o decrece según las necesidades de forma dinámica. las listas, árboles, conjuntos y el caso de algunos tipos de cadenas de caracteres.
Atendiendo a la forma en la que los datos se ordenan dentro de la estructura
que no se ordenan de por sí, y debe ser el programador el encargado de ordenar los datos si fuera necesario los arrays
ordenadas: al incorporar un dato nuevo a todos los datos existentes, este se almacena en una posición concreta que irá en función del orden. El orden establecido en la estructura puede variar dependiendo de las necesidades del programa: alfabético, orden numérico de mayor a menor, momento de inserción, etc. ArrayList, TreeSet

Todavía no conoces mucho de las estructuras, y probablemente todo te suena raro y extraño. No te preocupes, poco a poco irás descubriéndolas. Verás que son sencillas de utilizar y muy cómodas.

2. Clases y métodos genéricos

2.1. Introducción a los Genéricos

¿Crees que el código es más legible al utilizar genéricos o que se complica? Al principio puede parecer difícil, pero con el tiempo notarás que el código se vuelve más claro y seguro en comparación con el uso de conversiones de tipo.

Los genéricos permiten definir clases y métodos que pueden operar con diferentes tipos de datos sin comprometer la seguridad de tipos. Esto evita la necesidad de conversiones explícitas y errores en tiempo de ejecución.

2.2. Clases Genéricas

Las clases genéricas funcionan de manera similar a los métodos genéricos, pero a nivel de clase. Permiten definir un parámetro de tipo que se usará a lo largo de toda la clase, facilitando la reutilización del código. Para definir una clase genérica, se indica un parámetro de tipo entre < > junto al nombre de la clase:

Ejemplo 1.01: clase genérica
public class Util<T> {
    T temp;


    public void invertir (T[] array) {
       for (int i = 0; i < array.length / 2; i++) {
          temp = array[i];
          array[i] = array[array.length - i - 1];
          array[array.length - i - 1] = temp;
       }
    }
}

En este ejemplo, la clase Util define un método invertir que intercambia los elementos de un array de cualquier tipo. Para utilizar esta clase genérica, se debe instanciar especificando el tipo concreto:

1
2
3
4
5
6
7
8
Integer[] numeros = {0,1,2,3,4,5,6,7,8,9}; //el array clase wrapper

Util<Integer> u = new Util<Integer>();

u.invertir(numeros);
for (int i=0; i<numeros.length; i++){
   System.out.println(numeros[i]);
}

Como puedes observar, el uso de genéricos es sencillo, tanto a nivel de clase como a nivel de método.

Simplemente, a la hora de crear una instancia de una clase genérica, hay que especificar el tipo, tanto en la definición (Util<Integer> u) como en la creación (new Util<Integer>()).

Los genéricos los vamos a usar ampliamente a partir de ahora, aplicados a un montón de clases genéricas que tiene Java y que son de gran utilidad, por lo que es conveniente que aprendas bien a usar una clase genérica.

Restricciones en los parámetros genéricos

  • Los parámetros de tipo genérico solo pueden ser clases.
  • No pueden ser tipos primitivos (int, double, char, etc.).
  • Se deben usar sus clases envoltorio (Integer, Double, Character, etc.).

Todavía hay un montón de cosas más sobre los métodos y las clases genéricas que deberías saber. A continuación se muestran algunos usos interesantes de los genéricos.

2.3. Usos Avanzados de Genéricos

2.3.1. Múltiples Parámetros Genéricos

Se pueden definir métodos o clases con múltiples parámetros de tipo:

1
2
3
4
5
public class Util<T, M> {
    public static <T, M> int sumaDeLongitudes(T[] a, M[] b) {
        return a.length + b.length;
    }
}
- En el ejemplo anterior se suman las longitudes de dos arrays que no tienen que ser del mismo tipo. - Usar un método o una clase con dos o más parámetros genéricos es sencillo, a la hora de invocar al método o crear la clase, se indican los tipos base separados por coma.

Ejemplo 1.02
1
2
3
4
5
6
Integer[] a1 = {0, 1, 2, 3, 4};
Double[]  a2 = {0d, 1d, 2d, 3d, 4d};

int resultado = Util.<Integer,Double>sumaDeLongitudes(a1, a2);

System.out.println(resultado);

2.3.2. Clases con Múltiples Parámetros Genéricos

También es posible definir clases que manejen múltiples tipos:

public class Terna<A, B, C> {
    A a;
    B b;
    C c;

    public Terna(A a, B b, C c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

    public A getA() { return a; }
    public B getB() { return b; }
    public C getC() { return c; }
}

2.3.3. Métodos Genéricos dentro de Clases Genéricas

Una clase genérica puede contener métodos con parámetros de tipo adicionales:

class Util<A> {
    A a;

    Util(A a) {
        this.a = a;
    }

    public <B> void salida(B b) {
        System.out.println(a.toString() + " " + b.toString());
    }
}

2.3.4. Inferencia de Tipos (Desde Java 7)

No siempre es necesario indicar los tipos al instanciar una clase genérica, ya que el compilador puede inferirlos:

Terna<Integer, Double, Float> t = new Terna<>(0, 1.3d, 1.4f);

2.3.5. Restricción de Tipos con extends

Podemos limitar los tipos permitidos en un genérico con extends, lo que permite trabajar solo con clases que extiendan una determinada jerarquía:

1
2
3
4
5
public class Util {
    public static <T extends Number> Double sumar(T t1, T t2) {
        return t1.doubleValue() + t2.doubleValue();
    }
}

Solo se permitirán clases derivadas de Number (como Integer, Double, Float), lo que garantiza que el método sumar pueda operar matemáticamente sobre los valores.

2.3.6. Wildcards (?): Parámetros de Tipo Desconocido

A veces no importa el tipo exacto, solo que sea un tipo válido. En esos casos, se usa ?:

1
2
3
4
5
6
7
public class Ejemplo<A> {
    A a;
}

void test(Ejemplo<?> e) {  // Permite cualquier tipo
    ...
}

Si queremos restringir los tipos permitidos, podemos usar ? extends:

1
2
3
void test(Ejemplo<? extends Number> e) {  // Solo clases que extienden de Number
    ...
}

2.3.7. Conclusión

El uso de clases y métodos genéricos mejora la reutilización del código y evita errores de tipo en tiempo de ejecución. Java ofrece una gran flexibilidad con genéricos, permitiendo:

  • Definir clases y métodos que trabajen con cualquier tipo de dato.
  • Limitar los tipos permitidos con extends.
  • Usar inferencia de tipos para reducir código repetitivo.
  • Manejar clases genéricas con parámetros desconocidos mediante wildcards (?).

Un poquito de ...