Por que es
public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}
más estricto entonces
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}
Este es un seguimiento de por qué el tipo de retorno lambda no está marcado en tiempo de compilación . Encontré usando el métodowithX()
como
.withX(MyInterface::getLength, "I am not a Long")
produce el error de tiempo de compilación deseado:
El tipo de getLength () del tipo BuilderExample.MyInterface es largo, esto es incompatible con el tipo de retorno del descriptor: String
mientras usa el método with()
no.
ejemplo completo:
import java.util.function.Function;
public class SO58376589 {
public static class Builder<T> {
public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
return this;
}
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
return this;
}
}
static interface MyInterface {
public Long getLength();
}
public static void main(String[] args) {
Builder<MyInterface> b = new Builder<MyInterface>();
Function<MyInterface, Long> getter = MyInterface::getLength;
b.with(getter, 2L);
b.with(MyInterface::getLength, 2L);
b.withX(getter, 2L);
b.withX(MyInterface::getLength, 2L);
b.with(getter, "No NUMBER"); // error
b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
b.withX(getter, "No NUMBER"); // error
b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
}
}
javac SO58376589.java
SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
b.with(getter, "No NUMBER"); // error
^
required: Function<MyInterface,R>,R
found: Function<MyInterface,Long>,String
reason: inference variable R has incompatible bounds
equality constraints: Long
lower bounds: String
where R,T are type-variables:
R extends Object declared in method <R>with(Function<T,R>,R)
T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
b.withX(getter, "No NUMBER"); // error
^
required: F,R
found: Function<MyInterface,Long>,String
reason: inference variable R has incompatible bounds
equality constraints: Long
lower bounds: String
where F,R,T are type-variables:
F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
R extends Object declared in method <R,F>withX(F,R)
T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
b.withX(MyInterface::getLength, "No NUMBER"); // error
^
(argument mismatch; bad return type in method reference
Long cannot be converted to String)
where R,F,T are type-variables:
R extends Object declared in method <R,F>withX(F,R)
F extends Function<T,R> declared in method <R,F>withX(F,R)
T extends Object declared in class Builder
3 errors
Ejemplo extendido
El siguiente ejemplo muestra el comportamiento diferente del método y el parámetro de tipo reducido a un Proveedor. Además, muestra la diferencia con el comportamiento del consumidor para un parámetro de tipo. Y muestra que no hace una diferencia si es un Consumidor o Proveedor para un parámetro de método.
import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference {
Number getNumber();
void setNumber(Number n);
@FunctionalInterface
interface Method<R> {
TypeInference be(R r);
}
//Supplier:
<R> R letBe(Supplier<R> supplier, R value);
<R, F extends Supplier<R>> R letBeX(F supplier, R value);
<R> Method<R> let(Supplier<R> supplier); // return (x) -> this;
//Consumer:
<R> R lettBe(Consumer<R> supplier, R value);
<R, F extends Consumer<R>> R lettBeX(F supplier, R value);
<R> Method<R> lett(Consumer<R> consumer);
public static void main(TypeInference t) {
t.letBe(t::getNumber, (Number) 2); // Compiles :-)
t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
t.letBe(t::getNumber, 2); // Compiles :-)
t.lettBe(t::setNumber, 2); // Compiles :-)
t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
t.lettBe(t::setNumber, "NaN"); // Does not compile :-)
t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
t.letBeX(t::getNumber, 2); // !!! Does not compile :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)
t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)
t.let(t::getNumber).be(2); // Compiles :-)
t.lett(t::setNumber).be(2); // Compiles :-)
t.let(t::getNumber).be("NaN"); // Does not compile :-)
t.lett(t::setNumber).be("NaN"); // Does not compile :-)
}
}
java
generics
lambda
type-inference
jukzi
fuente
fuente
javac
Tiene el mismo problema al compilar con herramientas sin formato o de compilación como Gradle o Maven?Respuestas:
Esta es una pregunta realmente interesante. La respuesta, me temo, es complicada.
tl; dr
Resolver la diferencia implica una lectura bastante profunda de la especificación de inferencia de tipos de Java , pero básicamente se reduce a esto:
with
hay una sustitución (ciertamente vaga) que satisface todos los requisitos sobreR
:Serializable
withX
, la introducción del parámetro de tipo adicionalF
obliga al compilador a resolverR
primero, sin considerar la restricciónF extends Function<T,R>
.R
se resuelve en (mucho más específico), loString
que significa que la inferencia deF
falla.Este último punto es el más importante, pero también el más ondulado a mano. No puedo pensar en una mejor forma concisa de redacción, así que si quieres más detalles, te sugiero que leas la explicación completa a continuación.
¿Es este el comportamiento previsto?
Voy a arriesgarme aquí y decir que no .
No estoy sugiriendo que haya un error en la especificación, más que (en el caso de
withX
) los diseñadores de idiomas han levantado la mano y han dicho "hay algunas situaciones en las que la inferencia de tipos se vuelve demasiado difícil, así que simplemente fallaremos" . Aunque el comportamiento del compilador con respecto awithX
parece ser lo que desea, consideraría que es un efecto secundario incidental de la especificación actual, en lugar de una decisión de diseño intencionadamente positiva.Esto es importante porque informa la pregunta ¿Debo confiar en este comportamiento en el diseño de mi aplicación? Yo diría que no deberías, porque no puedes garantizar que futuras versiones del lenguaje continuarán comportándose de esta manera.
Si bien es cierto que los diseñadores de idiomas se esfuerzan mucho por no romper las aplicaciones existentes cuando actualizan sus especificaciones / diseño / compilador, el problema es que el comportamiento en el que desea confiar es aquel en el que el compilador falla actualmente (es decir, no es una aplicación existente ). Las actualizaciones de Langauge convierten el código que no se compila en código de compilación todo el tiempo. Por ejemplo, se podría garantizar que el siguiente código no se compilará en Java 7, pero sí en Java 8:
Su caso de uso no es diferente.
Otra razón por la que sería cauteloso al usar su
withX
método es elF
parámetro en sí. Generalmente, existe un parámetro de tipo genérico en un método (que no aparece en el tipo de retorno) para unir los tipos de varias partes de la firma. Está diciendo:No me importa lo que
T
sea, pero quiero estar seguro de que donde sea que lo useT
es del mismo tipo.Lógicamente, entonces, esperaríamos que cada parámetro de tipo aparezca al menos dos veces en la firma de un método, de lo contrario, "no está haciendo nada".
F
en suwithX
solo aparece una vez en la firma, lo que me sugiere el uso de un parámetro de tipo que no está en línea con la intención de esta característica del lenguaje.Una implementación alternativa
Una forma de implementar esto de una manera un poco más de "comportamiento previsto" sería dividir su
with
método en una cadena de 2:Esto se puede usar de la siguiente manera:
Esto no incluye un parámetro de tipo extraño como lo
withX
hace. Al dividir el método en dos firmas, también expresa mejor la intención de lo que está tratando de hacer, desde un punto de vista de seguridad de tipo:With
) que define el tipo en función de la referencia del método.of
) restringe el tipo devalue
compatibilidad con lo que configuró anteriormente.La única forma en que una versión futura del lenguaje podría compilar esto es si se implementa el tipeo completo, lo que parece poco probable.
Una nota final para hacer que todo esto sea irrelevante: creo que Mockito (y en particular su funcionalidad de copia de seguridad) básicamente ya podría hacer lo que está tratando de lograr con su "generador de tipos genéricos seguros". ¿Tal vez podrías usar eso en su lugar?
La explicación completa (ish)
Voy a trabajar a través del procedimiento de inferencia de tipos para ambos
with
ywithX
. Esto es bastante largo, así que tómalo con calma. A pesar de ser largo, todavía he dejado bastantes detalles. Es posible que desee consultar la especificación para obtener más detalles (siga los enlaces) para convencerse de que tengo razón (es posible que haya cometido un error).Además, para simplificar un poco las cosas, voy a usar una muestra de código más mínima. La principal diferencia es que se intercambia,
Function
porSupplier
lo que hay menos tipos y parámetros en juego. Aquí hay un fragmento completo que reproduce el comportamiento que describió:Analicemos la inferencia de aplicabilidad de tipo y el procedimiento de inferencia de tipo para cada invocación de método:
with
Tenemos:
El conjunto límite inicial, B 0 , es:
R <: Object
Todas las expresiones de parámetros son pertinentes a la aplicabilidad .
Por lo tanto, la restricción inicial establecida para la inferencia de aplicabilidad , C , es:
TypeInference::getLong
es compatible conSupplier<R>
"Not a long"
es compatible conR
Esto se reduce al conjunto de enlaces B 2 de:
R <: Object
(de B 0 )Long <: R
(desde la primera restricción)String <: R
(de la segunda restricción)Dado que esto no contiene el límite ' falso ', y (supongo) la resolución de
R
éxitos (darSerializable
), entonces la invocación es aplicable.Entonces, pasamos a la inferencia de tipo de invocación .
El nuevo conjunto de restricciones, C , con variables de entrada y salida asociadas , es:
TypeInference::getLong
es compatible conSupplier<R>
R
Esto no contiene interdependencias entre las variables de entrada y salida , por lo que puede reducirse en un solo paso, y el conjunto de límite final, B 4 , es el mismo que B 2 . Por lo tanto, la resolución tiene éxito como antes, ¡y el compilador da un suspiro de alivio!
withX
Tenemos:
El conjunto límite inicial, B 0 , es:
R <: Object
F <: Supplier<R>
Solo la expresión del segundo parámetro es pertinente a la aplicabilidad . El primero (
TypeInference::getLong
) no lo es, porque cumple con la siguiente condición:Por lo tanto, la restricción inicial establecida para la inferencia de aplicabilidad , C , es:
"Also not a long"
es compatible conR
Esto se reduce al conjunto de enlaces B 2 de:
R <: Object
(de B 0 )F <: Supplier<R>
(de B 0 )String <: R
(de la restricción)Nuevamente, dado que esto no contiene el límite ' falso ' y la resolución de los
R
éxitos (donacionesString
), entonces la invocación es aplicable.Inferencia de tipo de invocación una vez más ...
Esta vez, el nuevo conjunto de restricciones, C , con variables de entrada y salida asociadas , es:
TypeInference::getLong
es compatible conF
F
Nuevamente, no tenemos interdependencias entre las variables de entrada y salida . Sin embargo esta vez, no es una variable de entrada (
F
), por lo que hay que resolver esto antes de intentar la reducción . Entonces, comenzamos con nuestro conjunto enlazado B 2 .Determinamos un subconjunto de la
V
siguiente manera:Por el segundo límite en B 2 , la resolución de
F
depende deR
, entoncesV := {F, R}
.Elegimos un subconjunto de
V
acuerdo con la regla:El único subconjunto de
V
eso satisface esta propiedad es{R}
.Usando el tercer límite (
String <: R
), instanciamosR = String
e incorporamos esto a nuestro conjunto de límites.R
ahora está resuelto, y el segundo límite se convierte efectivamenteF <: Supplier<String>
.Usando el segundo límite (revisado), instanciamos
F = Supplier<String>
.F
ahora está resuelto.Ahora que
F
está resuelto, podemos proceder con la reducción , utilizando la nueva restricción:TypeInference::getLong
es compatible conSupplier<String>
Long
es compatible conString
... y tenemos un error de compilación!
Notas adicionales sobre el 'Ejemplo extendido'
El ejemplo extendido en la pregunta analiza algunos casos interesantes que no están cubiertos directamente por el funcionamiento anterior:
Integer <: Number
)Consumer
lugar deSupplier
)En particular, 3 de las invocaciones dadas se destacan por sugerir potencialmente un comportamiento de compilador 'diferente' al descrito en las explicaciones:
El segundo de estos 3 pasará exactamente por el mismo proceso de inferencia que el
withX
anterior (solo reemplaceLong
conNumber
yString
conInteger
). Esto ilustra otra razón más por la que no debe confiar en este comportamiento de inferencia de tipo fallido para el diseño de su clase, ya que la falla al compilar aquí probablemente no sea un comportamiento deseable.Para los otros 2 (y, de hecho, para cualquiera de las otras invocaciones que involucran un proceso por el
Consumer
que desea trabajar), el comportamiento debería ser evidente si trabaja a través del procedimiento de inferencia de tipos establecido para uno de los métodos anteriores (es decir,with
para el primero,withX
para el tercero). Solo hay un pequeño cambio que debe tener en cuenta:t::setNumber
es compatible conConsumer<R>
) se reducirá enR <: Number
lugar deNumber <: R
como lo hace paraSupplier<R>
. Esto se describe en la documentación vinculada sobre la reducción.Lo dejo como un ejercicio para que el lector trabaje cuidadosamente a través de uno de los procedimientos anteriores, armado con este conocimiento adicional, para demostrarse a sí mismo exactamente por qué una invocación particular se compila o no.
fuente
TypeInference::getLong
podría imlementSupplier<Long>
orSupplier<Serializable>
orSupplier<Number>
etc, ¡pero crucialmente solo puede implementar uno de ellos (como cualquier otra clase)! Esto es diferente de todas las demás expresiones, donde todos los tipos implementados se conocen por adelantado, y el compilador solo tiene que determinar si uno de ellos cumple con los requisitos de restricción.