Definición de genéricos en Java
Empecemos con una pequeña definición de interfaces List e Iterator en el paquete java.util.
1public interface List<E> {2 void add(E x);3 Iterator<E> iterator();4}5
6public interface Iterator<E> {7 E next();8 boolean hasNext();9}
Todo este código debería ser familiar, excepto por las partes entre los ángulos <>
. Estas son las declaraciones de los parámetros de tipo formales de las interfaces List
e Iterator
.
Los parámetros de tipo se pueden usar a lo largo de la declaración genérica, prácticamente donde usaríamos tipos ordinarios.
En la introducción, vimos invocaciones de la declaración de tipo genérico List
, como List<Integer>
. En la invocación (usualmente llamada tipo parametrizado), todas las ocurrencias del parámetro de tipo formal (E
en este caso) se reemplazan por el argumento de tipo real (en este caso, Integer
).
Podríamos imaginar que List<Integer>
representa una versión de List
donde E
ha sido reemplazado uniformemente por Integer
:
1public interface ListaEnteros {2 void add(Integer x);3 Iterator<Integer> iterator();4}
Esta intuición puede ser útil, pero también engañosa.
Es útil, porque el tipo parametrizado List<Integer>
de hecho tiene métodos que se ven exactamente como esta expansión.
Es engañosa, porque la declaración de un genérico nunca se expande realmente de esta manera. No hay múltiples copias del código, ni en el código fuente, ni en binario, ni en disco ni en memoria.
Una declaración de tipo genérico se compila una sola vez y se convierte en un solo archivo de clase, al igual que una declaración ordinaria de clase o interfaz.
Los parámetros de tipo son análogos a los parámetros ordinarios utilizados en métodos o constructores. Así como un método tiene parámetros de valor formales que describen los tipos de valores sobre los que opera, una declaración genérica tiene parámetros de tipo formales.
Cuando se invoca un método, los argumentos reales se sustituyen por los parámetros formales, y se evalúa el cuerpo del método. Cuando se invoca una declaración genérica, los argumentos de tipo reales se sustituyen por los parámetros de tipo formales.
Una nota sobre las convenciones de nomenclatura. Se recomienda utilizar nombres concisos (de un solo carácter si es posible) pero representativos para los parámetros de tipo formales. Es mejor evitar caracteres minúsculos en esos nombres, facilitando distinguir los parámetros de tipo formales de las clases e interfaces ordinarias. Muchos tipos contenedores usan E, de element (elemento), o T, de type (tipo), como nombres de parámetro de tipo.
Ejemplo de uso de genéricos
Veamos un ejemplo de una clase genérica que implementa una pila.
1public class Pila<E> {2 private List<E> elementos = new ArrayList<E>();3
4 public void push(E elemento) {5 elementos.add(elemento);6 }7
8 public E pop() {9 return elementos.remove(elementos.size() - 1);10 }11
12 public boolean estaVacia() {13 return elementos.isEmpty();14 }15}
Esto debemos implementarlo en su propio archivo, llamado Pila.java
. Luego, en nuestro archivo App.java
, podemos usar la clase Pila
de la siguiente manera:
1public class App {2 public static void main(String[] args) {3 Pila<Integer> pilaEnteros = new Pila<Integer>();4 pilaEnteros.push(1);5 pilaEnteros.push(2);6 pilaEnteros.push(3);7
8 while (!pilaEnteros.estaVacia()) {9 System.out.println(pilaEnteros.pop());10 }11 }12}
Este código imprimirá:
321
Si, en cambio, usamos Pila
para almacenar cadenas, el código se vería así:
1public class App {2
3 public static void main(String[] args) {4 Pila<String> pila = new Pila<>();5 pila.push("Hola");6 pila.push("Mundo");7 pila.push("!");8 pila.push("Java");9 while (!pila.estaVacia()) {10 System.out.println(pila.pop());11 }12 }13}
Este código imprimirá:
Java!MundoHola