CodeGym /Cursos /JAVA 25 SELF /Introducción a los genéricos

Introducción a los genéricos

JAVA 25 SELF
Nivel 16, Lección 4
Disponible

1. Introducción

Imagina que tienes una caja universal en la que puedes meter cualquier cosa: una manzana, un libro o incluso un juguete. En Java, esa «caja universal» es una clase que almacena datos del tipo más general, Object. Este tipo es la superclase de todas las demás clases en Java, por lo que en una variable de tipo Object se puede guardar absolutamente cualquier objeto.

Ahora imagina un almacén con esas cajas. Si las cajas no llevan etiqueta, puedes meter ahí lo que quieras, pero cuando llegue el momento de sacar algo, tendrás que abrir la caja y adivinar qué hay dentro. Con los genéricos, la situación se parece a un almacén con rótulos ordenados: «Solo manzanas», «Solo libros», «Solo herramientas». Ahora siempre sabes qué hay en cada caja y no podrás meter por accidente un libro en una caja para manzanas.

A primera vista, almacenar en Object parece cómodo: no hace falta crear clases separadas para distintos tipos de datos. Pero en la práctica esa «universalidad» se convierte en problemas:

  1. Es fácil cometer un error. Puedes meter por accidente en la caja un objeto distinto del esperado.
  2. El compilador no verá el problema. Simplemente te permitirá guardar lo que sea, porque el tipo Object lo permite.
  3. Hay que «desempaquetar» manualmente. Cuando sacas un objeto de esa caja, volverá a ser de tipo Object, y tendrás que convertirlo tú mismo al tipo correcto (esto se llama conversión de tipos o cast). Y si te equivocas con el tipo, ¡la aplicación terminará con un error!

Veámoslo con un ejemplo sencillo.

class Box {
    private Object value;

    public void set(Object value) {
        this.value = value;
    }

    public Object get() {
        return value;
    }
}

Ahora usemos esta caja:

Box box = new Box();
box.set("Hola"); // Metimos una cadena
String s = (String) box.get(); // Sacamos la cadena, todo bien

box.set(123); // Metimos un número
// El compilador no ve el problema...
String t = (String) box.get(); // ¡Error en tiempo de ejecución!

Como ves, el compilador dejó pasar código que acabó provocando un error. Nos enteramos del problema solo cuando la aplicación se ejecutó y falló.

2. La solución: genéricos

Generics (genéricos) son la forma de resolver este problema. Es una sintaxis especial que permite vincular una clase o método a un tipo de datos concreto ya en tiempo de compilación. Dicho simple, es como una etiqueta en la caja que dice: «En esta caja pueden ir solo cadenas» o «En esta caja pueden ir solo números».

De este modo obtenemos seguridad de tipos: el compilador no nos dejará meter en la «caja» un objeto incorrecto. Comprobará nuestro código y señalará el error antes de ejecutar el programa.

La misma clase Box, pero ahora con genéricos:

class Box<Type> {
    private Type value;

    public void set(Type value) {
        this.value = value;
    }

    public Type get() {
        return value;
    }
}

Aquí Type es un parámetro de tipo. Es un nombre simbólico que elegimos nosotros (normalmente se usan T, E, K, V) y viene a decir: «Cuando creemos una Box, indicaremos con qué tipo va a trabajar, y usaré ese tipo en todos los lugares donde en el código ponga Type».

3. Uso de los genéricos

Cuando creamos un objeto de una clase genérica, indicamos el tipo concreto entre los corchetes angulares <...>.

// Creamos una caja que solo funciona con cadenas
Box<String> stringBox = new Box<>();
stringBox.set("¡Hola, mundo!"); // OK, metimos una cadena
String s = stringBox.get(); // Sacamos la cadena sin conversiones de tipo

stringBox.set(123); // ¡Error de compilación! El compilador no permitirá esto.

Si creamos Box<Integer>, el compilador velará por que solo se metan números:

// Creamos una caja que solo funciona con números
Box<Integer> intBox = new Box<>();
intBox.set(42); // OK, metimos un número
Integer number = intBox.get(); // Sacamos el número sin conversiones de tipo

intBox.set("Hola"); // ¡Error de compilación!

Ahora el compilador sabe exactamente qué tipo de datos debe haber en cada caja y nos protege de los errores.

4. Cómo funciona con ejemplos

Los genéricos se pueden usar no solo en clases, sino también en métodos. Esto permite escribir un código muy flexible y universal.

Ejemplo 1 — clase con genéricos

Supongamos que necesitamos una clase Pair que guarde dos objetos del mismo tipo. Con genéricos se ve así:

class Pair<T> {
    private T first;
    private T second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }
}

Uso:

Pair<String> greetings = new Pair<String>("Hola", "Mundo");
System.out.println(greetings.getFirst() + " " + greetings.getSecond());

Pair<Integer> numbers = new Pair<Integer>(10, 20);
System.out.println(numbers.getFirst() + numbers.getSecond());

En el primer caso, el compilador está seguro de que en greetings hay cadenas; en el segundo, números.

Ejemplo 2 — método genérico

Podemos escribir un método que funcione con cualquier tipo de dato.

