java - juridica - tipos de ambiguedad
Método de sobrecarga de ambigüedad con Java 8 primitivas condicionales y sin caja ternarias (2)
En breve:
El compilador no sabe qué método elegir, ya que el orden entre los tipos de referencia y primitivos no está definido en JLS en lo que respecta a la elección del método más específico.
Cuando usa Integer en lugar de int, el compilador elige el método con Integer porque Integer es un subtipo de Object.
Cuando usas Double en lugar de double, el compilador elige el método que no involucra boxeo o unboxing.
Antes de Java 8, las reglas eran diferentes, por lo que este código podía compilarse.
El siguiente es el código compilado en Java 7, pero no openjdk-1.8.0.45-31.b13.fc21.
static void f(Object o1, int i) {}
static void f(Object o1, Object o2) {}
static void test(boolean b) {
String s = "string";
double d = 1.0;
// The supremum of types ''String'' and ''double'' is ''Object''
Object o = b ? s : d;
Double boxedDouble = d;
int i = 1;
f(o, i); // fine
f(b ? s : boxedDouble, i); // fine
f(b ? s : d, i); // ERROR! Ambiguous
}
El compilador reclama la última llamada al método ambiguo.
Si cambiamos el tipo del segundo parámetro de f
de int
a Integer
, entonces el código se compila en ambas plataformas. ¿Por qué el código publicado no se compila en Java 8?
Primero consideremos una versión simplificada que no tiene un condicional ternario y no se compila en Java HotSpot VM (compilación 1.8.0_25-b17):
public class Test {
void f(Object o1, int i) {}
void f(Object o1, Object o2) {}
void test() {
double d = 1.0;
int i = 1;
f(d, i); // ERROR! Ambiguous
}
}
El error del compilador es:
Error:(12, 9) java: reference to f is ambiguous
both method f(java.lang.Object,int) in test.Test and method f(java.lang.Object,java.lang.Object) in test.Test match
Según JLS 15.12.2. Tiempo de compilación, paso 2: determinar la firma del método
Un método es aplicable si es aplicable por una de invocación estricta (§15.12.2.2), invocación suelta (§15.12.2.3), o invocación de aridad variable (§15.12.2.4).
La invocación tiene que ver con el contexto de invocación que se explica aquí JLS 5.3. Contextos de invocación
Cuando no se involucra boxeo o unboxing para una invocación de método, se aplica una invocación estricta. Cuando se involucra boxeo o unboxing para una invocación de método, se aplica la invocación suelta.
La identificación de los métodos aplicables se divide en 3 fases.
La primera fase (§15.12.2.2) realiza la resolución de sobrecarga sin permitir la conversión de boxeo o unboxing, o el uso de invocación del método de aridad variable. Si no se encuentra ningún método aplicable durante esta fase, el procesamiento continúa hasta la segunda fase.
La segunda fase (§15.12.2.3) realiza la resolución de sobrecarga al tiempo que permite el boxeo y el desempaquetado, pero aún impide el uso de la invocación del método de aridad variable. Si no se encuentra ningún método aplicable durante esta fase, el procesamiento continúa hasta la tercera fase.
La tercera fase (§15.12.2.4) permite que la sobrecarga se combine con métodos de aridad variable, boxeo y unboxing.
Para nuestro caso no hay métodos aplicables por invocación estricta. Ambos métodos son aplicables por invocación suelta, ya que el valor doble debe ser recuadrado.
De acuerdo con JLS 15.12.2.5 Elegir el 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 utiliza 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 se cumple alguna de las siguientes condiciones:
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 suelta, 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 todos 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 k parámetros de variable arity tipos de m1 son S1, ..., Sk y los primeros k parámetros de variable arity tipos de m2 son T1, .. ., Tk, el tipo Si es más específico que Ti para el argumento ei para todos los i (1 ≤ i ≤ k). Además, si m2 tiene k + 1 parámetros, entonces el tipo de parámetro de aridad variable k + 1''th de m1 es un subtipo del tipo de parámetro de aridad variable k + 1''th de m2.
Las condiciones anteriores son las únicas circunstancias en las que 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).
Puede parecer que la segunda condición coincide con este caso, pero en realidad no lo hace porque int no es un subtipo de Object: no es cierto que int <: Object . Sin embargo, si reemplazamos int con Integer en la firma del método f, esta condición coincidirá. Tenga en cuenta que el primer parámetro en los métodos coincide con esta condición, ya que Object <: Object es verdadero.
Según $4.10 no se define una relación de subtipo / supertipo entre tipos primitivos y tipos de clase / interfaz. Así que int no es un subtipo de Object, por ejemplo. Por lo tanto, int no es más específico que Object .
Dado que entre los 2 métodos no hay más métodos específicos, por lo tanto, no puede haber estrictamente más específico ni puede ser el más específico (el JLS proporciona definiciones para esos términos en el mismo párrafo JLS 15.12.2.5 Elección del método más específico ). Así que ambos métodos son máximamente específicos .
En este caso el JLS da 2 opciones:
Si todos los métodos máximamente específicos tienen firmas de anulación equivalente (§8.4.2) ...
Este no es nuestro caso, por lo tanto
De lo contrario, la invocación del método es ambigua y se produce un error en tiempo de compilación.
El error en tiempo de compilación para nuestro caso parece válido de acuerdo con el JLS.
¿Qué sucede si cambiamos el tipo de parámetro del método de int a Integer?
En este caso, ambos métodos siguen siendo aplicables por invocación suelta. Sin embargo, el método con el parámetro Integer es más específico que el método con 2 parámetros Object desde Integer <: Object. El método con el parámetro Integer es estrictamente más específico y específico, por lo que el compilador lo elegirá y no lanzará un error de compilación.
¿Qué pasa si cambiamos doble a doble en esta línea: doble d = 1.0 ;?
En este caso, hay exactamente 1 método aplicable por invocación estricta: no se requiere boxeo ni unboxing para invocar este método: f (Objeto o1, int i). Para el otro método, debe hacer un boxeo de valor int para que sea aplicable por invocación suelta. El compilador puede elegir el método aplicable por invocación estricta, por lo que no se produce ningún error de compilación.
Como señaló Marco13 en su comentario, hay un caso similar discutido en esta publicación. ¿Por qué este método se sobrecarga de manera ambigua?
Como se explicó en la respuesta, hubo algunos cambios importantes relacionados con los mecanismos de invocación de métodos entre Java 7 y Java 8. Esto explica por qué el código se compila en Java 7 pero no en Java 8.
¡Ahora viene la parte divertida!
Añadamos un operador condicional ternario:
public class Test {
void f(Object o1, int i) {
System.out.println("1");
}
void f(Object o1, Object o2) {
System.out.println("2");
}
void test(boolean b) {
String s = "string";
double d = 1.0;
int i = 1;
f(b ? s : d, i); // ERROR! Ambiguous
}
public static void main(String[] args) {
new Test().test(true);
}
}
El compilador se queja de la invocación ambigua del método. El JLS 15.12.2 no dicta ninguna regla especial relacionada con los operadores condicionales ternarios al realizar invocaciones de métodos.
Sin embargo, hay JLS 15.25 Operador Condicional? : y JLS 15.25.3. Expresiones condicionales de referencia . El primero clasifica las expresiones condicionales en 3 subcategorías: booleano, numérico y expresión condicional de referencia. El segundo y tercer operandos de nuestra expresión condicional tienen tipos String y double respectivamente. De acuerdo con el JLS, nuestra expresión condicional es una expresión condicional de referencia.
Entonces de acuerdo con JLS 15.25.3. Expresiones condicionales de referencia nuestra expresión condicional es una expresión condicional de referencia múltiple ya que aparece en un contexto de invocación. El tipo de nuestra expresión condicional poli es, por lo tanto, Objeto (el tipo de destino en el contexto de invocación). Desde aquí podríamos continuar los pasos como si el primer parámetro fuera un Objeto, en cuyo caso el compilador debería elegir el método con int como segundo parámetro (y no lanzar el error del compilador).
La parte difícil es esta nota de JLS:
sus expresiones de segundo y tercer operando aparecen de manera similar en un contexto del mismo tipo con el tipo de destino T.
De esto podemos suponer (también el "poli" en el nombre lo implica) que en el contexto de la invocación del método, los 2 operandos deben considerarse de forma independiente. Lo que esto significa es que cuando el compilador tiene que decidir si se requiere una operación de boxeo para tal argumento, debe examinar cada uno de los operandos y ver si se puede requerir un boxeo. Para nuestro caso específico, String no requiere boxeo y el doble requerirá boxeo. Por lo tanto, el compilador decide que para ambos métodos sobrecargados debería ser una invocación de método suelto. Los pasos adicionales son los mismos que en el caso cuando, en lugar de una expresión condicional ternaria, usamos un valor doble.
De la explicación anterior, parece que el JLS en sí mismo es vago y ambiguo en la parte relacionada con las expresiones condicionales cuando se aplica a métodos sobrecargados, por lo que tuvimos que hacer algunas suposiciones.
Lo interesante es que mi IDE (IntelliJ IDEA) no detecta el último caso (con la expresión condicional ternaria) como un error del compilador. Todos los demás casos se detectan de acuerdo con el compilador java de JDK. Esto significa que el compilador de Java JDK o el analizador interno de IDE tienen un error.