retornan - Java: Método genérico de sobrecarga de ambigüedad
metodos genericos java (4)
Considera el siguiente código:
public class Converter {
public <K> MyContainer<K> pack(K key, String[] values) {
return new MyContainer<>(key);
}
public MyContainer<IntWrapper> pack(int key, String[] values) {
return new MyContainer<>(new IntWrapper(key));
}
public static final class MyContainer<T> {
public MyContainer(T object) { }
}
public static final class IntWrapper {
public IntWrapper(int i) { }
}
public static void main(String[] args) {
Converter converter = new Converter();
MyContainer<IntWrapper> test = converter.pack(1, new String[]{"Test", "Test2"});
}
}
El código anterior se compila sin problemas. Sin embargo, si uno cambia String[]
a String...
en ambas firmas de pack
y new String[]{"Test", "Test2"}
a "Test", "Test2"
, el compilador se queja de la llamada a converter.pack
siendo ambiguo
Ahora, puedo entender por qué podría considerarse ambiguo (como int
se puede autocapturar en un Integer
, por lo que se corresponden las condiciones, o la falta de ellas, de K
). Sin embargo, lo que no puedo entender es por qué la ambigüedad no está ahí si estás usando String[]
lugar de String...
¿Puede alguien explicar este extraño comportamiento?
Aquí tienes, la diferencia entre los dos métodos siguientes: Método 1:
public MyContainer<IntWrapper> pack(int key, Object[] values) {
return new MyContainer<>(new IntWrapper(""));
}
Método 2:
public MyContainer<IntWrapper> pack(int key, Object ... values) {
return new MyContainer<>(new IntWrapper(""));
}
El Método 2 es tan bueno como
public MyContainer<IntWrapper> pack(Object ... values) {
return new MyContainer<>(new IntWrapper(""));
}
Es por eso que obtienes una ambigüedad ...
EDITAR Sí, quiero decir que son los mismos para la compilación. El propósito de usar argumentos variables es permitir que un usuario defina un método cuando no está seguro del número de argumentos de un tipo dado.
Entonces, si está utilizando un objeto como argumentos variables, simplemente diga el compilador que no estoy seguro de cuántos objetos enviaré y, por otro lado, está diciendo: "Estoy pasando un número entero y una cantidad desconocida de objetos". Para el compilador, el entero también es un objeto.
Si desea verificar la validez, intente pasar un entero como primer argumento y luego pase un argumento variable de String. Verás la diferencia.
Por ejemplo:
public class Converter {
public static void a(int x, String... y) {
}
public static void a(String... y) {
}
public static void main(String[] args) {
a(1, "2", "3");
}
}
Además, no use las matrices y argumentos variables de manera intercambiable, tienen algunos propósitos diferentes por completo.
Cuando utiliza varargs, el método no espera una matriz sino diferentes parámetros del mismo tipo, a los que se puede acceder de forma indexada.
En este caso
(1) m(K, String[])
(2) m(int, String[])
m(1, new String[]{..});
m (1) satisface 15.12.2.3. Fase 2: Identificar los métodos de concordancia de coincidencia aplicables mediante conversión de invocación de método
m (2) satisface 15.12.2.2. Fase 1: Identificar los métodos de concordancia coincidentes aplicables mediante subtipificación
El compilador se detiene en la Fase 1; encontró m (2) como el único método aplicable en esa fase, por lo tanto se elige m (2).
En el caso var arg
(3) m(K, String...)
(4) m(int, String...)
m(1, str1, str2);
Ambos m (3) ym (4) satisfacen 15.12.2.4. Fase 3: Identificar los métodos aplicables de la variable Arity . Ninguno es más específico que el otro, por lo tanto, la ambigüedad.
Podemos agrupar los métodos aplicables en 4 grupos:
- aplicable por subtipificación
- aplicable por conversión de invocación de método
- vararg, aplicable mediante subtipificación
- vararg, aplicable por conversión de invocación de método
La especificación fusionó el grupo 3 y 4 y los trató a ambos en la Fase 3. Por lo tanto, la incoherencia.
¿Por qué hicieron eso? Maye, simplemente se cansaron de eso.
Otra crítica sería que no debería haber todas estas fases, porque los programadores no piensan de esa manera. Simplemente deberíamos encontrar todos los métodos aplicables indiscriminadamente, luego elegir el más específico (con algún mecanismo para evitar el boxeo / unboxing)
En primer lugar, esto es solo algunas primeras pistas ... puede editar por más.
El compilador siempre busca y selecciona el método más específico disponible. Aunque es un poco torpe de leer, todo está especificado en JLS 15.12.2.5 . Por lo tanto, llamando
convertidor.pack (1, "Prueba", "Prueba 2")
no es determinable para el compilador si el 1
se disolverá en K
o int
. En otras palabras, K puede aplicarse a cualquier tipo, por lo que está en el mismo nivel que int / Integer.
La diferencia radica en el número y tipo de argumentos. Tenga en cuenta que el new String[]{"Test", "Test2"}
es un conjunto, mientras que "Test", "Test2"
son dos argumentos de tipo String.
convertidor.pack (1); // ambiguo, error del compilador
convertidor.pack (1, nulo); // llama al método 2, advertencia del compilador
convertidor.pack (1, nuevo String [] {}); // llama al método 2, advertencia del compilador
converter.pack (1, new Object ()); // ambiguo, error del compilador
convertidor.pack (1, nuevo Objeto [] {}); // llama al método 2, sin advertencia
Su primer caso es bastante directo. El siguiente método:
public MyContainer<IntWrapper> pack(int key, Object[] values)
es una coincidencia exacta para los argumentos - (1, String[])
. De la Sección 15.12.2 de JLS :
La primera fase (§15.12.2.2) realiza la resolución de sobrecarga sin permitir la conversión de boxeo o unboxing
Ahora, no hay boxeo involucrado al pasar esos parámetros al segundo método. Como Object[]
es un súper tipo de String[]
. Y pasar String[]
argumento String[]
para el parámetro Object[]
era una invocación válida incluso antes de Java 5.
El compilador parece jugar un truco en tu 2da caja:
En su segundo caso, dado que ha utilizado var-args, la resolución de sobrecarga del método se realizará usando var-args, boxeo o unboxing, según la tercera fase explicada en esa sección JLS:
La tercera fase (§15.12.2.4) permite que la sobrecarga se combine con métodos de aridad variable, boxeo y desempaquetado.
Tenga en cuenta que la segunda fase no es aplicable aquí, debido al uso de var-args :
La segunda fase (§15.12.2.3) realiza la resolución de sobrecarga al tiempo que permite el boxeo y el desempaquetado, pero todavía impide el uso de la invocación del método de aridad variable.
Ahora lo que está sucediendo aquí es que el compilador no está deduciendo el argumento de tipo correctamente * (En realidad, lo está deduciendo correctamente ya que el parámetro de tipo se usa como parámetro formal, consulte la actualización al final de esta respuesta). Entonces, para su invocación de método:
MyContainer<IntWrapper> test = converter.pack(1, "Test", "Test2");
el compilador debería haber inferido el tipo de K
en el método genérico para ser IntWrapper
, del LHS. Pero parece que se está infiriendo que K
es un tipo Integer
, debido a que ambos métodos ahora son igualmente aplicables para esta llamada a método, ya que ambos requieren var-args
o boxing
.
Sin embargo, si el resultado de ese método no está asignado a alguna referencia, entonces puedo entender que el compilador no puede inferir el tipo apropiado como en este caso, donde es perfectamente aceptable dar un error de ambigüedad:
converter.pack(1, "Test", "Test2");
Puede ser, supongo, solo para mantener la consistencia, también está marcado como ambiguo para el primer caso. Pero, de nuevo, no estoy muy seguro, ya que no he encontrado ninguna fuente confiable de JLS u otra referencia oficial que trate sobre este tema. Seguiré buscando y, si logro encontrar uno, actualizaré la respuesta.
Vamos a engañar al compilador por información de tipo explícito:
Si cambia la invocación del método para proporcionar información de tipo explícita:
MyContainer<IntWrapper> test = converter.<IntWrapper>pack(1, "Test", "Test2");
Ahora, el tipo K
se IntWrapper
como IntWrapper
, pero como 1
no es convertible a IntWrapper
, ese método se descarta, y se invocará el segundo método, que funcionará perfectamente bien.
Francamente hablando, realmente no sé lo que está sucediendo aquí. Yo esperaría que el compilador también deduzca el parámetro de tipo del contexto de invocación del método en el primer caso, ya que funciona para el siguiente problema:
public static <T> HashSet<T> create(int size) {
return new HashSet<T>(size);
}
// Type inferred as `Integer`, from LHS.
HashSet<Integer> hi = create(10);
Pero, no está haciendo en este caso. Entonces esto puede ser un error.
* O puede ser que no entiendo exactamente cómo el compilador infiere los argumentos de tipo, cuando el tipo no se pasa como argumento. Entonces, para aprender más sobre esto, traté de revisar - JLS §15.12.2.7 y JLS §15.12.2.8 , que trata sobre cómo el compilador infiere el argumento de tipo, pero eso está yendo por completo al límite de mi cabeza.
Entonces, por ahora, tiene que vivir con eso y usar la alternativa (proporcionando un argumento de tipo explícito).
Resulta que el compilador no estaba jugando ningún truco:
Como finalmente se explicó en el comentario de @ zhong.j.yu., El compilador solo aplica la sección 15.12.2.8 para la inferencia de tipo, cuando no puede inferirlo según la sección 15.12.2.7. Pero aquí, puede inferir el tipo como Integer
del argumento que se pasa, ya que claramente el parámetro type es un parámetro de formato en el método.
Entonces, sí, el compilador infiere correctamente el tipo como Integer
y, por lo tanto, la ambigüedad es válida. Y ahora creo que esta respuesta está completa.