icon ejemplo definicion java generics compiler-errors java-8

ejemplo - ¿Por qué este código genérico se compila en Java 8?



jtextpane definicion (3)

No sé por qué esta compilación. Por otro lado, puedo explicar cómo puede aprovechar al máximo las comprobaciones en tiempo de compilación.

Entonces, newList() es un método genérico, tiene un parámetro de tipo. Si especifica este parámetro, el compilador lo comprobará por usted:

No se puede compilar:

String s = Main.<String>newList(); // this doesn''t compile anymore System.out.println(s);

Pasa el paso de compilación:

List<Integer> l = Main.<ArrayList<Integer>>newList(); // this compiles and works well System.out.println(l);

Especificar el parámetro de tipo

Los parámetros de tipo proporcionan solo verificación en tiempo de compilación. Esto es así por diseño, Java utiliza el borrado de tipos para los tipos genéricos. Para que el compilador funcione para usted, debe especificar esos tipos en el código.

Tipo de parámetro en la creación de instancia

El caso más común es especificar los patrones para una instancia de objeto. Es decir, para listas:

List<String> list = new ArrayList<>();

Aquí podemos ver que List<String> especifica el tipo de los elementos de la lista. Por otro lado, la nueva ArrayList<>() no lo hace. Utiliza el operador de diamante en su lugar. Es decir, el compilador de Java infers el tipo basado en la declaración.

Parámetro de tipo implícito en la invocación del método

Cuando invocas un método estático, debes especificar el tipo de otra manera. A veces puede especificarlo como parámetro:

public static <T extends Number> T max(T n1, T n2) { if (n1.doubleValue() < n2.doubleValue()) { return n2; } return n1; }

Puedes usarlo así:

int max = max(3, 4); // implicit param type: Integer

O así:

double max2 = max(3.0, 4.0); // implicit param type: Double

Parámetros de tipo explícito en la invocación del método:

Digamos, por ejemplo, que así es como puede crear una lista vacía de tipo seguro:

List<Integer> noIntegers = Collections.<Integer>emptyList();

El parámetro tipo <Integer> se pasa al método emptyList() . La única restricción es que también debe especificar la clase. Es decir, no puedes hacer esto:

import static java.util.Collections.emptyList; ... List<Integer> noIntegers = <Integer>emptyList(); // this won''t compile

Token de tipo de tiempo de ejecución

Si ninguno de estos trucos puede ayudarte, entonces puedes especificar un token de tipo de tiempo de ejecución . Es decir, proporciona una clase como parámetro. Un ejemplo común es el EnumMap :

private static enum Letters {A, B, C}; // dummy enum ... public static void main(String[] args) { Map<Letters, Integer> map = new EnumMap<>(Letters.class); }

Me topé con un código que me hizo preguntarme por qué se compila con éxito:

public class Main { public static void main(String[] args) { String s = newList(); // why does this line compile? System.out.println(s); } private static <T extends List<Integer>> T newList() { return (T) new ArrayList<Integer>(); } }

Lo interesante es que si newList la firma del método newList con <T extends ArrayList<Integer>> ya no funciona.

Actualización después de comentarios y respuestas: si muevo el tipo genérico del método a la clase, el código ya no se compila:

public class SomeClass<T extends List<Integer>> { public void main(String[] args) { String s = newList(); // this doesn''t compile anymore System.out.println(s); } private T newList() { return (T) new ArrayList<Integer>(); } }


Si declara un parámetro de tipo en un método, está permitiendo que la persona que llama elija un tipo real para él, siempre que ese tipo real cumpla con las restricciones. Ese tipo no tiene que ser un tipo concreto real, podría ser un tipo abstracto, una variable de tipo o un tipo de intersección, en otras palabras más coloquiales, un tipo hipotético. Entonces, como dijo Mureinik , podría haber un tipo que extienda String e implemente List . No podemos especificar manualmente un tipo de intersección para la invocación, pero podemos usar una variable de tipo para demostrar la lógica:

public class Main { public static <X extends String&List<Integer>> void main(String[] args) { String s = Main.<X>newList(); System.out.println(s); } private static <T extends List<Integer>> T newList() { return (T) new ArrayList<Integer>(); } }

Por supuesto, newList() no puede cumplir la expectativa de devolver dicho tipo, pero ese es el problema de la definición (o implementación) de este método. Debería recibir una advertencia "sin marcar" al ArrayList en T La única implementación correcta posible sería devolver null aquí, lo que hace que el método sea bastante inútil.

El punto, para repetir la declaración inicial, es que la persona que llama de un método genérico elige los tipos reales para los parámetros de tipo. Por el contrario, cuando declaras una clase genérica como con

public class SomeClass<T extends List<Integer>> { public void main(String[] args) { String s = newList(); // this doesn''t compile anymore System.out.println(s); } private T newList() { return (T) new ArrayList<Integer>(); } }

el parámetro de tipo es parte del contrato de la clase, por lo que quien crea una instancia elegirá los tipos reales para esa instancia. El método de instancia main es parte de esa clase y tiene que obedecer ese contrato. No puedes elegir la T que quieres; se ha establecido el tipo real para T y en Java, por lo general, ni siquiera se puede saber qué es T

El punto clave de la programación genérica es escribir código que funcione independientemente de los tipos reales que se hayan elegido para los parámetros de tipo.

Pero tenga en cuenta que puede crear otra instancia independiente con el tipo que desee e invocar el método, p. Ej.

public class SomeClass<T extends List<Integer>> { public <X extends String&List<Integer>> void main(String[] args) { String s = new SomeClass<X>().newList(); System.out.println(s); } private T newList() { return (T) new ArrayList<Integer>(); } }

Aquí, el creador de la nueva instancia elige los tipos reales para esa instancia. Como se dijo, ese tipo real no necesita ser un tipo concreto.


Supongo que esto se debe a que List es una interfaz. Si ignoramos el hecho de que String es final por un segundo, podría, en teoría, tener una clase que extends String (lo que significa que podría asignarlo a s ) pero implements List<Integer> (lo que significa que podría devolverse de newList() ) Una vez que cambia el tipo de retorno de una interfaz ( T extends List ) a una clase concreta ( T extends ArrayList ), el compilador puede deducir que no se pueden asignar entre sí y produce un error.

Esto, por supuesto, se rompe ya que String es, de hecho, final , y podríamos esperar que el compilador tenga esto en cuenta. En mi humilde opinión, es un error, aunque debo admitir que no soy un experto en compilación y podría haber una buena razón para ignorar el modificador final en este momento.