class Utils {
    // <T> antes de void indica que el método trabajará con un parámetro de tipo T
    public static <T> void printTwice(T value) {
        System.out.println(value);
        System.out.println(value);
    }
}

Ahora podemos llamar a este método con cualquier tipo de dato, y funcionará igual:

Utils.printTwice("Java");
Utils.printTwice(123);
Utils.printTwice(3.14);

5. Ventajas de los genéricos

Los genéricos no son solo azúcar sintáctico, sino una herramienta potente que resuelve problemas reales en el desarrollo. Veamos tres ventajas clave.

Seguridad de tipos es la principal ventaja de los genéricos. Sin ellos, el compilador no puede comprobar si usas correctamente los tipos de datos en tus clases y métodos «universales». Errores como intentar meter una cadena en una «caja para números» solo se detectarán en tiempo de ejecución, cuando la aplicación de repente «falle» con una excepción ClassCastException. Con genéricos, el compilador vigila estrictamente los tipos. Comprueba que metas en Box<String> solo cadenas y en Box<Integer> solo números. Si intentas hacer algo incorrecto, te lo señalará enseguida y podrás corregirlo antes de ejecutar el programa. Esto hace tu código mucho más fiable y predecible.

Código más limpio (sin conversiones de tipo innecesarias). Recuerda cómo se veía el código con nuestra «caja universal» sin genéricos: Box box = new Box(); box.set("Hola"); String s = (String) box.get(); Cada vez que sacabas un objeto de la caja, tenías que escribir (String), (Integer), etc. Con genéricos, esa necesidad desaparece. El compilador ya sabe qué tipo hay dentro y lo convierte automáticamente por ti: Box<String> stringBox = new Box<>(); stringBox.set("Hola"); String s = stringBox.get(); Esto no solo acorta el código, sino que mejora mucho su legibilidad y comodidad.

Flexibilidad y reutilización de código. Los genéricos permiten crear clases y métodos realmente universales que funcionan con distintos tipos de datos sin perder seguridad de tipos. Por ejemplo, la clase Box<T> se puede usar para almacenar cadenas, números, tus propias clases (Student, Car) — ¡para lo que sea! No necesitas escribir una clase aparte StringBox, IntegerBox y StudentBox. Escribes una clase universal Box<T> e indicas el tipo necesario al crear el objeto. Esto permite reducir la cantidad de código, evitar duplicaciones y hacer tu programa más modular y flexible.

6. Limitaciones de los genéricos

A pesar de todas sus ventajas, los genéricos tienen varias limitaciones importantes que conviene conocer.

Primitivos (primitives). No puedes usar tipos primitivos (como int, double, boolean, etc.) como parámetro de tipo. Por ejemplo, el código Box<int> intBox = new Box<>(); dará un error de compilación. En su lugar debes usar sus «clases envoltorio» (Integer, Double, Boolean). Box<Integer> intBox = new Box<>();. El compilador de Java sabe convertir automáticamente primitivos a clases envoltorio y viceversa (int a Integer y al revés): este mecanismo se llama autoboxing/unboxing.

Borrado de tipos (type erasure). Es la característica clave de cómo están implementados los genéricos en Java. La idea es que el compilador usa la información de los genéricos solo durante la compilación. Una vez que ha comprobado tu código y se ha asegurado de su seguridad de tipos, borra toda la información sobre los parámetros de tipo. Por ejemplo, Box<String> y Box<Integer> en el código compilado (bytecode) se verán simplemente como Box. Esto significa que para la máquina virtual de Java (JVM) pasan a ser el mismo tipo.

¿Qué significa esto para ti? No puedes crear arreglos de genéricos, por ejemplo new Box<String>[10], ni puedes usar instanceof con genéricos para comprobar si un objeto es instanceof Box<String>. Es un tema más complejo, pero entenderlo es importante para profundizar en Java. En esta etapa basta con recordar que los genéricos son, en esencia, una «etiqueta» para el compilador que le ayuda a comprobar el código, pero esa etiqueta se quita cuando el programa está listo para ejecutarse.

1
Tarea
JAVA 25 SELF, nivel 16, lección 4
Bloqueada
Almacén antiguo: una caja universal pero sin tipo definido 📦
Almacén antiguo: una caja universal pero sin tipo definido 📦
1
Tarea
JAVA 25 SELF, nivel 16, lección 4
Bloqueada
Almacén moderno: caja "inteligente" con etiquetado preciso 🏷️
Almacén moderno: caja "inteligente" con etiquetado preciso 🏷️
1
Tarea
JAVA 25 SELF, nivel 16, lección 4
Bloqueada
Sistema de registro de vínculos: pares genéricos de datos 🖇️
Sistema de registro de vínculos: pares genéricos de datos 🖇️
1
Tarea
JAVA 25 SELF, nivel 16, lección 4
Bloqueada
Herramientas del desarrollador: eco universal 💬
Herramientas del desarrollador: eco universal 💬
1
Cuestionario/control
Clases anidadas e internas, nivel 16, lección 4
No disponible
Clases anidadas e internas
Clases anidadas e internas
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION