Introducción
A partir de JSE 5.0, se agregaron genéricos al arsenal del lenguaje Java.¿Qué son los genéricos en Java?
Los genéricos (generalizaciones) son medios especiales del lenguaje Java para implementar programación generalizada: un enfoque especial para describir datos y algoritmos que le permite trabajar con diferentes tipos de datos sin cambiar su descripción. En el sitio web de Oracle, hay un tutorial separado dedicado a los genéricos: " Lección: Genéricos ".
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello");
String text = list.get(0) + ", world!";
System.out.print(text);
}
}
Este código funcionará bien. Pero, ¿y si vinieran a nosotros y nos dijeran que la frase "¡Hola, mundo!" golpeado y solo puedes regresar Hola? Eliminemos la concatenación con la cadena del código ", world!"
. Parecería que ¿qué podría ser más inofensivo? Pero, de hecho, recibiremos un error DURANTE LA COMPILACIÓN : error: incompatible types: Object cannot be converted to String
El caso es que en nuestro caso Lista almacena una lista de objetos de tipo Objeto. Dado que String es descendiente de Object (ya que todas las clases se heredan implícitamente de Object en Java), requiere una conversión explícita, lo cual no hicimos. Y al concatenar, se llamará al método estático String.valueOf(obj) en el objeto, que finalmente llamará al método toString en el Objeto. Es decir, nuestra Lista contiene Objeto. Resulta que cuando necesitemos un tipo específico, y no un Objeto, tendremos que realizar la conversión de tipos nosotros mismos:
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println((String)str);
}
}
}
Sin embargo, en este caso, porque List acepta una lista de objetos, almacena no solo String, sino también Integer. Pero lo peor es que en este caso el compilador no verá nada malo. Y aquí recibiremos un error DURANTE LA EJECUCIÓN (también dicen que el error se recibió “en Runtime”). El error será: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
De acuerdo, no es el más agradable. Y todo esto se debe a que el compilador no es inteligencia artificial y no puede adivinar todo lo que quiere decir el programador. Para decirle al compilador más sobre qué tipos vamos a utilizar, Java SE 5 introdujo los genéricos . Corrijamos nuestra versión diciéndole al compilador lo que queremos:
import java.util.*;
public class HelloWorld {
public static void main(String []args){
List<String> list = new ArrayList<>();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println(str);
}
}
}
Como podemos ver, ya no necesitamos la conversión a String. Además, ahora contamos con corchetes angulares que enmarcan los genéricos. Ahora el compilador no permitirá que se compile la clase hasta que eliminemos la adición de 123 a la lista, porque este es un número entero. Él nos lo dirá. Mucha gente llama a los genéricos "azúcar sintáctico". Y tienen razón, ya que los genéricos se convertirán en esas mismas castas cuando se recopilen. Veamos el código de bytes de las clases compiladas: con conversión manual y uso de genéricos:
Tipos crudos o tipos crudos
Cuando hablamos de genéricos, siempre tenemos dos categorías: tipos escritos (Tipos genéricos) y tipos “sin formato” (Tipos sin formato). Los tipos sin formato son tipos sin especificar la “calificación” entre paréntesis angulares:<>
la sintaxis del diamante también está asociada con el concepto de " Inferencia de tipos ", o inferencia de tipos. Después de todo, el compilador, al ver <> a la derecha, mira hacia el lado izquierdo, donde se encuentra la declaración del tipo de variable a la que se le asigna el valor. Y a partir de esta parte comprende de qué tipo se escribe el valor de la derecha. De hecho, si se especifica un genérico en el lado izquierdo y no en el lado derecho, el compilador podrá inferir el tipo:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = new ArrayList();
list.add("Hello World");
String data = list.get(0);
System.out.println(data);
}
}
Sin embargo, esto sería una mezcla del nuevo estilo con genéricos y el estilo antiguo sin ellos. Y esto es extremadamente indeseable. Al compilar el código anterior recibiremos el mensaje: Note: HelloWorld.java uses unchecked or unsafe operations
. De hecho, no parece claro por qué necesitamos agregar diamantes aquí. Pero he aquí un ejemplo:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = Arrays.asList("Hello", "World");
List<Integer> data = new ArrayList(list);
Integer intNumber = data.get(0);
System.out.println(data);
}
}
Como recordamos, ArrayList también tiene un segundo constructor que toma una colección como entrada. Y aquí es donde reside el engaño. Sin la sintaxis de Diamond, el compilador no entiende que está siendo engañado, pero con Diamond sí. Por lo tanto, regla número 1 : usar siempre sintaxis de diamante si usamos tipos escritos. De lo contrario, corremos el riesgo de perdernos donde usamos el tipo sin formato. Para evitar advertencias en el registro que "utiliza operaciones no comprobadas o inseguras", puede especificar una anotación especial en el método o clase que se utiliza: @SuppressWarnings("unchecked")
Suprimir se traduce como suprimir, es decir, literalmente, suprimir advertencias. Pero piensa por qué decidiste indicarlo. Recuerde la regla número uno y tal vez necesite agregar escritura.
Métodos genéricos
Los genéricos le permiten escribir métodos. Hay una sección separada dedicada a esta característica en el tutorial de Oracle: " Métodos genéricos ". De este tutorial, es importante recordar la sintaxis:- incluye una lista de parámetros escritos entre corchetes angulares;
- la lista de parámetros escritos va antes del método devuelto.
import java.util.*;
public class HelloWorld{
public static class Util {
public static <T> T getValue(Object obj, Class<T> clazz) {
return (T) obj;
}
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList("Author", "Book");
for (Object element : list) {
String data = Util.getValue(element, String.class);
System.out.println(data);
System.out.println(Util.<String>getValue(element));
}
}
}
Si observa la clase Util, vemos dos métodos escritos en ella. Con la inferencia de tipos, podemos proporcionar la definición de tipo directamente al compilador o podemos especificarla nosotros mismos. Ambas opciones se presentan en el ejemplo. Por cierto, si lo piensas bien, la sintaxis es bastante lógica. Al escribir un método, especificamos el genérico ANTES del método porque si usamos el genérico después del método, Java no podrá determinar qué tipo usar. Por lo tanto, primero anunciamos que usaremos T genérico y luego decimos que vamos a devolver este genérico. Naturalmente, Util.<Integer>getValue(element, String.class)
fallará con un error incompatible types: Class<String> cannot be converted to Class<Integer>
. Cuando utilice métodos escritos, siempre debe recordar el borrado de textos. Veamos un ejemplo:
import java.util.*;
public class HelloWorld {
public static class Util {
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList(2, 3);
for (Object element : list) {
System.out.println(Util.<Integer>getValue(element) + 1);
}
}
}
Funcionará muy bien. Pero solo siempre que el compilador comprenda que el método llamado tiene un tipo entero. Reemplacemos la salida de la consola con la siguiente línea: System.out.println(Util.getValue(element) + 1);
Y obtenemos el error: tipos de operandos incorrectos para el operador binario '+', primer tipo: Objeto, segundo tipo: int Es decir, los tipos se han borrado. El compilador ve que nadie ha especificado el tipo, el tipo se especifica como Objeto y la ejecución del código falla con un error.
Tipos genéricos
Puede escribir no sólo métodos, sino también clases en sí. Oracle tiene una sección de " Tipos genéricos " dedicada a esto en su guía. Veamos un ejemplo:public static class SomeType<T> {
public <E> void test(Collection<E> collection) {
for (E element : collection) {
System.out.println(element);
}
}
public void test(List<Integer> collection) {
for (Integer element : collection) {
System.out.println(element);
}
}
}
Aquí todo es sencillo. Si usamos una clase, el genérico aparece después del nombre de la clase. Ahora creemos una instancia de esta clase en el método principal:
public static void main(String []args) {
SomeType<String> st = new SomeType<>();
List<String> list = Arrays.asList("test");
st.test(list);
}
Funcionará bien. El compilador ve que hay una Lista de números y una Colección de tipo Cadena. Pero ¿y si borramos los genéricos y hacemos esto?
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Recibiremos el error: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
escriba borrar nuevamente. Como la clase ya no tiene un genérico, el compilador decide que, dado que pasamos una Lista, un método con Lista<Integer> es más apropiado. Y caemos con un error. Por lo tanto, regla número 2: si se escribe una clase, especifique siempre el tipo en el formato genérico .
Restricciones
Podemos aplicar una restricción a los tipos especificados en genéricos. Por ejemplo, queremos que el contenedor acepte solo Número como entrada. Esta característica se describe en el Tutorial de Oracle en la sección Parámetros de tipo delimitado . Veamos un ejemplo:import java.util.*;
public class HelloWorld{
public static class NumberContainer<T extends Number> {
private T number;
public NumberContainer(T number) { this.number = number; }
public void print() {
System.out.println(number);
}
}
public static void main(String []args) {
NumberContainer number1 = new NumberContainer(2L);
NumberContainer number2 = new NumberContainer(1);
NumberContainer number3 = new NumberContainer("f");
}
}
Como puede ver, hemos limitado el tipo genérico para que sea la clase/interfaz Número y sus descendientes. Curiosamente, puedes especificar no sólo una clase, sino también interfaces. Por ejemplo: public static class NumberContainer<T extends Number & Comparable> {
Los genéricos también tienen el concepto de comodín https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html Estos, a su vez, se dividen en tres tipos:
- Comodines de límite superior: < ? extiende Número >
- Comodines ilimitados - < ? >
- Comodines de límite inferior - < ? súper entero >
public static class TestClass {
public static void print(List<? extends String> list) {
list.add("Hello World!");
System.out.println(list.get(0));
}
}
public static void main(String []args) {
List<String> list = new ArrayList<>();
TestClass.print(list);
}
Pero si reemplazas extends con super, todo estará bien. Dado que llenamos la lista con un valor antes de generarlo, es un consumidor para nosotros, es decir, un consumidor. Por eso utilizamos super.
Herencia
Hay otra característica inusual de los genéricos: su herencia. La herencia de genéricos se describe en el tutorial de Oracle en la sección " Genéricos, herencia y subtipos ". Lo principal es recordar y darse cuenta de lo siguiente. No podemos hacer esto:List<CharSequence> list1 = new ArrayList<String>();
Porque la herencia funciona de manera diferente con los genéricos:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Aquí también todo es sencillo. List<String> no es descendiente de List<Object>, aunque String es descendiente de Object.
Final
Así que refrescamos nuestra memoria sobre los genéricos. Si rara vez se utilizan en todo su poder, algunos detalles se olvidan. Espero que esta breve reseña ayude a refrescar tu memoria. Y para obtener mejores resultados, le recomiendo encarecidamente que lea los siguientes materiales:- Yuri Tkach: Tipos sin procesar - Genéricos n.° 1 - Java avanzado
- Herencia y extensores genéricos - Genéricos #2 - Java avanzado
- Extensión de tipo recursivo - Genéricos n.º 3 - Java avanzado
- Alexander Matorin - Genéricos no obvios
- Introducción a Java. Genéricos. Comodines | Tecnocorriente
- O'Reilly: genéricos y colecciones de Java
GO TO FULL VERSION