JavaRush /Blog Java /Random-ES /La teoría de los genéricos en Java o cómo poner los parén...
Viacheslav
Nivel 3

La teoría de los genéricos en Java o cómo poner los paréntesis en la práctica

Publicado en el grupo Random-ES

Introducción

A partir de JSE 5.0, se agregaron genéricos al arsenal del lenguaje Java.
La teoría de los genéricos en Java o como poner paréntesis en la práctica - 1

¿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 ".

En primer lugar, para comprender los genéricos, es necesario comprender por qué son necesarios y qué aportan. En el tutorial de la sección "¿ Por qué utilizar genéricos ?" Se dice que uno de los propósitos es una verificación de tipos más sólida en tiempo de compilación y la eliminación de la necesidad de una conversión explícita.
La teoría de los genéricos en Java o cómo poner los paréntesis en la práctica - 2
Preparemos nuestro compilador Java en línea tutorialspoint favorito para experimentos . Imaginemos este código:
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:
La teoría de los genéricos en Java o cómo poner los paréntesis en la práctica - 3
Después de la compilación, se borra cualquier información sobre genéricos. Esto se llama "Tipo de borrado" o " Tipo de borrado ". El borrado de tipos y los genéricos están diseñados para proporcionar compatibilidad con versiones anteriores del JDK y, al mismo tiempo, permitir que el compilador ayude con la inferencia de tipos en versiones más nuevas de Java.
La teoría de los genéricos en Java o cómo poner los paréntesis en la práctica - 4

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 teoría de los genéricos en Java o cómo poner los paréntesis en la práctica - 5
Los tipos mecanografiados son lo contrario, con la indicación "aclaración":
La teoría de los genéricos en Java o cómo poner los paréntesis en la práctica - 6
Como podemos ver, utilizamos un diseño inusual, marcado con una flecha en la captura de pantalla. Esta es una sintaxis especial que se agregó en Java SE 7 y se llama " el diamante ", que significa diamante. ¿Por qué? Puede establecer una analogía entre la forma de un diamante y la forma de las llaves: <> 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.
La teoría de los genéricos en Java o cómo poner los paréntesis en la práctica - 7

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.
Veamos un ejemplo:
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.
La teoría de los genéricos en Java o cómo poner los paréntesis en la práctica - 8

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: El llamado principio Get Put se aplica a los comodines . Se pueden expresar de la siguiente forma:
La teoría de los genéricos en Java o cómo poner los paréntesis en la práctica - 9
Este principio también se denomina principio PECS (Producer Extends Consumer Super). Puede leer más sobre Habré en el artículo " Uso de comodines genéricos para mejorar la usabilidad de la API de Java ", así como en la excelente discusión sobre stackoverflow: " Uso de comodines en Java genérico ". Aquí hay un pequeño ejemplo de la fuente de Java: el método Collections.copy:
La teoría de los genéricos en Java o como poner paréntesis en la práctica - 10
Bueno, un pequeño ejemplo de cómo NO funcionará:
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:
La teoría de los genéricos en Java o cómo poner los paréntesis en la práctica - 11
Y aquí hay otro buen ejemplo que fallará con un error:
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: #viacheslav
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION