domingo, 2 de diciembre de 2012

Introducción a los genéricos (generics) en Java

votar
Érase una vez, en una galaxia muy, muy lejana, antes de Java 5 y de la existencia de genéricos, los programadores java que utilizaban colecciones, se enfrentaban a un temible peligro.

Ellos podían, por ejemplo, crear una lista de Strings como la siguiente:

List miLista = new ArrayList( );
miLista.add(“Hola”);
miLista.add(“Galaxia”);
miLista.add(new Integer(2001));

podían, incluso, hacer un casting al obtener los elementos de esa lista:

String s = (String) miLista.get(0);
String st = (String) miLista.get(1);
String str = (String) miLista.get(2);

¡Pero nada de eso evitaba que al ejecutarse el programa se produjera un error! ¿Por qué? ¡Porque el tercer elemento no es un String, sino un Integer, pero nada impedía añadirlo a la lista! Por suerte, los genéricos vinieron al rescate de estos desdichados programadores y nunca más se produjeron errores de este tipo.

Bueno, vale, puede que la historia no fuese exactamente así, pero nos sirve para explicar en qué consisten los genéricos y su función. Desde luego los genéricos no se utilizan sólo con las colecciones, pero es cierto que es en ellas donde te los vas a encontrar mayoritariamente.

Los genéricos permiten asignar parámetros a las clases, interfaces, métodos..., de forma que sólo admitan los tipos de objetos que tu quieras. No se pueden utilizar con primitivos, pero si con las clases que se corresponden con ellos. Por eso no puedes hacer un genérico tipo int, pero sí un Integer. Veamos el ejemplo anterior utilizando genéricos:

List <String> miLista = new ArrayList <String>();
//al poner <String> estamos indicando que la lista sólo acepta Strings
miLista.add(“Hola”); //un String, perfecto
miLista.add(“Galaxia”); //otro String, muy bien
miLista.add(new Integer(2001)); //¡error de compilación, no es un String!

Evidentemente, es mucho más sencillo solucionar un error de compilación que un error de ejecución. Además, no hace falta ningún casting para obtener los elementos de la lista:

String s = miLista.get(0);
String st = miLista.get(1);
//el compilador ya sabe que es un String, no hace falta casting.

Para conseguir esto, basta poner entre los signos < >, llamados diamante, el tipo al que queremos que pertenezcan los elementos: String, Integer o lo que sea. Además, para facilitar más las cosas, a partir de Java 7 la primera línea de este código se escribiría así:

List <String> miLista = new ArrayList<>();

porque el compilador ya infiere de qué tipo son los elementos de la lista.

Pero, ¿cómo crear nuestra propia clase genérica? Fíjate en este ejemplo:

public class Cajón<T>{
   private T t;
   public void set (T t){
   this.t = t;
   }
   public T get( ){
   return t;
   }
}

Ya hemos construído una clase genérica, y le hemos proporcionado dos métodos. La T es el tipo de parámetro que le vamos a asignar. Supón por ejemplo que quieres un Cajón de Calcetines y otro Cajón de Camisetas, pues sustituyes T por Calcetines o por Camisetas (por supuesto, si no existen las clases Camisetas ni Calcetines, debes crearlas) de esta forma:

public class Cajón<Calcetines> o public class Cajón<Camisetas>

Todos los métodos de tu clase genérica pueden ser utilizados en cualquiera de ellas, pero no podrás usar calcetines con Cajón<Camisetas> ni camisetas con Cajón<Calcetines>.

Por convención, los nombres de los tipos de parámetros se escriben como una letra mayúscula, y los más habituales son :
  • T, de tipo
  • E, de elemento, muy usado con la Java Collection Framework
  • N, de número (class Number)
  • K, de key
  • V, de value, estas dos últimas utilizadas sobre todo en mapas (Map)

¿Y cómo sería un método en el que estemos utilizando genéricos?

void añadirCalcetines (Cajón<Calcetines> cal){
   cal.add(new Calcetines( ));
}

Un método muy simplón, pero sirve para que veas la sintaxis. Y hablando de sintaxis, es muy habitual con los genéricos ver esto: <?> acompañado a veces de “extends” o “super”. Ahora mismo te explico lo que significa. Supongamos que tienes una lista de números enteros:

List<Integer> miLista = new List<>( );

y piensas, “bueno, Integer es una subclase de Number, así que puedo hacer esto” y escribes:

List<Number> miLista = new List<Integer>( );

¡No compila! ¿Por qué? ¡Porque List<Integer> no es un subtipo de List<Number>! En List<Number> sólo pueden entrar objetos que pertenezcan a la clase Number, no objetos de la clase Integer, ni de la clase Double, ni Short, ni Long. Sólo de la clase Number, por eso, aunque a la lista puedes añadirle números enteros, o decimales o los que quieras que pertenezcan a Number, no puedes decir que es una nueva lista de números enteros. Ahora bien, si escribimos esto:

List<? extends Number>

estamos indicando que esta lista va a admitir objetos de la clase Number y de cualquier otra que la extienda (“?” significa cualquiera). Así pues, modificamos

List<?extends Number> miLista = new List<Integer>( );

y ahora sí que compila. Y también compilará si en lugar de List<Integer> utilizamos cualquier otra subclase de Number, como List<Double>.

Pero, ¿qué ocurre si lo que queremos es lo contrario, es decir, que la lista admita objetos de la clase Integer y su superclase, pero no ninguna otra subclase de Number?
En ese caso, haríamos lo siguiente:

List<? super Integer>

con lo que la lista admitirá objetos de la clase Integer y de cualquier otra clase superior a ella, como Number.

¿Y qué ocurre si hacemos esto?:

List<?>

Lo que estamos indicando ahora es que la lista admitiría cualquier tipo de objetos. No confundir con “objetos de la clase Object”. Es decir, esto:

List<Object>

sólo admite objetos que pertenezcan a la clase Object, y no podemos hacer lo siguiente:

List<Object> miLista = new List<Calcetines>( ); //no compila

Por último, quiero añadir que es cierto que los genéricos en Java no son perfectos (como los programadores en C siempre señalan amablemente) pero eso es así porque desde un principio se busco que fuese compatible con todo el código escrito con anterioridad a Java 5, que no los utilizaba. No era cuestión de que todo ese código dejase de funcionar, ¿no?

Si has tenido dificultades en seguir esta entrada porque no sabes lo que son las colecciones, puedes ver esta entrada anterior. Si el problema es con el polimorfismo y la herencia, consulta esta otra. Para ampliar conocimientos, consulta los tutoriales de Oracle.

Si tienes dudas, pregúntame. Y si te ha gustado, compártelo.;-)


En respuesta a la consulta de Anet (ver Comentarios)
Hola, Anet:
¡Es un placer ver una chica por aquí!
Respecto a tu pregunta, no tengo muy claro qué es exactamente lo que quieres hacer, así que no sé si la respuesta será la que buscas.
Como ya hemos visto, los genéricos siguen las reglas de herencia de Java a nivel de tipo, pero no a nivel de parámetro.
Por ejemplo, podemos hacer

Set<String> mySet = new HashSet<String>();

porque HashSet es un subtipo de Set, pero no

Set<Object> mySet = new HashSet<String>();

porque sólo puedes usar Object como parámetro, no String, y por eso utilizamos los wildcards y extends, como muestro arriba.
Con las interfaces, sería lo mismo, solo que en lugar de extends, usaríamos implements.

A continuación te dejo un ejemplo de un código, donde creo una clase que va a ser un subtipo de ArrayList. Fíjate que por eso no necesitamos que mi clase implemente la interfaz List, porque eso ya lo hace ArrayList.
Además, vamos a hacer que implemente una interfaz que yo he creado, de esa manera podrás ver cómo se hace.

import java.util.*;
public  class TestGen{

    public static void main(String args[]){
   MyGen<String> miLista = new MyGen<String>();
miLista.add(0,"A");
miLista.add(1,"B");
miLista.add(2,"C");
for (String item: miLista){
System.out.println(item);
}
ArrayList<String> miOtraLista = new MyGen<String>();
miOtraLista.addAll(miLista);
for (String item: miOtraLista){
System.out.println(item);
}
if (miOtraLista.containsAll(miLista)){
System.out.println("Tienen los mismos elementos");
}
String miEjemplo = miLista.ejemplo().toString();
System.out.println(miEjemplo);
}

}
//La clase MyGen extiende ArrayList (que a su vez implementa List) y además implementa la interfaz //Ejemplo
class MyGen<String>  extends ArrayList<String> implements Ejemplo<Double>{
//aquí sustituímos T por Double, pero podríamos poner cualquier otro tipo
public MyGen(){
super();
}
//nuestra versión del método ejemplo
@Override
public Double ejemplo(){
return 15.25;
}
}
//creamos la interfaz usando T, de forma que cuando la implementemos podamos usar el tipo que //queramos.
 interface Ejemplo<T>{
T ejemplo();
}

De esta manera, la clase MyGen puede utilizar los métodos de ArrayList, que a su vez tiene los de las interfaces que implementa, y además va a poder usar el método de la interfaz Ejemplo.
Espero que ésto te ayude a aclarar los conceptos.

¡Un saludo!

11 comentarios:

  1. buenisimo... gracias :), sali de la duda en cuanto al famoso extends y su famoso ?

    ResponderEliminar
  2. hola, una pregunta podrías proporcionarme un código con herencia, interfaz y tipos genéricos es que no se como mesclarlos, gracias y saludos.

    ResponderEliminar
    Respuestas
    1. Hola, Anet:
      Por algún motivo, Blogger no me deja responder a tu pregunta como yo quisiera, así que he añadido mi respuesta al final de la entrada. ¡Espero que te sirva!

      Eliminar
  3. Gracias y felicidades, tenia problemas con este concepto pero con tu explicacion en forma de preguntas fue MUY BUENA!!!!

    ResponderEliminar
    Respuestas
    1. Muchas gracias, Jesús. Me alegro de que te haya servido.:-)

      Eliminar
  4. Excelente explicación.

    ResponderEliminar
  5. OMG!!!! en ningun otro libro,y desde hace mucho tiempo que no entendia esto, esta tan bien explicado como aqui!!!!! Aplausos!!! lovin it :D!!
    Andrea.

    ResponderEliminar