CodeGym/Blog Java/Random-ES/Java Generics: cómo usar corchetes angulares en la prácti...
Autor
Artem Divertitto
Senior Android Developer at United Tech

Java Generics: cómo usar corchetes angulares 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.

¿Qué son los genéricos en Java?

Los genéricos son el mecanismo especial de Java para implementar la programación genérica: una forma de describir datos y algoritmos que le permite trabajar con diferentes tipos de datos sin cambiar la descripción de los algoritmos. El sitio web de Oracle tiene un tutorial separado dedicado a los genéricos: " Lección ". Para comprender los genéricos, primero debe averiguar por qué son necesarios y qué ofrecen. La sección "¿ Por qué usar genéricos? " del tutorial dice que un par de propósitos son una verificación de tipo más fuerte en tiempo de compilación y la eliminación de la necesidad de conversiones explícitas. Genéricos en Java: cómo usar paréntesis angulares en la práctica - 1Preparémonos para algunas pruebas en nuestro amado compilador de Java en línea Tutorialspoint . Supongamos que tiene el siguiente 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á perfectamente bien. Pero, ¿qué pasa si el jefe viene a nosotros y nos dice que "¡Hola, mundo!" es una frase usada en exceso y que debe devolver solo "Hola"? Eliminaremos el código que concatena ", ¡mundo!" Esto parece bastante inofensivo, ¿verdad? Pero en realidad recibimos un error EN TIEMPO DE COMPILACIÓN:
error: incompatible types: Object cannot be converted to String
El problema es que en nuestra Lista se almacenan Objetos. String es un descendiente de Object (ya que todas las clases de Java heredan implícitamente Object ), lo que significa que necesitamos una conversión explícita, pero no la agregamos. Durante la operación de concatenación, se llamará al método estático String.valueOf(obj) utilizando el objeto. Eventualmente, llamará al método toString de la clase Object . En otras palabras, nuestra Lista contiene un Objeto . Esto significa que siempre que necesitemos un tipo específico (no Object ), tendremos que hacer 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, debido a que List toma objetos, puede almacenar no solo String s, sino también Integer s. Pero lo peor es que el compilador no ve nada malo aquí. Y ahora obtendremos un error EN TIEMPO DE EJECUCIÓN (conocido como "error de tiempo de ejecución"). El error será:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Debes estar de acuerdo en que esto no es muy bueno. Y todo ello porque el compilador no es una inteligencia artificial capaz de adivinar siempre correctamente la intención del programador. Java SE 5 introdujo genéricos para permitirnos informar al compilador sobre nuestras intenciones, sobre qué tipos vamos a usar. Arreglamos nuestro código 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 puede ver, ya no necesitamos una conversión a String . Además, tenemos corchetes angulares que rodean el argumento de tipo. Ahora el compilador no nos permitirá compilar la clase hasta que eliminemos la línea que agrega 123 a la lista, ya que este es un Integer . Y nos lo dirá. Mucha gente llama a los genéricos "azúcar sintáctico". Y tienen razón, ya que después de compilar los genéricos, realmente se convierten en conversiones del mismo tipo. Veamos el código de bytes de las clases compiladas: una que usa una conversión explícita y otra que usa genéricos: Genéricos en Java: cómo usar paréntesis angulares en la práctica - 2después de la compilación, todos los genéricos se borran. Esto se llama " borrado de tipo". El borrado de tipos y los genéricos están diseñados para ser compatibles con versiones anteriores de JDK y, al mismo tiempo, permitir que el compilador ayude con las definiciones de tipos en las nuevas versiones de Java.

tipos crudos

Hablando de genéricos, siempre tenemos dos categorías: tipos parametrizados y tipos sin procesar. Los tipos sin procesar son tipos que omiten la "aclaración de tipo" entre paréntesis angulares: Genéricos en Java: cómo usar paréntesis angulares en la práctica - 3los tipos parametrizados, por otro lado, incluyen una "aclaración": Genéricos en Java: cómo usar paréntesis angulares en la práctica - 4como puede ver, usamos una construcción inusual, marcada con una flecha en la captura de pantalla. Esta es una sintaxis especial que se agregó a Java SE 7. Se llama " diamante ". ¿Por qué? Los paréntesis angulares forman un rombo: <> . También debe saber que la sintaxis del diamante está asociada con el concepto de " inferencia de tipo ". Después de todo, el compilador, viendo <>a la derecha, mira el lado izquierdo del operador de asignación, donde encuentra el tipo de variable cuyo valor se está asignando. Basándose en lo que encuentra en esta parte, entiende el tipo del valor de la derecha. De hecho, si se da un tipo genérico a la izquierda, pero no a la derecha, el compilador puede 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);
	}
}
Pero esto mezcla el nuevo estilo con genéricos y el viejo estilo sin ellos. Y esto es altamente indeseable. Al compilar el código anterior, recibimos el siguiente mensaje:
Note: HelloWorld.java uses unchecked or unsafe operations
De hecho, la razón por la que incluso necesita agregar un diamante aquí parece incomprensible. Pero aquí hay 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);
	}
}
Recordará que ArrayList tiene un segundo constructor que toma una colección como argumento. Y aquí es donde se esconde algo siniestro. Sin la sintaxis de diamante, el compilador no entiende que está siendo engañado. Con la sintaxis de diamante, lo hace. Entonces, la Regla #1 es: siempre use la sintaxis de diamante con tipos parametrizados. De lo contrario, corremos el riesgo de perder dónde estamos usando tipos sin formato. Para eliminar las advertencias de "utiliza operaciones no verificadas o no seguras", podemos usar la anotación @SuppressWarnings ("no verificada") en un método o clase. Pero piensa por qué has decidido usarlo. Recuerda la regla número uno. Tal vez necesite agregar un argumento de tipo.

Métodos genéricos de Java

Los genéricos le permiten crear métodos cuyos tipos de parámetros y tipo de devolución están parametrizados. Se dedica una sección separada a esta capacidad en el tutorial de Oracle: " Métodos genéricos ". Es importante recordar la sintaxis enseñada en este tutorial:
  • incluye una lista de parámetros de tipo entre paréntesis angulares;
  • la lista de parámetros de tipo va antes del tipo de retorno del método.
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 , verá que tiene dos métodos genéricos. Gracias a la posibilidad de inferencia de tipos, podemos indicar el tipo directamente al compilador o especificarlo nosotros mismos. Ambas opciones se presentan en el ejemplo. Por cierto, la sintaxis tiene mucho sentido si lo piensas bien. Cuando declaramos un método genérico, especificamos el parámetro de tipo ANTES del método, porque si declaramos el parámetro de tipo después del método, la JVM no podría determinar qué tipo usar. En consecuencia, primero declaramos que usaremos el parámetro de tipo T y luego decimos que vamos a devolver este tipo. Naturalmente, Util.<Integer>getValue(element, String.class) fallará con un error:tipos incompatibles: Class<String> no se puede convertir a Class<Integer> . Cuando utilice métodos genéricos, siempre debe recordar el borrado de tipos. 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);
		}
    }
}
Esto funcionará bien. Pero solo mientras el compilador comprenda que el tipo de retorno del método que se llama es Integer . Reemplace la declaración de salida de la consola con la siguiente línea:
System.out.println(Util.getValue(element) + 1);
Obtenemos un error:
bad operand types for binary operator '+', first type: Object, second type: int.
En otras palabras, se ha producido un borrado de tipos. El compilador ve que nadie ha especificado el tipo, por lo que el tipo se indica como Objeto y el método falla con un error.

Clases genéricas

No solo se pueden parametrizar métodos. Las clases también pueden. La sección "Tipos genéricos" del tutorial de Oracle está dedicada a esto. Consideremos 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);
		}
	}
}
Todo es simple aquí. Si usamos la clase genérica, el parámetro de tipo se indica después del nombre de la clase. Ahora vamos a crear 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);
}
Este código funcionará bien. El compilador ve que hay una Lista de números y una Colección de cadenas . Pero, ¿y si eliminamos el parámetro de tipo y hacemos esto?
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Obtenemos un error:
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Nuevamente, esto es borrado de tipos. Dado que la clase ya no usa un parámetro de tipo, el compilador decide que, dado que pasamos una List , el método con List<Integer> es el más apropiado. Y fallamos con un error. Por lo tanto, tenemos la Regla #2: si tiene una clase genérica, siempre especifique los parámetros de tipo.

Restricciones

Podemos restringir los tipos especificados en métodos genéricos y clases. Por ejemplo, supongamos que queremos que un contenedor acepte solo un número como argumento de tipo. Esta función se describe en la sección Parámetros de tipo acotado del tutorial de Oracle. 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 restringido el parámetro de tipo a la clase/interfaz Number o sus descendientes. Tenga en cuenta que puede especificar no solo una clase, sino también interfaces. Por ejemplo:
public static class NumberContainer<T extends Number & Comparable> {
Los genéricos también admiten comodines. Se dividen en tres tipos: Su uso de comodines debe cumplir con el principio Get-Put . Se puede expresar de la siguiente manera:
  • Use un comodín de extensión cuando solo obtenga valores de una estructura.
  • Use un súper comodín cuando solo coloque valores en una estructura.
  • Y no use un comodín cuando quiera obtener y poner desde/hacia una estructura.
Este principio también se conoce como el principio Producer Extends Consumer Super (PECS). Aquí hay un pequeño ejemplo del código fuente del método Collections.copy de Java : Genéricos en Java: cómo usar paréntesis angulares en la práctica - 5Y aquí hay un pequeño ejemplo de lo que 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 reemplaza extends con super , entonces todo está bien. Debido a que completamos la lista con un valor antes de mostrar su contenido, es un consumidor . En consecuencia, usamos super.

Herencia

Los genéricos tienen otra característica interesante: la herencia. La forma en que funciona la herencia para los genéricos se describe en " Genéricos, herencia y subtipos " en el tutorial de Oracle. Lo importante es recordar y reconocer lo siguiente. No podemos hacer esto:
List<CharSequence> list1 = new ArrayList<String>();
Porque la herencia funciona de manera diferente con los genéricos: Genéricos en Java: cómo usar paréntesis angulares en la práctica - 6Y aquí hay otro buen ejemplo que fallará con un error:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Una vez más, todo es simple aquí. List<String> no es descendiente de List<Object> , aunque String es descendiente de Object . Para reforzar lo que aprendió, le sugerimos que vea una lección en video de nuestro Curso de Java

Conclusión

Así que hemos refrescado nuestra memoria con respecto a los genéricos. Si rara vez aprovecha al máximo sus capacidades, algunos de los detalles se vuelven borrosos. Espero que esta breve reseña te haya ayudado a refrescarte la memoria. Para obtener resultados aún mejores, le recomiendo enfáticamente que se familiarice con el siguiente material:
Comentarios
  • Populares
  • Nuevas
  • Antiguas
Debes iniciar sesión para dejar un comentario
Esta página aún no tiene comentarios