ternario ternaria operador busqueda anidado alternativa java generics java-8

java - ternaria - operador ternario php



Error de compilación de genéricos con operador ternario en Java 8, pero no en Java 7 (3)

Esta clase compila ok en Java 7, pero no en Java 8:

public class Foo { public static void main(String[] args) throws Exception { //compiles fine in Java 7 and Java 8: Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class; CharSequence foo = foo(aClass); //Inlining the variable, compiles in Java 7, but not in Java 8: CharSequence foo2 = foo(true ? String.class : StringBuilder.class); } static <T> T foo(Class<T> clazz) throws Exception { return clazz.newInstance(); } }

Error de compilación:

Error: (9, 29) java: el método foo en la clase Foo no se puede aplicar a los tipos dados; requerido: java.lang.Class found: true? Clase de str
razón: el tipo inferido no se ajusta a las restricciones de igualdad inferidas: java.lang.StringBuilder restricciones de igualdad (s): java.lang.StringBuilder, java.lang.String

¿Por qué esto ha dejado de funcionar en Java 8? ¿Es intencional / un efecto secundario de alguna otra característica, o es simplemente un error del compilador?


El problema aparece solo en el contexto de los parámetros y las asignaciones. Es decir

CharSequence cs1=(true? String.class: StringBuilder.class).newInstance();

trabajos. A diferencia de las otras afirmaciones de respuesta, el uso de un método genérico <T> T ternary(boolean cond, T a, T b) no funciona. Esto aún falla cuando la invocación se pasa a un método genérico como <T> T foo(Class<T> clazz) para que se <T> T foo(Class<T> clazz) el tipo real de <T> . Sin embargo funciona en el ejemplo de asignación.

Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class;

como el tipo resultante ya está especificado explícitamente. Por supuesto, un tipo emitido a Class<? extends CharSequence> Class<? extends CharSequence> siempre arreglaría eso para los otros casos de uso también.

El problema es que el algoritmo se especifica para encontrar primero el "límite inferior superior" del segundo y tercer operando y luego aplicar la "conversión de captura". El primer paso ya falla para los dos tipos, Class<String> y Class<StringBuilder> por lo que ni siquiera se intentará el segundo paso que consideraría el contexto.

Esta no es una situación satisfactoria, sin embargo, en este caso especial hay una alternativa elegante:

public static void main(String... arg) { CharSequence cs=foo(true? String::new: StringBuilder::new); } static <T> T foo(Supplier<T> s) { return s.get(); }


Esto no es un error javac, de acuerdo con la especificación actual. Escribí una respuesta aquí SO para un issue similar. Aquí el problema es más o menos el mismo.

En un contexto de asignación o invocación, las expresiones condicionales de referencia son expresiones poli. Esto significa que el tipo de expresión no es el resultado de aplicar la conversión de captura a lub (T1, T2); consulte JSL-15.25.3 para obtener una definición detallada de T1 y T2. En cambio, tenemos, también de esta parte de la especificación que:

Cuando aparece una expresión condicional de referencia múltiple en un contexto de un tipo particular con el tipo de destino T, sus expresiones de segundo y tercer operando aparecen de manera similar en un contexto del mismo tipo con el tipo de destino T.

El tipo de una expresión condicional de referencia múltiple es el mismo que su tipo de destino.

Entonces, esto significa que el tipo de destino se reduce a ambos operandos de la expresión condicional de referencia, y ambos operandos se atribuyen a ese tipo de destino. Entonces, el compilador termina reuniendo restricciones de ambos operandos, lo que lleva a un conjunto de restricciones irresolubles y, por lo tanto, a un error.

OK, pero ¿por qué tenemos límites de igualdad para T aquí?

Veamos en detalle, de la llamada:

foo(true ? String.class : StringBuilder.class)

donde foo es:

static <T> T foo(Class<T> clazz) throws Exception { return clazz.newInstance(); }

Tenemos eso ya que estamos invocando el método foo() con la expresión true ? String.class : StringBuilder.class true ? String.class : StringBuilder.class . Esta expresión condicional de referencia debe ser compatible en un contexto de invocación con tipo de Class<T> . Esto se representa como, ver JLS-18.1.2 :

true ? String.class : StringBuilder.class → Class<T>

Como sigue de JLS-18.2.1 tenemos que:

Una fórmula de restricción de la forma <Expresión → T> se reduce de la siguiente manera:

...

  • Si la expresión es una expresión condicional de la forma e1? e2: e3, la restricción se reduce a dos fórmulas de restricción, <e2 → T> y <e3 → T>.

Esto implica que obtenemos las siguientes fórmulas de restricción:

String.class → Class<T> StringBuilder.class → Class<T>

o:

Class<String> → Class<T> Class<StringBuilder> → Class<T>

Más tarde de JLS-18.2.2 tenemos que:

Una fórmula de restricción de la forma <S → T> se reduce de la siguiente manera:

...

  • De lo contrario, la restricción se reduce a <S <: T>.

Solo estoy incluyendo las partes relacionadas. Así que ahora tenemos:

Class<String> <: Class<T> Class<StringBuilder> <: Class<T>

De JLS-18.2.3 , tenemos:

Una fórmula de restricción de la forma ‹S <: T› se reduce de la siguiente manera:

...

  • De lo contrario, la restricción se reduce de acuerdo con la forma de T:
    • Si T es una clase parametrizada o tipo de interfaz, o un tipo de clase interna de una clase parametrizada o tipo de interfaz (directa o indirectamente), deje que A1, ..., An sean los argumentos de tipo T. Entre los supertipos de S, a Se identifica la clase correspondiente o el tipo de interfaz, con los argumentos de tipo B1, ..., Bn. Si no existe tal tipo, la restricción se reduce a falso. De lo contrario, la restricción se reduce a las siguientes restricciones nuevas: para todas las i (1 ≤ i ≤ n), ‹Bi <= Ai›.

Entonces, como la Class<T> , la Class<String> y la Class<StringBuilder> son clases parametrizadas, esto implica que ahora nuestras restricciones se reducen a:

String <= T StringBuilder <= T

También de JLS-18.2.3 , tenemos:

Una fórmula de restricción de la forma <S <= T>, donde S y T son argumentos de tipo (§4.5.1), se reduce de la siguiente manera:

...

  • Si T es un tipo:
    • Si S es un tipo, la restricción se reduce a <S = T>.

Así terminamos con estas restricciones para T:

String = T StringBuilder = T

Finalmente en JLS-18.2.4 tenemos eso:

Una fórmula de restricción de la forma ‹S = T›, donde S y T son tipos, se reduce de la siguiente manera:

...

  • De lo contrario, si T es una variable de inferencia, α, la restricción se reduce al límite S = α.

Y no hay solución para la variable de tipo T con límites T = String y T = StringBuilder . No hay ningún tipo que el compilador pueda sustituir por T que satisfaga ambas restricciones. Por esta razón, el compilador muestra el mensaje de error.

Entonces javac está bien de acuerdo con la especificación actual, pero ¿es la especificación correcta en esto? Bueno, hay un problema de compatibilidad entre 7 y 8 que debe ser investigado. Por esta razón, he archivado JDK-8044053 para que podamos rastrear este problema.


Voy a dar un paso en falso y decir que este error (aunque puede o no ajustarse al JLS actualizado, que admito que no he leído en detalle) se debe a una incoherencia en el manejo de tipos por parte del JDK 8 compilador.

En general, el operador ternario usó el mismo tipo de inferencia que para un método de dos argumentos que tenía los parámetros formales basados ​​en el mismo parámetro de tipo. Por ejemplo:

static <T> T foo(Class<? extends T> clazz, Class<? extends T> clazz2) { return null; } public static void main(String[] args) { CharSequence foo2 = foo(String.class, StringBuilder.class); }

En este ejemplo, se puede inferir que T es una captura de ? extends Object & Serializable & CharSequence ? extends Object & Serializable & CharSequence . De manera similar, en JDK 7, si volvemos a su ejemplo original:

CharSequence foo2 = foo(true ? String.class : StringBuilder.class);

Esto hace casi exactamente la misma inferencia de tipo que antes, pero en este caso considere el operador ternario como un método como tal:

static <T> T ternary(boolean cond, T a, T b) { if (cond) return a; else return b; }

Entonces, en este caso, si pasa String.class y StringBuilder.class como los parámetros, el tipo inferido de T es (en términos generales) Class<? extends Object & Serializable & CharSequence> Class<? extends Object & Serializable & CharSequence> , que es lo que queríamos.

De hecho, puede reemplazar el uso de su operador ternario en el fragmento original con este método, por lo tanto:

public class Foo { public static void main(String[] args) throws Exception { //compiles fine in Java 7 and Java 8: Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class; CharSequence foo = foo(aClass); //Inlining the variable using ''ternary'' method: CharSequence foo2 = foo(ternary(true, String.class, StringBuilder.class)); } static <T> T foo(Class<T> clazz) throws Exception { return clazz.newInstance(); } static <T> T ternary(boolean cond, T a, T b) { if (cond) return a; else return b; } }

... Y ahora se compila en Java 7 y 8 ( editar: ¡en realidad también falla con Java 8! editar de nuevo: ahora funciona, Jdk 8u20). ¿Lo que da? por alguna razón, ahora se impone una restricción de igualdad en T (en el método foo), en lugar de una restricción de límites inferiores. La sección relevante de JLS para Java 7 es 15.12.2.7 ; para Java 8 hay un capítulo completamente nuevo sobre inferencia de tipos ( capítulo 18 ).

Tenga en cuenta que escribir explícitamente T en la llamada a ''ternary'' permite la compilación con Java 7 y 8, pero esto no parece ser necesario. Java 7 hace lo correcto, donde Java 8 da un error a pesar de que hay un tipo apropiado que se puede inferir para T.