Saltar a contenido

Genéricos

alt text

Antes de Java 5 cuando introducíamos objetos en una colección estos se guardaban como objetos de tipo Object, aprovechando el polimorfismo para poder introducir cualquier tipo de objeto en la colección. Esto nos obligaba a hacer un casting al tipo original al obtener los elementos de la colección.

public class Ejemplo {  
  public static void main(String[] args) {  
    List lista = new ArrayList();  
    lista.add("Hola mundo");  

    String cadena = (String) lista.get(0);  
    System.out.println(cadena);  
  }  
} 

Esta forma de trabajar no solo nos ocasiona tener que escribir más código innecesariamente, sino que es propenso a errores porque carecemos de un sistema de comprobación de tipos. Si introdujéramos un objeto de tipo incorrecto el programa compilaría pero lanzaría una excepción en tiempo de ejecución al intentar convertir el objeto en String:

public class Ejemplo {  
  public static void main(String[] args) {  
    List lista = new ArrayList();  
    lista.add(22);  

    String cadena = (String) lista.get(0);  
    System.out.println(cadena);  
  } 
} 

Desde Java 5 contamos con una característica llamada generics que puede solventar esta clase de problemas. Los generics son una mejora al sistema de tipos que nos permite programar abstrayéndonos de los tipos de datos.

Genéricos significa tipos parametrizados. La idea es permitir que el tipo (Integer, String, etc., y tipos definidos por el usuario) sea un parámetro para métodos, clases e interfaces. Utilizando Generics, es posible crear clases que trabajen con diferentes tipos de datos. Una entidad como clase, interfaz o método que opera en un tipo parametrizado es una entidad genérica.

Gracias a los genéricos podemos especificar el tipo de objeto que introduciremos en la colección, de forma que el compilador conozca el tipo de objeto que vamos a utilizar, evitándonos así el casting. Además, gracias a esta información, el compilador podrá comprobar el tipo de los objetos que introducimos, y lanzar un error en tiempo de compilación si se intenta introducir un objeto de un tipo incompatible, en lugar de que se produzca una excepción en tiempo de ejecución.

alt text

El operador Diamond <>

Para utilizar generics con nuestras colecciones tan solo tenemos que indicar el tipo entre el operador Diamond <> a la hora de crearla. A estas clases a las que podemos pasar un tipo como {«parámetro»+} se les llama clases parametrizadas, clases genéricas o simplemente genéricas (generics).

public class Ejemplo {  
  public static void main(String[] args) {  
    List<String> lista = new ArrayList<String>();  
    lista.add("Hola mundo");  

    String cadena = lista.get(0);  
    System.out.println(cadena);  
  }  
} 

El código anterior no compilaría, si intentáramos insertar en la lista un número lista.add(14);, nos daría un error de compilación de tipos.

alt text

Note

Algo a tener en cuenta, es que el tipo parámetro debe ser una clase. No podemos utilizar tipos primitivos.

Clases genéricas

Al crear una clase que utiliza o contiene algún atributo genérico, me obliga a añadir este tipo de genérico en la definición de clase. Por convención se suele utilizar una sola letra mayúscula para el tipo genérico.

Es decir, si mi clase tiene un atributo T elemento genérico que no sé qué tipo de dato va a ser, entero, double, float.... le pongo una letra y con esto le digo que ese atributo es de tipo genérico, puede ser cualquier tipo de dato.

public class Box<T> {

  private T elemento;

  public T get() { return elemento; }
  public void set(T elemento) { this.elemento = elemento; }

}
alt text

Según las convenciones los nombres de los parámetros de tipo usados comúnmente son los siguientes:

  • E: elemento de una colección.
  • K: clave.
  • N: número.
  • T: tipo.
  • V: valor.
  • S, U, V etc: para segundos, terceros y cuartos tipos.

En la api de Java tenemos muchos ejemplos de uso

alt text

En el momento de la instanciación de un tipo genérico indicaremos el argumento para el tipo, en este caso Box contendrá una referencia a un tipo Integer.

//Las dos formas son válidas:
Box<Integer> integerBox1 = new Box<Integer>();
Box<Integer> integerBox2 = new Box<>();
integerBox2.set(4);
integerBox1.set(6);
int suma=integerBox1.get()+integerBox2.get();

Box<String> textoBox = new Box<>();
textoBox.set("Hola mundo");

Estamos creando objetos de la clase Box, tanto de tipo Integer como String.

Nota

La programación genérica permite:

  • Reutilizar código, evitando tener que crear una clase diferente para cada de tipo de objeto que se quiera manejar, aunque el comportamiento vaya a ser el mismo.
  • Reducir errores en tiempo de ejecución, eludiendo la generación de excepciones del tipo ClassCastException.
  • Disminuir la necesidad de hacer conversiones de tipos primitivos, pues cuando el código es extenso se vuelve engorroso.

Métodos genéricos

Al igual que ocurre con las clases, si me creo un método genérico, es decir, que recibe tipos de datos genéricos únicos que no están definidos en la clase, tengo que especificar en la signatura del método esos genéricos:

Ejemplo

public static <T, R> void executeFunction(List<T> lista, Function<T,R> function) {
  for(T t: lista) {
    System.out.println(function.apply(t));
  }
}

El comodín <?>

En Java, <?> se lee como:

“un tipo desconocido”

Se usa cuando:

  • No nos importa el tipo exacto
  • Solo queremos leer datos, no modificarlos
  • Queremos que el método funcione con cualquier tipo genérico

Si tenemos la clase Caja

public class Caja<T> {
    private T contenido;

    public Caja(T contenido) {
        this.contenido = contenido;
    }

    public T getContenido() {
        return contenido;
    }

    public void setContenido(T contenido) {
        this.contenido = contenido;
    }
}
Podemos crear objetos como ya hemos visto como

Caja<String> c1 = new Caja<>("hola");
String s = c1.getContenido(); // NO hace falta cast

Caja<Integer> c2 = new Caja<>(10);
int n = c2.getContenido(); // auto-unboxing

Mediante el operador <?> podemos definir objetos desconocidos

Caja<?> c = new Caja<>("hola");
Object x = c.getContenido(); // solo puedo leer como Object
//no compila
Caja<?> c = new Caja<>("hola");
c.setContenido("adios"); // ❌ ERROR
El siguiente ejemplo no compila. c podría ser realmente Caja<Integer> y estarías metiendo un String. Java no es capaz de inferirlo

//no compila
Caja<?> c = new Caja<>("hola");
c.setContenido("adios"); // ❌ ERROR
Lo único que se puede “meter” en Caja<?> es null.

Porque Caja<String> NO es Caja<Object>

Caja<?> c = new Caja<>("hola");
c.setContenido(null); // ✅ (null vale para cualquier tipo)
Lo correcto si quieres “cualquier Caja”:

Caja<?> c = new Caja<>("hola");
Pero hay que realizar un cash para obtener el dato

// String s= c.getContenido();  ❌ ERROR
String s= (String) c.getContenido();
El sitio típico donde usamos <?> en en la definición de equals, donde no sabemos el genérico usado

@Override
    public boolean equals(Object o) {
      //No compila: ❌ ERROR
        if (!(o instanceof Caja<T> caja)) return false;
        return Objects.equals(contenido, caja.contenido);
    }
Tú no sabes si obj es:

Caja<String>

Caja<Integer>

o cualquier otro tipo

Lo correcto es usar <?>

@Override
    public boolean equals(Object o) {
        if (!(o instanceof Caja<?> caja)) return false;
        return Objects.equals(contenido, caja.contenido);
    }
En resumen

alt text