Genéricos

Introducción

Cuando hablamos de genérico nos referimos a “tipo genérico”. Es decir tipaje de objetos libre, no es necesario decir el tipo de clase que es o el tipo de dato que va a devolver un método. Con esta implicación se pueden crear Clases genéricas, métodos genéricos o incluso iterfaces genéricas. La idea principal de usar genéricos es ahorrar código y también generar código más flexible. La representación de algo genérico se expresa entre , la letra puede ser cualquiera y se suele representar en mayúsculas. Veamos un ejemplo:

Supongamos que deseamos definir un método que nos permita imprimir arrays de diferentes tipos de datos: enteros, caracteres, cadenas,… Hasta ahora, esto lo conseguíamos mediante la sobrecarga de métodos. Por cada tipo de dato, añadíamos a nuestra clase una nueva versión del método que esencialmente era la misma que las otras, con la salvedad de que el dato sobre el que operaba era diferente:

public class DemoGen1 {
 public static void imprimeArray(int[] arr) {
  for (int val: arr) System.out.printf("%s ", val);
    System.out.println();
 }
 public static void imprimeArray(double[] arr) {
  for (double val: arr) System.out.printf("%s ", val);
    System.out.println();
 }
 public static void main(String[] args) {
  double[] a1 = { 3.5, 2.0, 4, -1.67 };
  int[] a2 = { 5, 0, 4, -1 };
  imprimeArray(a1);
  imprimeArray(a2);
  }
}

La idea fundamental es, en lugar de indicar el tipo de dato, indicamos un tipo “genérico” (le llamaremos T) y realizaremos la implementación con él. Durante la ejecución, será como si dicho tipo genérico se “sustituyera” por el tipo correcto correspondiente:

public class DemoGen2 {
 
 public static <T> void imprimeArray(T[] arr) {
    for (T val: arr) System.out.printf("%s ", val);
        System.out.println();
}

 public static void main(String[] args) {
    Double[] a1 = { 3.5, 2.0, 4, -1.67 };
    Integer[] a2 = { 5, 0, 4, -1 };
    String[] a3 = { "mi", "casa, ", "teléfono " };
    imprimeArray(a1);
    imprimeArray(a2);
    imprimeArray(a3);
 }
}

Acabéis de ver un método genérico para imprimir un array, vamos ahora a por clases genéricas.

Nota importante: al usar métodos o clases con tipos de datos genéricos si se necesita usar tipos de datos primitivos se tiene que utilizar los wrappers correspondientes.

Clases genéricas

En la definición de una clase a la derecha del nombre de la clase con < > se indicará uno o varios tipos parametrizados (una lista de letras en mayúscula): [modif] class nombreClase<lista_tipos_param> [extends …] [implements …] { … }

public class ClaseGen<T> {
 T miVal; //se declara una variable tipo parametrizado

 //tipo T lo podemos usar en métodos sin problemas
 public ClaseGen(T val) {
 this.miVal = val;
 }
 public T getMiVal() {
 return this .miVal;
 }
 public String toString() {
 return "ClaseGen<" + this.miVal.getClass().getName() + ">: miVal = " + this.miVal;
 }
}
// En ejecución hay que indicar que tipos vamos a usar con esa clase genérica

public class DemoGen3 {
 public static void main(String[] args) {
    ClaseGen<Double> obj1 = new ClaseGen<Double>(23.75);
    double v1 = obj1.getMiVal();
    System.out.println("obj1 miVal = " + v1);
    ClaseGen<String> obj2 = new ClaseGen<String>("Hello!");
    String v2 = obj2.getMiVal();
    System.out.println("obj2 miVal = " + v2);
    System.out.println("obj1 = " + obj1);
    System.out.println("obj2 = " + obj2);
 }
}

Fíjate que al declarar una nueva variable de la clase genérica ClaseGen, debemos indicar el tipo “real” que reemplazará, en esa variable concreta, al tipo parametrizado (en este caso, Double). Al crear el objeto, todas las referencias internas de la clase en atributos, variables, métodos,… al tipo parametrizado T serán sustituidas por referencias al tipo indicado. No se pueden usar tipos primitivos.

Varios tipos parametrizados

Si por ejemplo, quisiésemos que el par de elementos fuesen de dos tipos, independientes el uno del otro, podríamos redefinir una clase Pair como:

