JavaRush /Blog Java /Random-ES /Genéricos para gatos
Viacheslav
Nivel 3

Genéricos para gatos

Publicado en el grupo Random-ES
Genéricos para gatos - 1

Introducción

Hoy es un gran día para recordar lo que sabemos sobre Java. Según el documento más importante, es decir. Especificación del lenguaje Java (JLS - Java Language Specifiaction), Java es un lenguaje fuertemente tipado, como se describe en el capítulo " Capítulo 4. Tipos, valores y variables ". ¿Qué quiere decir esto? Digamos que tenemos un método principal:
public static void main(String[] args) {
String text = "Hello world!";
System.out.println(text);
}
La tipificación segura garantiza que cuando se compila este código, el compilador verificará que si especificamos el tipo de variable de texto como Cadena, entonces no estamos intentando usarla en ningún lugar como una variable de otro tipo (por ejemplo, como un Entero). . Por ejemplo, si intentamos guardar un valor en lugar de texto 2L(es decir, largo en lugar de cadena), obtendremos un error en el momento de la compilación:

Main.java:3: error: incompatible types: long cannot be converted to String
String text = 2L;
Aquellos. La escritura segura le permite garantizar que las operaciones en objetos se realicen solo cuando esas operaciones sean legales para esos objetos. Esto también se llama seguridad de tipos. Como se indica en JLS, existen dos categorías de tipos en Java: tipos primitivos y tipos de referencia. Puede recordar los tipos primitivos del artículo de revisión: " Tipos primitivos en Java: no son tan primitivos ". Los tipos de referencia se pueden representar mediante una clase, interfaz o matriz. Y hoy nos interesarán los tipos de referencia. Y comencemos con las matrices:
class Main {
  public static void main(String[] args) {
    String[] text = new String[5];
    text[0] = "Hello";
  }
}
Este código se ejecuta sin errores. Como sabemos (por ejemplo, del " Tutorial de Oracle Java: matrices "), una matriz es un contenedor que almacena datos de un solo tipo. En este caso, sólo líneas. Intentemos agregar long a la matriz en lugar de String:
text[1] = 4L;
Ejecutemos este código (por ejemplo, en Repl.it Online Java Compiler ) y obtendremos un error:
error: incompatible types: long cannot be converted to String
La matriz y la seguridad de tipos del lenguaje no nos permitieron guardar en una matriz lo que no se ajustaba al tipo. Esta es una manifestación de seguridad de tipos. Nos dijeron: "Corrija el error, pero hasta entonces no compilaré el código". Y lo más importante de esto es que esto sucede en el momento de la compilación y no cuando se inicia el programa. Es decir, vemos errores inmediatamente y no “algún día”. Y como recordamos las matrices, recordemos también el marco de colecciones de Java . Allí teníamos diferentes estructuras. Por ejemplo, listas. Reescribamos el ejemplo:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List text = new ArrayList(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(0);
  }
}
Al compilarlo recibiremos testun error en la línea de inicialización de la variable:
incompatible types: Object cannot be converted to String
En nuestro caso, List puede almacenar cualquier objeto (es decir, un objeto de tipo Objeto). Por lo tanto, el compilador dice que no puede asumir tal carga de responsabilidad. Por lo tanto, necesitamos especificar explícitamente el tipo que obtendremos de la lista:
String test = (String) text.get(0);
Esta indicación se llama conversión de tipos o conversión de tipos. Y todo funcionará bien ahora hasta que intentemos obtener el elemento en el índice 1, porque es de tipo Largo. Y obtendremos un error justo, pero ya mientras el programa se está ejecutando (en Runtime):

type conversion, typecasting
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
Como podemos ver, aquí hay varias desventajas importantes. En primer lugar, nos vemos obligados a "transmitir" el valor obtenido de la lista a la clase String. De acuerdo, esto es feo. En segundo lugar, en caso de error, lo veremos sólo cuando se ejecute el programa. Si nuestro código fuera más complejo, es posible que no detectáramos inmediatamente dicho error. Y los desarrolladores comenzaron a pensar en cómo hacer que el trabajo en tales situaciones sea más fácil y el código más claro. Y nacieron: genéricos.
Genéricos para gatos - 2

Genéricos

