qué - metodos genericos java
¿Por qué la inferencia de tipo genérico Java 8 elige esta sobrecarga? (4)
Considere el siguiente programa:
public class GenericTypeInference {
public static void main(String[] args) {
print(new SillyGenericWrapper().get());
}
private static void print(Object object) {
System.out.println("Object");
}
private static void print(String string) {
System.out.println("String");
}
public static class SillyGenericWrapper {
public <T> T get() {
return null;
}
}
}
Imprime "String" en Java 8 y "Object" en Java 7.
Hubiera esperado que esto fuera una ambigüedad en Java 8, porque ambos métodos sobrecargados coinciden.
¿Por qué el compilador selecciona
print(String)
después de
JEP 101
?
Justificado o no, esto rompe la compatibilidad con versiones anteriores y el cambio no se puede detectar en tiempo de compilación. El código simplemente se comporta de manera diferente después de actualizar a Java 8.
NOTA:
SillyGenericWrapper
se llama "tonto" por una razón.
Estoy tratando de entender por qué el compilador se comporta de la manera en que lo hace, no me digas que el envoltorio tonto es un mal diseño en primer lugar.
ACTUALIZACIÓN: También he intentado compilar y ejecutar el ejemplo en Java 8 pero usando un nivel de lenguaje Java 7. El comportamiento era consistente con Java 7. Eso era lo esperado, pero aún sentía la necesidad de verificar.
En java7, las expresiones se interpretan de abajo hacia arriba (con muy pocas excepciones); El significado de una sub-expresión es "libre de contexto". Para una invocación de método, los tipos de argumentos se resuelven primero; el compilador luego usa esa información para resolver el significado de la invocación, por ejemplo, para elegir un ganador entre los métodos sobrecargados aplicables.
En java8, esa filosofía ya no funciona, porque esperamos usar lambda implícita (como
x->foo(x)
) en todas partes;
los tipos de parámetros lambda no se especifican y deben inferirse del contexto.
Eso significa que, para las invocaciones de métodos, a veces los tipos de parámetros del método deciden los tipos de argumento.
Obviamente hay un dilema si el método está sobrecargado. Por lo tanto, en algunos casos, es necesario resolver la sobrecarga del método primero para elegir un ganador, antes de compilar los argumentos.
Ese es un cambio importante; y algunos códigos antiguos como el suyo serán víctimas de incompatibilidad.
Una solución alternativa es proporcionar un "tipeo de destino" al argumento con "contexto de conversión"
print( (Object)new SillyGenericWrapper().get() );
o, como la sugerencia de @ Holger, proporcione el parámetro de tipo
<Object>get()
para evitar inferencia por completo.
La sobrecarga del método Java es extremadamente complicada; El beneficio de la complejidad es dudoso. Recuerde, la sobrecarga nunca es una necesidad: si son métodos diferentes, puede darles nombres diferentes.
En primer lugar, no tiene nada que ver con anular, pero tiene que ver con la sobrecarga.
Jls. La Sección 15 proporciona mucha información sobre cómo exactamente el compilador selecciona el método sobrecargado
El método más específico se elige en tiempo de compilación; su descriptor determina qué método se ejecuta realmente en tiempo de ejecución.
Entonces al invocar
print(new SillyGenericWrapper().get());
El compilador elige
String
versión de
String
sobre
Object
porque el método de
print
que toma
String
es más específico que el que toma
Object
.
Si hubo
Integer
lugar de
String
, se seleccionará.
Además, si desea invocar un método que tome
Object
como parámetro, puede asignar el valor de retorno al parámetro de tipo
object
Eg.
public class GenericTypeInference {
public static void main(String[] args) {
final SillyGenericWrapper sillyGenericWrapper = new SillyGenericWrapper();
final Object o = sillyGenericWrapper.get();
print(o);
print(sillyGenericWrapper.get());
}
private static void print(Object object) {
System.out.println("Object");
}
private static void print(Integer integer) {
System.out.println("Integer");
}
public static class SillyGenericWrapper {
public <T> T get() {
return null;
}
}
}
Sale
Object
Integer
La situación comienza a ser interesante cuando digamos que tiene 2 definiciones de métodos válidos que son elegibles para sobrecarga. P.ej
private static void print(Integer integer) {
System.out.println("Integer");
}
private static void print(String integer) {
System.out.println("String");
}
y ahora si invocas
print(sillyGenericWrapper.get());
El compilador tendrá 2 definiciones de métodos válidos para elegir, por lo tanto, obtendrá un error de compilación porque no puede dar preferencia a un método sobre el otro.
Las reglas de inferencia de tipos han recibido una revisión importante en Java 8; más notablemente, la inferencia de tipos de destino ha mejorado mucho. Entonces, mientras que antes de Java 8 el sitio de argumento del método no recibió ninguna inferencia, por defecto a Object, en Java 8 se infiere el tipo aplicable más específico, en este caso String. JLS para Java 8 introdujo un nuevo capítulo Capítulo 18. Inferencia de tipo que falta en JLS para Java 7.
Las versiones anteriores de JDK 1.8 (hasta 1.8.0_25) tenían un error relacionado con la resolución de métodos sobrecargados cuando el compilador compiló con éxito el código que según JLS debería haber producido un error de ambigüedad ¿Por qué este método está sobrecargando de manera ambigua? Como Marco13 señala en los comentarios
Esta parte del JLS es probablemente la más complicada.
que explica los errores en versiones anteriores de JDK 1.8 y también el problema de compatibilidad que ve.
Como se muestra en el ejemplo de Java Tutoral ( inferencia de tipos )
Considere el siguiente método:
void processStringList(List<String> stringList) {
// process stringList
}
Suponga que desea invocar el método processStringList con una lista vacía. En Java SE 7, la siguiente declaración no se compila:
processStringList(Collections.emptyList());
El compilador Java SE 7 genera un mensaje de error similar al siguiente:
List<Object> cannot be converted to List<String>
El compilador requiere un valor para el argumento de tipo T, por lo que comienza con el valor Object. En consecuencia, la invocación de Collections.emptyList devuelve un valor de tipo List, que es incompatible con el método processStringList. Por lo tanto, en Java SE 7, debe especificar el valor del valor del argumento de tipo de la siguiente manera:
processStringList(Collections.<String>emptyList());
Esto ya no es necesario en Java SE 8. La noción de qué es un tipo de destino se ha ampliado para incluir argumentos de método, como el argumento del método processStringList. En este caso, processStringList requiere un argumento de tipo List
Collections.emptyList()
es un método genérico similar al método
get()
de la pregunta.
En Java 7, el método
print(String string)
ni siquiera es aplicable a la invocación del método, por lo que no participa en el proceso de resolución de sobrecarga
.
Mientras que en Java 8 ambos métodos son aplicables.
Vale la pena mencionar esta incompatibilidad en la Guía de compatibilidad para JDK 8 .
Puede consultar mi respuesta para una pregunta similar relacionada con la resolución de métodos sobrecargados Ambigüedad de sobrecarga de métodos con primitivas ternarias condicionales y sin caja de Java 8
De acuerdo con JLS 15.12.2.5 Elección del método más específico :
Si más de un método miembro es accesible y aplicable a una invocación de método, es necesario elegir uno para proporcionar el descriptor para el envío del método en tiempo de ejecución. El lenguaje de programación Java usa la regla de que se elige el método más específico.
Entonces:
Un método aplicable m1 es más específico que otro método aplicable m2, para una invocación con expresiones de argumento e1, ..., ek, si alguno de los siguientes es verdadero:
m2 es genérico y se infiere que m1 es más específico que m2 para las expresiones de argumento e1, ..., ek según §18.5.4.
m2 no es genérico, y m1 y m2 son aplicables por invocación estricta o flexible, y donde m1 tiene tipos de parámetros formales S1, ..., Sn y m2 tiene tipos de parámetros formales T1, ..., Tn, el tipo Si es más específico que Ti para el argumento ei para todo i (1 ≤ i ≤ n, n = k).
m2 no es genérico, y m1 y m2 son aplicables por invocación de aridad variable, y donde los primeros tipos de parámetros de aridad variable k de m1 son S1, ..., Sk y los primeros tipos de parámetros de aridad variable k de m2 son T1, .. ., Tk, el tipo Si es más específico que Ti para el argumento ei para todo i (1 ≤ i ≤ k). Además, si m2 tiene k + 1 parámetros, entonces el tipo de parámetro de aridad variable k + 1 ''de m1 es un subtipo del tipo de parámetro de aridad variable k + 1'' de m2.
Las condiciones anteriores son las únicas circunstancias bajo las cuales un método puede ser más específico que otro.
Un tipo S es más específico que un tipo T para cualquier expresión si S <: T (§4.10).
La segunda de las tres opciones coincide con nuestro caso.
Como
String
es un subtipo de
Object
(
String <: Object
) es más específico.
Por lo tanto, el método en sí es
más específico
.
Siguiendo el JLS, este método también es
estrictamente más específico
y
más específico
y es elegido por el compilador.
Lo ejecuté usando Java 1.8.0_40 y obtuve "Object".
Si ejecuta el siguiente código:
public class GenericTypeInference {
private static final String fmt = "%24s: %s%n";
public static void main(String[] args) {
print(new SillyGenericWrapper().get());
Method[] allMethods = SillyGenericWrapper.class.getDeclaredMethods();
for (Method m : allMethods) {
System.out.format("%s%n", m.toGenericString());
System.out.format(fmt, "ReturnType", m.getReturnType());
System.out.format(fmt, "GenericReturnType", m.getGenericReturnType());
}
private static void print(Object object) {
System.out.println("Object");
}
private static void print(String string) {
System.out.println("String");
}
public static class SillyGenericWrapper {
public <T> T get() {
return null;
}
}
}
Verás que obtienes:
Objeto public T com.xxx.GenericTypeInference $ SillyGenericWrapper.get () ReturnType: class java.lang.Object GenericReturnType: T
Lo que explica por qué se utiliza el método sobrecargado con Object y no el String.