java - type - ¿Por qué esta inferencia de tipo no funciona con este escenario de expresión Lambda?
lambda expressions java (6)
Bajo el capó
Usando algunas funciones ocultas de javac
, podemos obtener más información sobre lo que está sucediendo:
$ javac -XDverboseResolution=deferred-inference,success,applicable LambdaInference.java
LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
Foo.foo(value -> true).booleanValue(); // Compile error here
^
phase: BASIC
with actuals: <none>
with type-args: no arguments
candidates:
#0 applicable method found: <T>foo(Bar<T>)
(partially instantiated to: (Bar<Object>)Object)
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
Foo.foo(value -> true).booleanValue(); // Compile error here
^
instantiated signature: (Bar<Object>)Object
target-type: <none>
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: error: cannot find symbol
Foo.foo(value -> true).booleanValue(); // Compile error here
^
symbol: method booleanValue()
location: class Object
1 error
Esta es mucha información, desglosémosla.
LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
Foo.foo(value -> true).booleanValue(); // Compile error here
^
phase: BASIC
with actuals: <none>
with type-args: no arguments
candidates:
#0 applicable method found: <T>foo(Bar<T>)
(partially instantiated to: (Bar<Object>)Object)
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
fase: fase de aplicabilidad del método
reales: los argumentos reales pasaron en
tipo-args: argumentos de tipo explícito
candidatos: métodos potencialmente aplicables
los valores reales son <none>
porque nuestra lambda implícitamente tipada no es pertinente para la aplicabilidad .
El compilador resuelve tu invocación de foo
al único método llamado foo
en Foo
. Se ha instanciado parcialmente a Foo.<Object> foo
(ya que no había datos reales o args de tipo), pero eso puede cambiar en la etapa de inferencia diferida.
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
Foo.foo(value -> true).booleanValue(); // Compile error here
^
instantiated signature: (Bar<Object>)Object
target-type: <none>
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
firma instanciada: la firma completamente instanciada de foo
. Es el resultado de este paso (en este momento no se harán más inferencias de tipo sobre la firma de foo
).
target-type: el contexto en el que se realiza la llamada. Si la invocación al método es parte de una asignación, será el lado izquierdo. Si la invocación de método es en sí misma parte de una invocación de método, será el tipo de parámetro.
Como la invocación de su método está colgando, no hay un tipo de destino. Como no hay un tipo de objetivo, no se puede hacer más inferencia en foo
y se infiere que T
es un Object
.
Análisis
El compilador no usa lambdas implícitamente tipeados durante la inferencia. Hasta cierto punto, esto tiene sentido. En general, dado param -> BODY
, no podrás compilar BODY
hasta que tengas un tipo para param
. Si intenta inferir el tipo para param
de BODY
, podría provocar un problema tipo gallina y huevo. Es posible que se hagan algunas mejoras al respecto en futuras versiones de Java.
Soluciones
Foo.<Boolean> foo(value -> true)
Esta solución proporciona un argumento de tipo explícito para foo
(observe la sección with type-args
continuación). Esto cambia la instanciación parcial de la firma del método a (Bar<Boolean>)Boolean
, que es lo que desea.
LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
Foo.<Boolean> foo(value -> true).booleanValue(); // Compile error here
^
phase: BASIC
with actuals: <none>
with type-args: Boolean
candidates:
#0 applicable method found: <T>foo(Bar<T>)
(partially instantiated to: (Bar<Boolean>)Boolean)
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
Foo.<Boolean> foo(value -> true).booleanValue(); // Compile error here
^
phase: BASIC
with actuals: no arguments
with type-args: no arguments
candidates:
#0 applicable method found: booleanValue()
Foo.foo((Value<Boolean> value) -> true)
Esta solución teclea explícitamente tu lambda, lo que permite que sea pertinente a la aplicabilidad (nota with actuals
continuación). Esto cambia la instanciación parcial de la firma del método a (Bar<Boolean>)Boolean
, que es lo que desea.
LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
^
phase: BASIC
with actuals: Bar<Boolean>
with type-args: no arguments
candidates:
#0 applicable method found: <T>foo(Bar<T>)
(partially instantiated to: (Bar<Boolean>)Boolean)
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
^
instantiated signature: (Bar<Boolean>)Boolean
target-type: <none>
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
^
phase: BASIC
with actuals: no arguments
with type-args: no arguments
candidates:
#0 applicable method found: booleanValue()
Foo.foo((Bar<Boolean>) value -> true)
Igual que arriba, pero con un sabor ligeramente diferente.
LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
^
phase: BASIC
with actuals: Bar<Boolean>
with type-args: no arguments
candidates:
#0 applicable method found: <T>foo(Bar<T>)
(partially instantiated to: (Bar<Boolean>)Boolean)
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
^
instantiated signature: (Bar<Boolean>)Boolean
target-type: <none>
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
^
phase: BASIC
with actuals: no arguments
with type-args: no arguments
candidates:
#0 applicable method found: booleanValue()
Boolean b = Foo.foo(value -> true)
Esta solución proporciona un objetivo explícito para su llamada al método (vea target-type
continuación). Esto permite que la instanciación diferida infiera que el parámetro tipo debe ser Boolean
lugar de Object
(ver instantiated signature
continuación).
LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
Boolean b = Foo.foo(value -> true);
^
phase: BASIC
with actuals: <none>
with type-args: no arguments
candidates:
#0 applicable method found: <T>foo(Bar<T>)
(partially instantiated to: (Bar<Object>)Object)
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
Boolean b = Foo.foo(value -> true);
^
instantiated signature: (Bar<Boolean>)Boolean
target-type: Boolean
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
Renuncia
Este es el comportamiento que está ocurriendo. No sé si esto es lo que se especifica en el JLS. Podía cavar alrededor y ver si podía encontrar la sección exacta que especifica este comportamiento, pero escribir notación de inferencia me da dolor de cabeza.
Esto tampoco explica completamente por qué el cambio de Bar
para usar un Value
sin formato solucionaría este problema:
LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
Foo.foo(value -> true).booleanValue();
^
phase: BASIC
with actuals: <none>
with type-args: no arguments
candidates:
#0 applicable method found: <T>foo(Bar<T>)
(partially instantiated to: (Bar<Object>)Object)
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
Foo.foo(value -> true).booleanValue();
^
instantiated signature: (Bar<Boolean>)Boolean
target-type: <none>
where T is a type-variable:
T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
Foo.foo(value -> true).booleanValue();
^
phase: BASIC
with actuals: no arguments
with type-args: no arguments
candidates:
#0 applicable method found: booleanValue()
Por alguna razón, al cambiarlo para usar un Value
procesar, la instanciación diferida puede inferir que T
es Boolean
. Si tuviera que especular, supongo que cuando el compilador intenta ajustar la lambda al Bar<T>
, puede inferir que T
es Boolean
mirando el cuerpo de la lambda. Esto implica que mi análisis anterior es incorrecto. El compilador puede realizar una inferencia de tipo en el cuerpo de una lambda, pero solo en variables de tipo que solo aparecen en el tipo de retorno.
Tengo un escenario extraño donde la inferencia de tipo no funciona como esperaba al usar una expresión lambda. Aquí hay una aproximación de mi escenario real:
static class Value<T> {
}
@FunctionalInterface
interface Bar<T> {
T apply(Value<T> value); // Change here resolves error
}
static class Foo {
public static <T> T foo(Bar<T> callback) {
}
}
void test() {
Foo.foo(value -> true).booleanValue(); // Compile error here
}
El error de compilación que obtengo en la penúltima línea es
El método booleanValue () no está definido para el tipo Object
si lanzo la lambda a Bar<Boolean>
:
Foo.foo((Bar<Boolean>)value -> true).booleanValue();
o si cambio la firma del método de Bar.apply
para usar tipos sin Bar.apply
:
T apply(Value value);
entonces el problema desaparece La forma en que espero que esto funcione es que:
-
Foo.foo
llamada deFoo.foo
debería inferir un tipo de retorno deboolean
-
value
en el lambda se debe inferir aValue<Boolean>
.
¿Por qué esta inferencia no funciona como se espera y cómo puedo cambiar esta API para que funcione como se esperaba?
Problema
El valor inferirá que se escriba Value<Object>
porque interpretó el lambda incorrecto. Piénselo, como llame con la lambda directamente al método de aplicar. Entonces lo que haces es:
Boolean apply(Value value);
y esto se infiere correctamente a:
Boolean apply(Value<Object> value);
ya que no ha dado el tipo de Valor.
Solución simple
Llame a la lambda de manera correcta:
Foo.foo((Value<Boolean> value) -> true).booleanValue();
esto se deducirá a:
Boolean apply(Value<Boolean> value);
(Mi) Solución recomendada
Su solución debe ser un poco más clara. Si desea una devolución de llamada, necesita un valor de tipo que será devuelto.
Creé una interfaz de devolución de llamada genérica, una clase de valor genérica y una clase de uso para mostrar cómo usarla.
Interfaz de devolución de llamada
/**
*
* @param <P> The parameter to call
* @param <R> The return value you get
*/
@FunctionalInterface
public interface Callback<P, R> {
public R call(P param);
}
Clase de valor
public class Value<T> {
private final T field;
public Value(T field) {
this.field = field;
}
public T getField() {
return field;
}
}
UsingClass clase
public class UsingClass<T> {
public T foo(Callback<Value<T>, T> callback, Value<T> value) {
return callback.call(value);
}
}
TestApp con principal
public class TestApp {
public static void main(String[] args) {
Value<Boolean> boolVal = new Value<>(false);
Value<String> stringVal = new Value<>("false");
Callback<Value<Boolean>, Boolean> boolCb = (v) -> v.getField();
Callback<Value<String>, String> stringCb = (v) -> v.getField();
UsingClass<Boolean> usingClass = new UsingClass<>();
boolean val = usingClass.foo(boolCb, boolVal);
System.out.println("Boolean value: " + val);
UsingClass<String> usingClass1 = new UsingClass<>();
String val1 = usingClass1.foo(stringCb, stringVal);
System.out.println("String value: " + val1);
// this will give you a clear and understandable compiler error
//boolean val = usingClass.foo(boolCb, stringVal);
}
}
Al igual que otras respuestas, también espero que alguien más inteligente pueda señalar por qué el compilador no puede inferir que T
es Boolean
.
Una forma de ayudar al compilador a hacer lo correcto, sin requerir ningún cambio en el diseño de clase / interfaz existente, es declarar explícitamente el tipo de parámetro formal en su expresión lambda. Entonces, en este caso, al declarar explícitamente que el tipo del parámetro de value
es Value<Boolean>
.
void test() {
Foo.foo((Value<Boolean> value) -> true).booleanValue();
}
La inferencia en el tipo de parámetro lambda no puede depender del cuerpo lambda.
El compilador se enfrenta a un trabajo difícil tratando de dar sentido a las expresiones implícitas lambda
foo( value -> GIBBERISH )
El tipo de value
debe inferir antes de poder compilar GIBBERISH, porque en general la interpretación de GIBBERISH depende de la definición de value
.
(En su caso especial, GIBBERISH pasa a ser una constante simple independiente de value
).
Javac debe inferir Value<T>
primero para el value
parámetro; no hay restricciones en el contexto, por lo tanto, T=Object
. Luego, lambda body es compilado y reconocido como booleano, compatible con T
Después de realizar el cambio a la interfaz funcional, el tipo de parámetro lambda no requiere deducción; T permanece no incluido. A continuación, compila el cuerpo lambda y el tipo de retorno parece ser booleano, que se establece como un límite inferior para T
Otro ejemplo que demuestra el problema
<T> void foo(T v, Function<T,T> f) { ... }
foo("", v->42); // Error. why can''t javac infer T=Object ?
T se infiere que es String
; el cuerpo de lambda no participó en la inferencia.
En este ejemplo, el comportamiento de javac nos parece muy razonable; es probable que haya evitado un error de programación. No quieres que la inferencia sea demasiado poderosa; si todo lo que escribimos se compila de alguna manera, perderemos la confianza en que el compilador nos encuentre errores.
Hay otros ejemplos donde el cuerpo lambda parece proporcionar restricciones inequívocas, sin embargo, el compilador no puede usar esa información. En Java, los tipos de parámetros lambda deben corregirse primero, antes de poder mirar el cuerpo. Esta es una decisión deliberada. Por el contrario, C # está dispuesto a probar diferentes tipos de parámetros y ver qué hace que el código se compile. Java lo considera demasiado arriesgado.
En cualquier caso, cuando la lambda implícita falla, lo que sucede con bastante frecuencia, proporcione tipos explícitos para los parámetros de lambda; en su caso, (Value<Boolean> value)->true
La manera más fácil de arreglar esto es una declaración de tipo en la llamada a foo
del método:
Foo.<Boolean>foo(value -> true).booleanValue();
Editar: No puedo encontrar la documentación específica sobre por qué es necesario, al igual que todos los demás. Sospeché que podría ser debido a los tipos primitivos, pero eso no estaba bien. De todos modos, esta sintaxis se llama utilizando un Tipo de objetivo . También Target Type en Lambdas . Sin embargo, las razones me eluden, no puedo encontrar documentación sobre por qué este caso de uso particular es necesario.
Editar 2: encontré esta pregunta relevante:
La inferencia de tipo genérico no funciona con el encadenamiento de métodos?
Parece que es porque estás encadenando los métodos aquí. De acuerdo con los comentarios JSR mencionados en la respuesta aceptada allí, fue una omisión deliberada de la funcionalidad porque el compilador no tiene una forma de pasar información de tipo genérico inferido entre las llamadas a métodos encadenados en ambas direcciones. Como resultado, todo el tipo de borrado por el tiempo llega a la llamada a booleanValue
. Agregar el tipo de destino elimina este comportamiento proporcionando la restricción de forma manual en lugar de dejar que el compilador tome la decisión utilizando las reglas descritas en JLS §18 , lo que no parece mencionarlo en absoluto. Esta es la única información que se me ocurre. Si alguien encuentra algo mejor, me encantaría verlo.
No sé por qué, pero debe agregar un tipo de devolución por separado:
public class HelloWorld{
static class Value<T> {
}
@FunctionalInterface
interface Bar<T,R> {
R apply(Value<T> value); // Return type added
}
static class Foo {
public static <T,R> R foo(Bar<T,R> callback) {
return callback.apply(new Value<T>());
}
}
void test() {
System.out.println( Foo.foo(value -> true).booleanValue() ); // No compile error here
}
public static void main(String []args){
new HelloWorld().test();
}
}
un tipo inteligente probablemente pueda explicar eso.