Entonces, genéricos. ¿Qué es? Un genérico es una forma especial de describir los tipos utilizados, que el compilador de código puede utilizar en su trabajo para garantizar la seguridad de los tipos. Se parece a esto:
Genéricos para gatos - 3
A continuación se muestra un breve ejemplo y una explicación:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> text = new ArrayList<String>(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(1);
  }
}
En este ejemplo, decimos que no solo tenemos List, sino Listque SÓLO funciona con objetos de tipo Cadena. Y ningún otro. Lo que se acaba de indicar entre paréntesis, lo podemos almacenar. Estos "corchetes" se denominan "corchetes angulares", es decir paréntesis angulares. El compilador tendrá la amabilidad de comprobar por nosotros si hemos cometido algún error al trabajar con una lista de cadenas (la lista se llama texto). El compilador verá que estamos intentando descaradamente poner Long en la lista de cadenas. Y en el momento de la compilación dará un error:
error: no suitable method found for add(long)
Quizás hayas recordado que String es descendiente de CharSequence. Y decide hacer algo como:
public static void main(String[] args) {
	ArrayList<CharSequence> text = new ArrayList<String>(5);
	text.add("Hello");
	String test = text.get(0);
}
Pero esto no es posible y obtendremos el error: error: incompatible types: ArrayList<String> cannot be converted to ArrayList<CharSequence> Parece extraño, porque. la línea CharSequence sec = "test";no contiene errores. Vamos a resolverlo. Dicen sobre este comportamiento: "Los genéricos son invariantes". ¿Qué es una "invariante"? Me gusta cómo se dice sobre esto en Wikipedia en el artículo “ Covarianza y contravarianza ”:
Genéricos para gatos - 4
Por tanto, la invariancia es la ausencia de herencia entre tipos derivados. Si Cat es un subtipo de Animals, entonces Set<Cats> no es un subtipo de Set<Animals> y Set<Animals> no es un subtipo de Set<Cats>. Por cierto, vale la pena decir que a partir de Java SE 7 apareció el llamado " Operador Diamante ". Porque los dos corchetes angulares <> son como un diamante. Esto nos permite utilizar genéricos como este:
public static void main(String[] args) {
  List<String> lines = new ArrayList<>();
  lines.add("Hello world!");
  System.out.println(lines);
}
Con base en este código, el compilador entiende que si indicamos en el lado izquierdo que contendrá Listobjetos de tipo String, entonces en el lado derecho queremos decir que queremos guardar linesuna nueva ArrayList en una variable, que también almacenará un objeto. del tipo especificado en el lado izquierdo. Entonces, el compilador del lado izquierdo comprende o infiere el tipo del lado derecho. Es por eso que este comportamiento se llama inferencia de tipos o "Type Inference" en inglés. Otra cosa interesante que vale la pena destacar son los tipos RAW o “tipos sin formato”. Porque Los genéricos no siempre han existido, y Java intenta mantener la compatibilidad con versiones anteriores siempre que sea posible, luego los genéricos se ven obligados a trabajar de alguna manera con código donde no se especifica ningún genérico. Veamos un ejemplo:
List<CharSequence> lines = new ArrayList<String>();
Como recordamos, dicha línea no se compilará debido a la invariancia de los genéricos.
List<Object> lines = new ArrayList<String>();
Y éste tampoco se compilará, por la misma razón.
List lines = new ArrayList<String>();
List<String> lines2 = new ArrayList();
Estas líneas se compilarán y funcionarán. Es en ellos donde se utilizan los tipos crudos, es decir. tipos no especificados. Una vez más, vale la pena señalar que los tipos sin formato NO DEBEN usarse en el código moderno.
Genéricos para gatos - 5

clases mecanografiadas

Entonces, clases mecanografiadas. Veamos cómo podemos escribir nuestra propia clase mecanografiada. Por ejemplo, tenemos una jerarquía de clases:
public static abstract class Animal {
  public abstract void voice();
}

public static class Cat extends Animal {
  public void voice(){
    System.out.println("Meow meow");
  }
}

