wrappers raw que ejemplos docs java generics java-8 autoboxing

raw - java docs generics



Java 8 autoboxing+genéricos: comportamiento diferente con variable vs. método (4)

Encontré un fragmento de código que, después de cambiar de Java 7 a Java 8, dejó de compilar. No presenta ninguna de las novedades de Java 8 como lambda o streams.

Reduje el código problemático a la siguiente situación:

GenericData<Double> g = new GenericData<>(1d); Double d = g == null ? 0 : g.getData(); // type error!!!

Probablemente pueda adivinar que el constructor de GenericData tiene un parámetro de ese tipo genérico y el método getData() devuelve solo ese tipo genérico. (Para ver el código fuente completo, vea a continuación).

Ahora lo que me molesta es que en Java 7 ese código se compiló bien, mientras que con Java 8 obtengo el siguiente error:

CompileMe.java:20: error: incompatible types: bad type in conditional expression Double d = g == null ? 0 : g.getData(); ^ int cannot be converted to Double

Parece que Java 7 fue capaz de hacer la transición de int -> double -> Double, pero Java 8 falla al intentar pasar inmediatamente de int -> Double.

Lo que me parece divertido en particular es que Java 8 acepta el código cuando lo cambio de getData() a data , es decir, accede al valor de GenericData través de la variable en lugar del método getter:

Double d2 = g == null ? 0 : g.data; // now why does this work...

Así que las dos preguntas que tengo aquí son:

  1. ¿Por qué Java 8 no infiere los tipos como Java 7 y mi int intuición se duplica antes de doblar automáticamente a doble?
  2. ¿Por qué ese problema solo ocurre con el método genérico pero no con la variable genérica?

Código fuente completo:

public class CompileMe { public void foo() { GenericData<Double> g = new GenericData(1d); Double d = g == null ? 0 : g.getData(); // type error!!! Double d2 = g == null ? 0 : g.data; // now why does this work... } } class GenericData<T> { public T data; public GenericData(T data) { this.data = data; } public T getData() { return data; } }

Para probarlo ejecute el compilador de la siguiente manera:

javac -source 1.7 -target 1.7 CompileMe.java # ok (just warnings) javac -source 1.8 -target 1.8 CompileMe.java # error (as described above)

Finalmente, en caso de que importe: ejecuto Windows 8 y Java 1.8.0_112 (64 bits).


CompileMe.java:4: error: tipos incompatibles: tipo incorrecto en

expresión condicional

Double d = g == null ? 0 : g.getData(); // type error!!!

int cannot be converted to Double

Aquí 0 es entero y lo estás poniendo dentro de Double.

Prueba esto

public class CompileMe { public static void main(String[] args) { GenericData<Double> g = new GenericData(1d); Double d = g == null ? 0d : g.getData(); // type error!!! Double d2 = g == null ? 0d : g.data; // now why does this work... System.out.println(d); System.out.println(d2); } } class GenericData<T> { public T data; public GenericData(T data) { this.data = data; } public T getData() { return data; } }

O use doble literal en lugar de la clase de contenedor doble

public class CompileMe { public static void main(String[] args) { GenericData<Double> g = new GenericData(1d); double d = g == null ? 0 : g.getData(); // type error!!! double d2 = g == null ? 0 : g.data; // now why does this work... System.out.println(d); System.out.println(d2); } } class GenericData<T> { public T data; public GenericData(T data) { this.data = data; } public T getData() { return data; } }

Porque la ampliación y el boxeo no tendrán lugar simultáneamente.


Este parece ser un problema conocido del compilador de Oracle: ID de error: JDK-8162708

Citar:

UNA DESCRIPCIÓN DEL PROBLEMA:
Si tiene un método en una clase genérica declarado como sigue:

class Foo<T> { public T getValue() { // returns a value ... } }

y usted llama al método anterior dentro de un operador ternario como sigue

Foo<Integer> foo = new Foo<>(); Float f = new Random().nextBoolean() ? foo.getValue() : 0f;

se obtiene un error de sintaxis del compilador javac 1.8.

Pero el código anterior compila sin errores y advertencias con javac 1.7 y 1.9.

Resolución: Sin resolver.

Versiones afectadas: 8

De los comentarios:

Este problema solo es aplicable a 8u, no hay problema en 7 y 9


Las expresiones de invocación de métodos son especiales, ya que pueden ser expresiones poligonales , sujetas a la tipificación de destino.

Considere los siguientes ejemplos:

static Double aDouble() { return 0D; } … Double d = g == null ? 0 : aDouble();

Esto lo compila sin ningún problema.

static <T> T any() { return null; } … Double d = g == null ? 0 : any();

aquí, la invocación de any() es una expresión de Poly y el compilador debe inferir T := Double . Esto reproduce el mismo error.

Esta es la primera inconsistencia. Si bien su método getData() refiere al parámetro de tipo T de GenericData , no es un método genérico (no debe haber inferencia de tipo involucrada para determinar que T es Double aquí).

JLS §8.4.4. Métodos genéricos

Un método es genérico si declara una o más variables de tipo

getData() no declara variables de tipo, solo usa una.

JLS §15.12. Método de las expresiones de invocación :

Una expresión de invocación de método es una expresión poli si todas las siguientes son verdaderas:

  • ...
  • El método a ser invocado, según lo determinan las siguientes subsecciones, es genérico (§8.4.4) y tiene un tipo de retorno que menciona al menos uno de los parámetros de tipo del método.

Dado que la invocación de este método no es una expresión poli, debe comportarse como el ejemplo con la invocación aDouble() , en lugar de any() .

Pero tenga en cuenta §15.25.3 :

Tenga en cuenta que una expresión condicional de referencia no tiene que contener una expresión poli como un operando para ser una expresión poli. Es una expresión polivinílica simplemente en virtud del contexto en el que aparece. Por ejemplo, en el siguiente código, la expresión condicional es una expresión poli, y se considera que cada operando está en una asignación de contexto de Class<? super Integer> Class<? super Integer> :

Class<? super Integer> choose(boolean b, Class<Integer> c1, Class<Number> c2) { return b ? c1 : c2; }

Entonces, ¿es una referencia condicional o una expresión numérica condicional?

§15.25. ¿Operador condicional? : dice:

Hay tres tipos de expresiones condicionales, clasificadas según las expresiones del segundo y tercer operando: expresiones condicionales booleanas, expresiones condicionales numéricas y expresiones condicionales de referencia . Las reglas de clasificación son las siguientes:

  • Si tanto la segunda como la tercera expresión de operando son expresiones booleanas, la expresión condicional es una expresión condicional booleana.
    ...
  • Si las expresiones del segundo y tercer operando son expresiones numéricas, la expresión condicional es una expresión condicional numérica.
    Para el propósito de clasificar un condicional, las siguientes expresiones son expresiones numéricas:
    • Una expresión de una forma independiente (§15.2) con un tipo que es convertible a un tipo numérico (§4.2, §5.1.8).
    • Una expresión numérica entre paréntesis (§15.8.5).
    • Una expresión de creación de instancia de clase (§15.9) para una clase que se puede convertir a un tipo numérico.
    • Una expresión de invocación de método (§15.12) para la cual el método más específico elegido (§15.12.2.5) tiene un tipo de retorno que se puede convertir a un tipo numérico.
    • Una expresión condicional numérica.
  • De lo contrario, la expresión condicional es una expresión condicional de referencia.

Entonces, de acuerdo con estas reglas, sin excluir las invocaciones de métodos genéricos, todas las expresiones condicionales mostradas son expresiones condicionales numéricas y deberían funcionar, ya que solo "de lo contrario " deben considerarse como expresiones condicionales de referencia. La versión de Eclipse, que probé, los compilé todos sin reportar ningún error.

Esto lleva a la extraña situación de que, en el caso de any() , necesitamos la tipificación de destino para descubrir que tiene un tipo de retorno numérico y deducir que el condicional es una expresión condicional numérica , es decir, una expresión independiente . Tenga en cuenta que para las expresiones condicionales booleanas , hay un comentario:

Tenga en cuenta que, para un método genérico, este es el tipo antes de crear una instancia de los argumentos de tipo del método.

pero para la expresión condicional numérica , no hay tal nota, ya sea intencional o no.

Pero como se dijo, esto solo se aplica al ejemplo any() todos modos, ya que el método getData() no es genérico.


Tener que reclamar esto no es una respuesta, simplemente un razonamiento. Con mi breve experiencia en el compilador (no específico de Javac), podría tener algo que ver con cómo se analiza el código.

En el siguiente código descompilado, puede ver el método GenericData.getData:()Ljava/lang/Object o al campo GenericData.data:Ljava/lang/Object , ambos obtienen primero el valor / método con Object devuelto, seguido por un cast .

stack=4, locals=4, args_size=1 0: new #2 // class general/GenericData 3: dup 4: dconst_1 5: invokestatic #3 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double; 8: invokespecial #4 // Method general/GenericData."<init>":(Ljava/lang/Object;)V 11: astore_1 12: aload_1 13: ifnonnull 23 16: dconst_0 17: invokestatic #3 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double; 20: goto 30 23: aload_1 24: invokevirtual #5 // Method general/GenericData.getData:()Ljava/lang/Object; 27: checkcast #6 // class java/lang/Double 30: astore_2 31: aload_1 32: ifnonnull 39 35: dconst_0 36: goto 49 39: aload_1 40: getfield #7 // Field general/GenericData.data:Ljava/lang/Object; 43: checkcast #6 // class java/lang/Double 46: invokevirtual #8 // Method java/lang/Double.doubleValue:()D 49: invokestatic #3 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double; 52: astore_3 53: return

Si compara la expresión del operador ternario con un equivalente if-else:

Integer v = 10; v = v != null ? 1 : 0; 0: bipush 10 2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: astore_1 6: aload_1 7: ifnull 14 10: iconst_1 11: goto 15 14: iconst_0 15: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 18: astore_1 19: return Integer v = 10; if (v != null) v = 1; else v = 0; 0: bipush 10 2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: astore_1 6: aload_1 7: ifnull 18 10: iconst_1 11: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 14: astore_1 15: goto 23 18: iconst_0 19: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 22: astore_1 23: return

No hay diferencia significativa en las dos versiones. Así que no creo que haya un secreto oculto haciendo toda la magia negra. Es el resultado de cómo el compilador analiza la expresión completa y se basa en el contexto para descubrir un tipo que haga que todos los componentes sean igualmente felices. P.ej,

Double val = 0; // compilation error: context is clear, 0 is an integer, so Integer.valueOf(i), but don''t match expected type - Double val = 0 + g.getData(); // OK, enough context to figure out the type should be Double

Aún así, la confusión está en que el campo genérico funciona pero no el método genérico ...

val = val == null ? 0 : g.data; // OK val = val == null ? 0 : g.getData(); // Compilation error

EDITAR: el documento citado por Holger parece ser una buena aclaración.