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.