public static class Dog extends Animal {
  public void voice(){
    System.out.println("Woof woof");
  }
}
Queremos crear una clase que implemente un contenedor de animales. Sería posible escribir una clase que contuviera cualquier archivo Animal. Esto es simple, comprensible, PERO... mezclar perros y gatos es malo, no son amigos entre sí. Además, si alguien recibe un contenedor de este tipo, puede arrojar por error gatos del contenedor a una jauría de perros... y esto no conducirá a ningún beneficio. Y aquí los genéricos nos ayudarán. Por ejemplo, escribamos la implementación así:
public static class Box<T> {
  List<T> slots = new ArrayList<>();
  public List<T> getSlots() {
    return slots;
  }
}
Nuestra clase trabajará con objetos de tipo especificado por un genérico llamado T. Este es un tipo de alias. Porque El genérico se especifica en el nombre de la clase, luego lo recibiremos al declarar la clase:
public static void main(String[] args) {
  Box<Cat> catBox = new Box<>();
  Cat murzik = new Cat();
  catBox.getSlots().add(murzik);
}
Como vemos, indicamos que tenemos Box, que solo funciona con Cat. El compilador se dio cuenta de que catBoxen lugar de un genérico, Tes necesario sustituir el tipo Catdondequiera que se especifique el nombre del genérico T:
Genéricos para gatos - 6
Aquellos. es gracias al Box<Cat>compilador que comprende lo que slotsdebería ser realmente List<Cat>. Porque Box<Dog>dentro habrá slots, conteniendo List<Dog>. Puede haber varios genéricos en una declaración de tipo, por ejemplo:
public static class Box<T, V> {
El nombre genérico puede ser cualquier cosa, aunque se recomienda seguir algunas reglas tácitas: "Convenciones de nomenclatura de parámetros de tipo": tipo de elemento - E, tipo de clave - K, tipo de número - N, T - para tipo, V - para tipo de valor . Por cierto, recuerda que dijimos que los genéricos son invariantes, es decir. no preservar la jerarquía de herencia. De hecho, podemos influir en esto. Es decir, tenemos la oportunidad de hacer que los genéricos sean COvariantes, es decir. manteniendo las herencias en el mismo orden. Este comportamiento se denomina "tipo delimitado", es decir. tipos limitados. Por ejemplo, nuestra clase Boxpodría contener todos los animales, entonces declararíamos un genérico como este:
public static class Box<T extends Animal> {
Es decir, establecemos el límite superior de la clase Animal. También podemos especificar varios tipos después de la palabra clave extends. Esto significará que el tipo con el que trabajaremos debe ser descendiente de alguna clase y al mismo tiempo implementar alguna interfaz. Por ejemplo:
public static class Box<T extends Animal & Comparable> {
En este caso, si intentamos poner Boxalgo que no sea heredero Animaly no se implemente Comparable, durante la compilación recibiremos un error:
error: type argument Cat is not within bounds of type-variable T
Genéricos para gatos - 7

Métodos de escritura

Los genéricos se utilizan no sólo en tipos, sino también en métodos individuales. La aplicación de los métodos se puede ver en el tutorial oficial: " Métodos Genéricos ".

Fondo:

Genéricos para gatos - 8
Miremos esta imagen. Como puede ver, el compilador mira la firma del método y ve que estamos tomando alguna clase indefinida como entrada. No determina por firma que estamos devolviendo algún tipo de objeto, es decir. Objeto. Por lo tanto, si queremos crear, digamos, una ArrayList, entonces debemos hacer esto:
ArrayList<String> object = (ArrayList<String>) createObject(ArrayList.class);
Tienes que escribir explícitamente que la salida será una ArrayList, lo cual es feo y agrega la posibilidad de cometer un error. Por ejemplo, podemos escribir esas tonterías y se compilará:
ArrayList object = (ArrayList) createObject(LinkedList.class);
¿Podemos ayudar al compilador? Sí, los genéricos nos permiten hacer esto. Veamos el mismo ejemplo:
Genéricos para gatos - 9
Entonces, podemos crear un objeto simplemente como este:
ArrayList<String> object = createObject(ArrayList.class);
Genéricos para gatos - 10

Comodín

Según el Tutorial sobre genéricos de Oracle, específicamente la sección " Comodines ", podemos describir un "tipo desconocido" con un signo de interrogación. Wildcard es una herramienta útil para mitigar algunas de las limitaciones de los genéricos. Por ejemplo, como comentamos anteriormente, los genéricos son invariantes. Esto significa que aunque todas las clases son descendientes (subtipos) del tipo Objeto, este List<любой тип>no es un subtipo List<Object>. PERO, List<любой тип>es un subtipo List<?>. Entonces podemos escribir el siguiente código:
public static void printList(List<?> list) {
  for (Object elem: list) {
    System.out.print(elem + " ");
  }
  System.out.println();
}
Al igual que los genéricos normales (es decir, sin el uso de comodines), los genéricos con comodines pueden ser limitados. El comodín del límite superior parece familiar:
public static void printCatList(List<? extends Cat> list) {
  for (Cat cat: list) {
    System.out.print(cat + " ");
  }
  System.out.println();
}
Pero también puedes limitarlo mediante el comodín de límite inferior:
public static void printCatList(List<? super Cat> list) {
Por lo tanto, el método comenzará a aceptar a todos los gatos, así como a los más altos en la jerarquía (hasta el Objeto).
Genéricos para gatos - 11

Tipo de borrado

Hablando de genéricos, vale la pena conocer el “borrado de tipos”. De hecho, el borrado de tipos se debe al hecho de que los genéricos son información para el compilador. Durante la ejecución del programa, no hay más información sobre los genéricos, esto se llama "borrar". Este borrado tiene el efecto de que el tipo genérico es reemplazado por el tipo específico. Si el genérico no tenía un límite, entonces se sustituirá por el tipo de Objeto. Si se especificó el borde (por ejemplo <T extends Comparable>), se sustituirá. Aquí hay un ejemplo del tutorial de Oracle: " Borrado de tipos genéricos ":
Genéricos para gatos - 12
Como se dijo anteriormente, en este ejemplo el genérico Tse borra hasta su borde, es decir. antes Comparable.
Genéricos para gatos - 13

Conclusión

Los genéricos son un tema muy interesante. Espero que este tema sea de tu interés. Para resumir, podemos decir que los genéricos son una excelente herramienta que los desarrolladores han recibido para solicitar al compilador información adicional para garantizar la seguridad de tipos por un lado y la flexibilidad por el otro. Y si estás interesado, te sugiero que consultes los recursos que me gustaron: #viacheslav
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION