Programación Avanzada

Programación Orientada a Objetos

Genéricos

Introducción

¿Recuerdas estructuras de datos tales como las listas, las colas o los árboles?

Estas estructuras de datos pueden trabajar con cualquier tipo de datos, podemos tener listas de Persona y también de String o File, la funcionalidad que la lista ofrece es independiente del tipo de datos que maneja.

En POO este comportamiento (independencia del tipo de datos) lo podemos conseguir utilizando genéricos.

Bibliografía

Contenidos

  1. Métodos genéricos.
  2. Clases genéricas.
  3. Interfaces genéricas.
  4. Límites.
  5. Comodines.
  6. Ejemplos en el paquete estándar.
  7. Resumen.

Métodos genéricos

Imagina que necesitas un método que te imprima información sobre una Persona.

public void muestra(Persona persona) {
    System.out.println(persona.toString() +
        " de tipo " + persona.getClass().getName());
}

Y ahora quieres hacer lo mismo para Empresa.

public void muestra(Empresa empresa) {
    System.out.println(empresa.toString() +
        " de tipo " + empresa.getClass().getName());
}

Hmmmm, ya nos estamos repitiendo.

Métodos genéricos

De acuerdo, este caso ya lo sabemos resolver, basta utilizar la clase madre de ambas. Polimorfismo al rescate:

public void muestra(Cliente cliente) {
    System.out.println(cliente.toString() +
        " de tipo " + cliente.getClass().getName());
}

Pero, ¿y si no existe una relación entre las clases?

public String muestra(Float numero) {
    return numero + " de tipo " + numero.getClass().getName();
}

public String muestra(Integer numero) {
    return numero + " de tipo " + numero.getClass().getName();
}

Métodos genéricos

Sería de gran ayuda si el tipo del argumento estuviese indeterminado en el momento de definir el método, y lo concretásemos en el momento de utilizar el método.

Esto es precisamente lo que nos permiten los genéricos, en el caso de los métodos, nos permiten dejar sin especificar el tipo del argumento.

Métodos genéricos

Cuando definimos un método podemos dejar sin especificar el tipo de alguno de los argumentos:

public class ClaseConMetodoGenerico {
    public static < T > void muestra(T dato) {
        System.out.println("El dato es:" + dato.toString() +
            " de tipo " + dato.getClass().getName());
    }
}

Y lo podemos usar así:

ClaseConMetodoGenerico.muestra(1);
ClaseConMetodoGenerico.muestra("Hola");
El dato es:1 de tipo java.lang.Integer
El dato es:Hola de tipo java.lang.String

Métodos genéricos

Lo que está ocurriendo es que el compilador de Java crea un método con el tipo más general, que en este caso es Object, y utiliza casting y métodos puente (bridge) cuando es necesario.

Esto se hace así para preservar la compatibilidad hacia atrás. La programación con genéricos fue introducida en la versión 5 de Java, de la mano de Martin Odersky.

A esta técnica se le llama borrado de tipos.

Métodos genéricos

Un método genérico puede tener más de un tipo de sus argumentos sin especificar:

public class MetodoGenerico {
    public static < T, U > boolean tiposIguales(T primero, U segundo) {
        return primero.getClass().equals(segundo.getClass());
    }
}

Lo podemos usar así:

System.out.println(MetodoGenerico.tiposIguales("Hola", "Adios"));
System.out.println(MetodoGenerico.tiposIguales("Hola", 1));
true
false

Métodos genéricos

¿Cual es la ventaja?

Seguir la buena práctica de programación DRY: Don't Repeat Yourself, no te repitas.

En otras asignaturas del grado, has visto algoritmos cuyo funcionamiento es independiente del tipo de datos sobre el que trabajan, por ejemplo, los algoritmos de ordenación. Para que estos funcionen, basta conocer, según algún criterio, si un elemento es mayor que otro.

Veremos un ejemplo al final de este capítulo.

Clases genéricas

Los genéricos también los podemos utilizar con clases.

Las clases genéricas son muy útiles cuando necesitamos que manejen cualquier tipo de datos. Un ejemplo son las clases que implementan estructuras de datos: Listas, Colas, Árboles, Grafos, etcétera.

Veamos como primer ejemplo una sencilla clase que no es más que un contenedor de datos genérico.

Clases genéricas

public class ContenedorGenerico< T > {
    private T dato;

    public ContenedorGenerico() {
        super();
    }
    public ContenedorGenerico(T dato) {
        super();
        this.dato = dato;
    }
    public T getDato() {
        return dato;
    }
    public void setDato(T dato) {
        this.dato = dato;
    }
}

Clases genéricas

Podemos instanciar la clase genérica así:

ContenedorGenerico< Integer > entero =
    new ContenedorGenerico< Integer >(1);
ContenedorGenerico< Float > real =
    new ContenedorGenerico< Float >(1.1f);
ContenedorGenerico< Persona > persona =
    new ContenedorGenerico< Persona >(new Persona(...));

Especificamos el tipo concreto cuando declaramos las referencias o cuando creamos instancias.

Un detalle muy importante, el tipo de entero es ContenedorGenerico< Integer >, no sólo ContenedorGenerico.

Vayamos un poco más lejos y creemos un contenedor enlazado.

Clases genéricas

public class ContenedorEnlazadoGenerico< T > {
    private ContenedorGenerico< T > dato;
    private ContenedorEnlazadoGenerico< T > siguiente;

    public ContenedorEnlazadoGenerico(T dato) {
        super();
        this.dato = new ContenedorGenerico< T >(dato);
    }
    public T getDato() {
        return dato.getDato();
    }
    public ContenedorEnlazadoGenerico< T > getSiguiente() {
        return siguiente;
    }
    public void setSiguiente(ContenedorEnlazadoGenerico< T >
        siguiente) {
        this.siguiente = siguiente;
    }
}

Clases genéricas

Ahora ya podemos crear una lista:

public class ListaGenerica< T > {
    private ContenedorEnlazadoGenerico< T > cabeza;
    private ContenedorEnlazadoGenerico< T > cola;
    private int cuantos;

    public ListaGenerica() {
        super();
        cabeza = cola = null;
        cuantos = 0;
    }
    public void addElemento(T dato) {
        ...sigue la implementación

Clases genéricas

Y podríamos trabajar con ella de esta manera:

ListaGenerica< Integer > enteros = new ListaGenerica< Integer >();
enteros.addElemento(1);
enteros.addElemento(2);
ListaGenerica< Float > reales = new ListaGenerica< Float >();
reales.addElement(1.1f);
reales.addElement(2.2f);

Además, las clases genéricas detectan incompatibilidad de tipos en tiempo de compilación.

ListaGenerica< Integer > enteros = new ListaGenerica< Integer >();
enteros.addElement(1.1f);

Dará un error, estamos intentando añadir un Float a un lista de Integer.

Interfaces genéricas

Las interfaces también pueden hacer uso de genéricos:

public interface DevuelveDatoGenerico< T > {
    T getDato();
    void setDato(T dato);
}
public class ContenedorGenerico< T >
    implements DevuelveDatoGenerico< T >{
    ....
    @Override
    public T getDato() {
        return dato;
    }
    @Override
    public void setDato(T dato) {
        this.dato = dato;
    }
}

Interfaces genéricas

Ahora podemos estar tentados de hacer lo siguiente:

DevuelveDatoGenerico< Float > dato =
    new ContenedorGenerico< Float >(2.2f);
dato = new ContenedorGenerico< Integer >(1);

Lo que nos da un error en tiempo de compilación.

¿Cómo es posible si DevuelveDatoGenerico< T > trabaja con genéricos?

Es cierto, pero el tipo de la referencia lo forma tanto el nombre del genérico como el tipo particular.

DevuelveDatoGenerico< T > no es un tipo.

DevuelveDatoGenerico< Integer > sí que lo es.

Límites

Supón que estas desarrollando una aplicación para una cooperativa agrícola. La cooperativa vende, entre otras frutas, mandarinas y naranjas.

Necesitan poder calcular los porcentajes de cada tipo de fruta según algún parámetro, por ejemplo:

  • Porcentaje de mandarinas con un calibre mayor que cierto calibre dado.
  • Porcentaje de naranjas con un calibre mayor que cierto calibre dado.
  • Porcentaje de naranjas de una cierta variedad.
  • Porcentaje de mandarinas con un índice de azúcar superior a un índice dado.

Límites

Cuando te pones a codificar, rápidamente te das cuenta que tanto las mandarinas como las naranjas tienen calibre (y en general cualquier fruta), así que decides crear una superclase Fruta:

public abstract class Fruta {
    private float tamanyo;

    public Fruta(float tamanyo) {
        this.tamanyo = tamanyo;
    }

    float getTamanyo() {
        return tamanyo;
    }
}

Límites

Las mandarinas quedan como:

public class Mandarina extends Fruta {
    private float indiceAzucar;

    public Mandarina(float tamanyo, float indiceAzucar) {
        super(tamanyo);
        this.indiceAzucar = indiceAzucar;
    }

    public float getIndiceAzucar() {
        return indiceAzucar;
    }
}

Límites

Y por su parte, las naranjas quedan como:

public class Naranja extends Fruta {
    private String variedad;

    public Naranja(float tamanyo, String variedad) {
        super(tamanyo, variedad);
        this.variedad = variedad;
    }

    public String getVariedad() {
        return variedad;
    }
}

Límites

Hasta aquí sencillo. Ahora quieres una clase que realice los cálculos de porcentaje para las mandarinas:

public class AnalizadorMandarinas {
    private Collection< Mandarina > mandarinas = new ArrayList< >();

    public float porcentajeIndiceAzucar(float indiceAzucar) {
        int contador = 0;
        for(Mandarina mandarina: mandarinas) {
            if(mandarina.getIndiceAzucar() >= indiceAzucar)
                contador++;
        }
        return (float)contador/mandarinas.size();
    }
}

Fíjate que necesitamos que la colección sea de Mandarina porque necesitamos consultar su índice de azúcar.

Límites

Para el analizador de Naranja la idea es muy parecida:

public class AnalizadorNaranjas {
    private Collection< Naranja > naranjas = new ArrayList< >();

    public float porcentajeVariedad(String variedad) {
        int contador = 0;
        for(Naranja naranja: naranjas) {
            if(variedad.equals(naranja.getVariedad()))
                contador++;
        }
        return (float)contador/naranjas.size();
    }
}

De nuevo, la colección es de Naranja.

Límites

Empieza lo bueno, como el calibre está definido en la clase Fruta, se nos ocurre crear otra clase de utilidad para hacer el cálculo de porcentaje según calibre:

public class UtilidadesFruta {
    public static Collection< Fruta > procesaCalibre(Collection< Fruta > frutas,
                                                     float calibre) {
        Collection< Fruta > validas = new ArrayList< >();
        for(Fruta fruta: frutas) {
            if(fruta.getTamanyo() >= calibre)
                validas.add(fruta);
        }
        return validas;
    }
}

Ningún problema, la definición del método es correcta.

Límites

Y queremos usarla así:

public class AnalizadorMandarinas {
    private Collection< Mandarina > mandarinas = new ArrayList< >();

    ....
    public float porcentajeCalibre(float calibre) {
        int contador = UtilidadesFruta.procesaCalibre(mandarinas, calibre).size();
        return (float)contador/mandarinas.size();
    }
}

No podemos, el método porcentajeCalibre espera un Collection< Fruta > pero ya sabemos que un Collection< Mandarina > no lo es. ¿Qué hacemos?

Límites

Solución:

public class AnalizadorMandarinas {
    .......
    public float porcentajeCalibre(float calibre) {
        Collection< Fruta > frutas = new  ArrayList< >();
        for(Mandarina mandarina: mandarinas)
            frutas.add(mandarina);
        int contador = UtilidadesFruta.procesaCalibre(frutas, calibre).size();
        return (float)contador/mandarinas.size();
    }
}

Claro, y también puedes programarlo en Cobol ;)

El problema está en UtilidadesFrutas que sólo sabe trabajar con Collection< Fruta >.

Límites

Intentemos utilizar genéricos:

public static < T > Collection< T > procesaCalibre(Collection< T > frutas,
                                                   float calibre) {
    Collection< T > validas = new ArrayList< >();
    for(T fruta: frutas) {
        if(fruta.getTamanyo() >= calibre)
            validas.add(fruta);
    }
    return validas;
}

El problema es que < T > es tan genérico que no podemos garantizar que esos tipos tengan definido el método getTamanyo().

Pongamos límites a esta situación.

Límites

Veamos la solución limpia utilizando límites:


public static < T extends Fruta> Collection< T > procesaCalibre(
        Collection< T > frutas, float calibre) {
    Collection< T > validas = new ArrayList< >();
    for(T fruta: frutas) {
        if(fruta.getTamanyo() >= calibre)
            validas.add(fruta);
    }
    return validas;
}

Este es el camino, estamos fijando un límite superior, queremos que el método sea genérico y a su vez que extienda a la clase Fruta.

Comodines

¿Y existe un mecanismo para poder definir referencias sin tipo genérico concreto? Afortunadamente sí, los Comodines.

DevuelveDatoGenerico< Integer > entero =
    new ContenedorGenerico< Integer >(1);
DevuelveDatoGenerico< ? >comodin =
    new ContenedorGenerico< Float >(2.0f);

Fantástico!!!... bueno, no tan rápido. Hay cosas que con los comodines no podemos hacer:

comodin.getDato(); // No hay problema.
comodin.setDato(3); // Tenemos un error.

No podemos utilizar las referencias comodín para modificar el estado de un objeto.

Comodines

Supón ahora que tenemos la siguiente jerarquía de clases:

Y que la clase Perro es abstract

public abstract class Perro extends Animal {
    public abstract String ladra();
}

Comodines

Y las dos implementaciones de la clase Perro:

public class Caniche extends Perro {
    @Override
    public String ladra() {
        return "Ladro como un caniche";
    }
}
public class Chiuaua extends Perro {
    @Override
    public String ladra() {
        return "Ladro como un chiuaua";
    }
}

Comodines

Ahora queremos una referencia de tipo DevuelveDatoGenerico que pueda referenciar a cualquier tipo de Perro.

Con lo que hasta ahora sabemos se nos ocurre:

DevuelveDatoGenerico< ? > perro =
    new ContenedorGenerico< Perro >(new Caniche());
perro = new ContenedorGenerico< Perro >(new Chiuaua());

Hasta aquí todo bien, pero ahora se nos ocurre probar:

perro = new ContenedorGenerico< Gato >(new Gato());

Vaya!, no hay problema, pero nosotros sólo queremos Perro

Comodines

¿Cómo limitamos el tipo de las referencias cuando usamos comodines?

Con los límites. Veamos primero los límites superiores:

Si queremos que nuestra referencia sea de cualquier tipo de Perro, es como decir que Perro es el límite superior del comodín:

DevuelveDatoGenerico< ? extends Perro > perro =
    new ContenedorGenerico< Caniche >(new Caniche());
perro = new ContenedorGenerico< Chiuaua >(new Chiuaua());

Sin problema, Caniche y Chiuaua extienden a Perro

Comodines

Intentémoslo de nuevo con el Gato:

perro = new ContenedorGenerico< Gato >(new Gato());

Obtenemos:

Type mismatch: cannot convert from ContenedorGenerico< Gato > to DevuelveDatoGenerico< ? extends Perro >

Bien!, porque Gato no es hija de Perro.

Los límites superiores marcan la clase superior que se puede utilizar con un tipo genérico.

Comodines

Si el límite lo fija un interface no una clase, la sintaxis sigue utilizando extends, no implements:

public interface UnInterface {
...
}
ArrayList< ? extends UnInterface > al = .... // Bien
ArrayList< ? implements UnIterface > al = ... // Mal!!!

Comodines

También podemos usar comodines con un límite inferior:

ArrayList< ? super Caniche > perros = new ArrayList< Perro >();

En este caso, estamos indicando que perros es una referencia a la que podemos asignar cualquier ArrayList cuyos elementos tengan como hija a Caniche, como por ejemplo Perro, tal y como aparece en el ejemplo.

Comodines

Imagina que tenemos la siguiente clase hija de Caniche

public class CanicheSudafricano extends Caniche {
    @Override
    public String ladra() {
        return "Ladro como un caniche... sudafricano.";
    }
}

En este caso NO podríamos hacer:


List< ? super Caniche > perros = new ArrayList< CanicheSudafricano >(); // Error

porque CanicheSudafricano no es madre de Caniche, sino una de sus hijas.

Ejemplos de genéricos en el paquete estándar

En el paquete estándar de Java tienes numerosos ejemplos de uso de genéricos. Veamos algunos ejemplos.

Colecciones. La clase ArrayList proporciona una colección a la que yo puedo añadir, recuperar, borrar, etcétera elementos:

ArrayList< Integer > enteros = new ArrayList< Integer >();
enteros.add(1);
System.out.println(enteros.size()); // Muestra 1
enteros.add(1.1); // Error, sólo Integer

Ejemplos de genéricos en el paquete estándar

Fíjate, además en la sintaxis:

ArrayList< Integer > enteros = new ArrayList< Integer >();
enteros.add(1);

Estamos añadiendo un tipo primitivo (int) a una lista que maneja referencias Integer. ¿Qué está ocurriendo?

La JVM crea un objeto de tipo Integer conteniendo un int para poder añadirlo a la lista. Esto se conoce como boxing.

Ejemplos de genéricos en el paquete estándar

ArrayList< Integer > enteros = new ArrayList< Integer >();
enteros.add(1);
System.out.println(enteros.get(0) + 1)

Fíjate que ahora ocurre al contrario, el método get(0) no devuelve una referencia de tipo Integer que parece que sumamos al entero 1. Lo que está ocurriendo es que la JVM obtiene el entero contenido en el objeto Integer, de nuevo de manera transparente para nosotros. A este mecanismo se le llama: unboxing.

A los dos mecanismos conjuntos se les llama autoboxing, y existe en Java desde que existe los genéricos.

Ejemplos de genéricos en el paquete estándar

El interface Comparable< T > lo puede implementar una clase cuyas instancias podamos compara entre sí para decidir cual de ellas es mayor.

La clase java.awt.Color no implementa Comparable, creemos una clase que sí lo implemente:

public class ColorComparable extends Color implements Comparable< Color > {
    public ColorComparable(int r, int g, int b) {
        super(r, g, b);
    }
    @Override
    public int compareTo(Color otroColor) {
        if(getRGB() < otroColor.getRGB()) return -1;
        else if(getRGB() > otroColor.getRGB()) return 1;
        else return 0;
    }
}

Ejemplos en el paquete estándar

Ahora podemos ordenar un array de colores comparables:

ColorComparable colores[] = {new ColorComparable(0, 0, 0),
    new ColorComparable(1, 1, 1),
    new ColorComparable(1, 0, 1)};

Arrays.sort(colores);

System.out.println(Arrays.asList(colores));

Obtenemos:

[[0, 0, 0], [1, 0, 1], [1, 1, 1]]

Resumen

La programación con genéricos nos permite definir métodos y clases que pueden trabajar con diferentes tipos de datos. El tipo de datos no es importante en el momento de definir la clase genérica, si no las operaciones que la clase genérica puede hacer sobre los tipos que maneja.

Los genéricos, además, nos avisan de errores en tiempo de compilación que de otro modo no aparecerían hasta el momento de la ejecución.

Es importante notar que en el momento de instanciar una clase genérica es cuando debemos indicar el tipo concreto. En este punto, el tipo de las referencias está formado tanto por el nombre de la clase genérica como por el tipo concreto que se utilizó en la instancia.