Los Genéricos de Java es una de las características más importantes introducidas en Java 5. Si has estado trabajando con Colecciones de Java y con la versión 5 o superior, estoy seguro de que lo has utilizado. Los genéricos en Java con clases de colección son muy fáciles, pero proporciona muchas más características que simplemente crear el tipo de colección. Intentaremos aprender las características de los genéricos en este artículo. Entender los genéricos puede volverse confuso a veces si usamos palabras técnicas, así que intentaré mantenerlo simple y fácil de entender.
Exploraremos los siguientes temas sobre genéricos en Java.
1. Genéricos en Java
Los genéricos se añadieron en Java 5 para proporcionar comprobación de tipos en tiempo de compilación y eliminar el riesgo de ClassCastException que era común al trabajar con clases de colección. Se reescribió todo el marco de colecciones para usar genéricos para la seguridad de tipos. Veamos cómo nos ayudan los genéricos a utilizar clases de colección de forma segura.
List list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //OK
for(Object obj : list){
// La conversión de tipos provoca ClassCastException en tiempo de ejecución
String str=(String) obj;
}
El código anterior compila bien pero genera ClassCastException en tiempo de ejecución porque estamos intentando convertir un Object en la lista a String mientras que uno de los elementos es de tipo Integer. Después de Java 5, usamos clases de colección como se muestra a continuación.
List list1 = new ArrayList(); // java 7 ? List list1 = new ArrayList<>();
list1.add("abc");
//list1.add(new Integer(5)); // error de compilación
for(String str : list1){
// no se necesita conversión de tipos, evita ClassCastException
}
Observe que al momento de crear la lista, hemos especificado que el tipo de elementos en la lista será String. Por lo tanto, si intentamos agregar cualquier otro tipo de objeto en la lista, el programa generará un error en tiempo de compilación. También observe que en el bucle for, no necesitamos la conversión de tipos del elemento en la lista, eliminando así la ClassCastException en tiempo de ejecución.
2. Clase Genérica en Java
Podemos definir nuestras propias clases con tipos genéricos. Un tipo genérico es una clase o interfaz que está parametrizada sobre tipos. Usamos corchetes angulares (<>) para especificar el parámetro de tipo. Para entender el beneficio, digamos que tenemos una clase simple como:
package com.journaldev.generics;
public class GenericsTypeOld {
private Object t;
public Object get() {
return t;
}
public void set(Object t) {
this.t = t;
}
public static void main(String args[]){
GenericsTypeOld type = new GenericsTypeOld();
type.set("Pankaj");
String str = (String) type.get(); //type casting, error prone and can cause ClassCastException
}
}
Observe que al usar esta clase, tenemos que realizar una conversión de tipo y puede producir ClassCastException en tiempo de ejecución. Ahora usaremos una clase genérica en Java para reescribir la misma clase como se muestra a continuación.
package com.journaldev.generics;
public class GenericsType<T> {
private T t;
public T get(){
return this.t;
}
public void set(T t1){
this.t=t1;
}
public static void main(String args[]){
GenericsType<String> type = new GenericsType<>();
type.set("Pankaj"); //valid
GenericsType type1 = new GenericsType(); //raw type
type1.set("Pankaj"); //valid
type1.set(10); //valid and autoboxing support
}
}
Observe el uso de la clase GenericsType en el método principal. No necesitamos hacer una conversión de tipo y podemos evitar ClassCastException en tiempo de ejecución. Si no proporcionamos el tipo en el momento de la creación, el compilador producirá una advertencia que dice “GenericsType es un tipo sin formato. Las referencias al tipo genérico GenericsType<T> deberían ser parametrizadas”. Cuando no proporcionamos el tipo, el tipo se convierte en Object
y, por lo tanto, permite tanto objetos String como Integer. Sin embargo, siempre debemos tratar de evitar esto porque tendremos que usar conversiones de tipo mientras trabajamos en tipos sin formato que pueden producir errores en tiempo de ejecución.
Consejo: Podemos usar la anotación @SuppressWarnings("rawtypes")
para suprimir la advertencia del compilador, consulta el tutorial de anotaciones de Java.
También observe que admite autoboxing en Java.
3. Interfaz Genérica de Java
La interfaz Comparable es un gran ejemplo de Genéricos en interfaces y se escribe como:
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
De manera similar, podemos crear interfaces genéricas en Java. También podemos tener múltiples parámetros de tipo como en la interfaz Map. Además, podemos proporcionar un valor parametrizado a un tipo parametrizado también, por ejemplo new HashMap<String, List<String>>();
es válido.
4. Tipo Genérico de Java
La convención de nombres de tipo genérico de Java nos ayuda a entender el código fácilmente y tener una convención de nombres es una de las mejores prácticas del lenguaje de programación Java. Por lo tanto, los genéricos también vienen con sus propias convenciones de nombres. Por lo general, los nombres de los parámetros de tipo son letras mayúsculas individuales para que sean fácilmente distinguibles de las variables de Java. Los nombres de parámetros de tipo más comúnmente utilizados son:
- E – Element (used extensively by the Java Collections Framework, for example ArrayList, Set etc.)
- K – Key (Used in Map)
- N – Number
- T – Type
- V – Value (Used in Map)
- S,U,V etc. – 2nd, 3rd, 4th types
5. Método Genérico de Java
A veces no queremos que toda la clase esté parametrizada, en ese caso, podemos crear un método genérico en Java. Dado que el constructor es un tipo especial de método, también podemos usar tipos genéricos en los constructores. Aquí hay una clase que muestra un ejemplo de un método genérico en Java.
package com.journaldev.generics;
public class GenericsMethods {
// Método genérico en Java
public static boolean isEqual(GenericsType g1, GenericsType g2){
return g1.get().equals(g2.get());
}
public static void main(String args[]){
GenericsType g1 = new GenericsType<>();
g1.set("Pankaj");
GenericsType g2 = new GenericsType<>();
g2.set("Pankaj");
boolean isEqual = GenericsMethods.isEqual(g1, g2);
// La declaración anterior se puede escribir simplemente como
isEqual = GenericsMethods.isEqual(g1, g2);
// Esta característica, conocida como inferencia de tipos, te permite invocar un método genérico como un método ordinario, sin especificar un tipo entre corchetes angulares.
// El compilador inferirá el tipo que se necesita
}
}
Observa la firma del método isEqual mostrando la sintaxis para usar tipos genéricos en los métodos. Además, observa cómo usar estos métodos en nuestro programa Java. Podemos especificar el tipo al llamar a estos métodos o podemos invocarlos como un método normal. El compilador de Java es lo suficientemente inteligente como para determinar el tipo de variable que se debe usar, esta facilidad se llama inferencia de tipos.
6. Parámetros de tipo acotado en Java Generics
Supongamos que queremos restringir el tipo de objetos que pueden usarse en el tipo parametrizado, por ejemplo, en un método que compara dos objetos y queremos asegurarnos de que los objetos aceptados sean Comparables. Para declarar un parámetro de tipo acotado, lista el nombre del parámetro de tipo, seguido de la palabra clave extends, seguido de su límite superior, similar al método a continuación.
public static <T extends Comparable<T>> int compare(T t1, T t2){
return t1.compareTo(t2);
}
La invocación de estos métodos es similar al método no acotado, excepto que si intentamos usar alguna clase que no sea Comparable, lanzará un error en tiempo de compilación. Los parámetros de tipo acotado se pueden usar con métodos, así como con clases e interfaces. Los genéricos de Java también admiten límites múltiples, es decir,
7. Genéricos de Java y Herencia
Sabemos que la herencia en Java nos permite asignar una variable A a otra variable B si A es una subclase de B. Entonces podríamos pensar que cualquier tipo genérico de A puede asignarse a un tipo genérico de B, pero no es el caso. Veamos esto con un programa simple.
package com.journaldev.generics;
public class GenericsInheritance {
public static void main(String[] args) {
String str = "abc";
Object obj = new Object();
obj=str; // works because String is-a Object, inheritance in java
MyClass myClass1 = new MyClass();
MyClass
No se nos permite asignar una variable MyClass<String> a una variable MyClass<Object> porque no están relacionadas, de hecho, el padre MyClass<T> es Object.
8. Clases y subtipos genéricos en Java
Podemos subtipar una clase genérica o interfaz extendiéndola o implementándola. La relación entre los parámetros de tipo de una clase o interfaz y los parámetros de tipo de otra se determinan mediante las cláusulas extends e implements. Por ejemplo, ArrayList<E> implementa List<E> que extiende Collection<E>, por lo que ArrayList<String> es un subtipo de List<String> y List<String> es subtipo de Collection<String>. La relación de subtipado se mantiene siempre que no cambiemos el argumento de tipo, a continuación se muestra un ejemplo de múltiples parámetros de tipo.
interface MyList<E,T> extends List<E>{
}
Los subtipos de List<String> pueden ser MyList<String,Object>, MyList<String,Integer> y así sucesivamente.
9. Comodines en Generics de Java
El signo de interrogación (?) es el comodín en los genéricos y representa un tipo desconocido. El comodín puede usarse como el tipo de un parámetro, campo o variable local, y a veces como un tipo de retorno. No podemos usar comodines al invocar un método genérico o al instanciar una clase genérica. En las siguientes secciones, aprenderemos sobre comodines delimitados superiormente, comodines delimitados inferiormente y captura de comodines.
9.1) Comodín delimitado superiormente en Java
Los comodines delimitados superiormente se usan para relajar la restricción en el tipo de variable en un método. Supongamos que queremos escribir un método que devuelva la suma de números en la lista, por lo que nuestra implementación será algo como esto.
public static double sum(List<Number> list){
double sum = 0;
for(Number n : list){
sum += n.doubleValue();
}
return sum;
}
Ahora, el problema con la implementación anterior es que no funcionará con Listas de Enteros o Dobles porque sabemos que List<Integer> y List<Double> no están relacionadas; aquí es cuando resulta útil un comodín delimitado superiormente. Usamos comodines genéricos con la palabra clave extends y la clase o interfaz de límite superior que nos permitirá pasar un argumento del tipo límite superior o sus subclases. La implementación anterior puede modificarse como el siguiente programa.
package com.journaldev.generics;
import java.util.ArrayList;
import java.util.List;
public class GenericsWildcards {
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>();
ints.add(3); ints.add(5); ints.add(10);
double sum = sum(ints);
System.out.println("Sum of ints="+sum);
}
public static double sum(List<? extends Number> list){
double sum = 0;
for(Number n : list){
sum += n.doubleValue();
}
return sum;
}
}
Es similar a escribir nuestro código en términos de interfaz, en el método anterior podemos usar todos los métodos de la clase de límite superior Number. Tenga en cuenta que con una lista de límites superiores, no se nos permite agregar ningún objeto a la lista excepto null. Si intentamos agregar un elemento a la lista dentro del método de suma, el programa no se compilará.
9.2) Comodín no delimitado de Java Generics
A veces tenemos una situación en la que queremos que nuestro método genérico funcione con todos los tipos, en este caso, se puede usar un comodín no delimitado. Es lo mismo que usar <? extends Object>.
public static void printData(List<?> list){
for(Object obj : list){
System.out.print(obj + "::");
}
}
Podemos proporcionar List<String> o List<Integer> o cualquier otro tipo de argumento de lista de objetos al método printData. Similar a la lista de límites superiores, no se nos permite agregar nada a la lista.
9.3) Comodín delimitado inferior de Java Generics
Supongamos que queremos agregar enteros a una lista de enteros en un método, podemos mantener el tipo de argumento como List<Integer> pero estará vinculado con Integers mientras que List<Number> y List<Object> también pueden contener enteros, por lo que podemos usar un comodín de límite inferior para lograr esto. Usamos el comodín de generics (?) con la palabra clave super y la clase de límite inferior para lograr esto. Podemos pasar el límite inferior o cualquier supertipo del límite inferior como argumento, en este caso, el compilador de Java permite agregar tipos de objetos de límite inferior a la lista.
public static void addIntegers(List<? super Integer> list){
list.add(new Integer(50));
}
10. Subtipado usando Comodines de Generics
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK. List<? extends Integer> is a subtype of List<? extends Number>
11. Borrado de Tipo en Generics de Java
Los generics en Java se agregaron para proporcionar verificación de tipos en tiempo de compilación y no tienen uso en tiempo de ejecución, por lo que el compilador de Java utiliza la característica de borrado de tipo para eliminar todo el código de verificación de tipos genéricos en el código de bytes e insertar conversiones de tipo si es necesario. El borrado de tipo asegura que no se creen nuevas clases para los tipos parametrizados; en consecuencia, los generics no incurren en ningún gasto en tiempo de ejecución. Por ejemplo, si tenemos una clase genérica como la siguiente;
public class Test<T extends Comparable<T>> {
private T data;
private Test<T> next;
public Test(T d, Test<T> n) {
this.data = d;
this.next = n;
}
public T getData() { return this.data; }
}
El compilador de Java reemplaza el parámetro de tipo delimitado T con la primera interfaz de límite, Comparable, como se muestra en el código siguiente:
public class Test {
private Comparable data;
private Test next;
public Node(Comparable d, Test n) {
this.data = d;
this.next = n;
}
public Comparable getData() { return data; }
}
12. Preguntas frecuentes sobre Genéricos
12.1) ¿Por qué usamos Genéricos en Java?
Los Genéricos proporcionan una verificación de tipos sólida en tiempo de compilación y reducen el riesgo de ClassCastException y la conversión explícita de objetos.
12.2) ¿Qué es T en Genéricos?
Usamos <T> para crear una clase, interfaz y método genéricos. La T se reemplaza con el tipo real cuando la usamos.
12.3) ¿Cómo funcionan los Genéricos en Java?
El código genérico garantiza la seguridad del tipo. El compilador utiliza el borrado de tipos para eliminar todos los parámetros de tipo en tiempo de compilación para reducir la sobrecarga en tiempo de ejecución.
13. Genéricos en Java – Lecturas adicionales
- Los genéricos no admiten subtipos, por lo que
List<Number> numbers = new ArrayList<Integer>();
no se compilará, aprenda por qué los genéricos no admiten subtipos. - No podemos crear un array genérico, por lo que
List<Integer>[] array = new ArrayList<Integer>[10]
no se compilará, lea ¿por qué no podemos crear un array genérico?.
Eso es todo para los genéricos en Java, los genéricos en Java son un tema realmente vasto y requieren mucho tiempo para entenderlos y usarlos de manera efectiva. Esta publicación aquí es un intento de proporcionar detalles básicos sobre los genéricos y cómo podemos usarlos para mejorar nuestro programa con seguridad de tipos.
Source:
https://www.digitalocean.com/community/tutorials/java-generics-example-method-class-interface