class Pair<T, V> {
 private T first;
 private V second;
 public T getFirst() {
 return first;
 }
 public V getSecond() {
 return second;
 }
 public void setFirst(T first) {
 this.first = first;
 }
 public void setSecond(V second) {
 this.second = second;
 }
}

Ejercicio 1

Utilizando la definición de clase anterior, crea un main que pruebe estas posibilidades:

  • Un par de Integer y Float.
  • Un par de String y array de enteros.
  • Un par de Persona y Persona. Crea esa clase Persona.

Tipos acotados

Podemos restringir o acotar el tipo genérico en nuestras clases genéricas con el uso de herencia (extends): Por ejemplo, supón que quieres crear una clase genérica que almacene valores numéricos y sea capaz de realizar diferentes operaciones sobre ellos (calcular el inverso,… ). ¿Cómo podríamos restringir que sólo se pudieran crear objetos utilizando clases como Integer, Float o Double? Para ello usaremos una superclase, clase o interface que debe extender el tipo. En este ejemplo Number:

public class ClaseGen<T extends Number> {
   T miVal;
   public ClaseGen(T val) { this.miVal = val; }
   public T getMiVal() { return this.miVal; }
   public double getDoubleVal() {
      return this .miVal.doubleValue();
   }
   public double getMiValInverse() {
      return 1/this.miVal.doubleValue();
   }
   public String toString() {
      return "ClaseGen<" + this.miVal.getClass().getName() + ">: miVal = " + this.miVal;
 }
}

Hemos podido usar el método doubleValue() porque es un método de Number.

Métodos y constructores genéricos

Los métodos también pueden ser genéricos sin necesidad de que los tipos genéricos esten definidos al nivel de clase. Por ejemplo, imagina una clase Util que tiene muchos métodos. Sólo uno de esos métodos, el método imprimirArray() necesita un tipo genérico, no es necesario ni apropiado definir el tipo genérico a nivel de clase, se hace directamente en el método:

class Util {
    static <T> void imprimirArray (T[] t) {
        for(int i=0;i<t.length;i++)
            System.out.println(t[i].toString());
    }
}

De la misma forma que los métodos… los constructores pueden usar los tipos genéricos de la clase:

public class Pareja<K, V> {
   private K clave;
   private V valor;
   public Pareja(K clave, V valor) {
      this.clave = clave;
      this.valor = valor;
   }
 public K getClave() { return this.clave; }
 public T getValor() { return this.valor; }
 public void setClave(K clave) { this.clave = clave; }
 public void setValor(V valor) { this.valor = valor; }
 public String toString() { return "Pareja [" + this.clave + ", " + this.valor + "]"; }
}

Ejercicio 2

Añade a la clase genérica Pair un constructor de forma que al crear los pares se pueda pasar al constructor los valores de first y second.

Ejercicio 3

Crea un main para utiliza el método anterior (imprimirArray) de forma que imprime un array de Integer, un array de Double y un array de personas. Crea tu tres arrays sencillitos de las clases anteriores e invoca al método.

Ejercicio 4

Partiendo de la siguiente implementación de lista enlazada que debes modificarla para que trabaje con cualquier tipo de dato, no sólo entero. Pruébala desde un main y crea y recorre una listas de Persona y otra de Integer.

class Nodo{
  private Nodo sig;
  private int dato;
  public Nodo(int dato) {
    this.dato = dato;
    this.sig=null;
  }
  public Nodo(int dato, Nodo sig) {
    this.dato = dato;
    this.sig = sig;
  }
  public void setSiguiente(Nodo sig) {
    this.sig = sig;
  }
  public Nodo getSiguiente() {
    return sig;
  }
  public int getDato() {
    return dato;
  }
}
class MiListaEnlazada{
    private Nodo primero=null;
    public void insertar(int dato){
        if(primero==null){
            primero=new Nodo(dato);
        }else{
            Nodo temp= new Nodo(dato,primero);
            primero=temp;
        }
            
    }
    public int tamano(){
        int i=0;
        Nodo temp=primero;
        while(temp!=null){
            i++;
            temp=temp.getSiguiente();
        }
        return i;
    }
    public int obtener(int indice){
        Nodo temp=primero;
        int i=0;
        while(i<indice){
            temp=temp.getSiguiente();
            i++;
        }
            
        return temp.getDato();
    }
}