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

javacTiene 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:
withhay una sustitución (ciertamente vaga) que satisface todos los requisitos sobreR:SerializablewithX, la introducción del parámetro de tipo adicionalFobliga al compilador a resolverRprimero, sin considerar la restricciónF extends Function<T,R>.Rse resuelve en (mucho más específico), loStringque significa que la inferencia deFfalla.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 awithXparece 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
withXmétodo es elFpará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
Tsea, pero quiero estar seguro de que donde sea que lo useTes 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".
Fen suwithXsolo 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
withmé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
withXhace. 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 devaluecompatibilidad 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
withywithX. 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,
FunctionporSupplierlo 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:
withTenemos:
El conjunto límite inicial, B 0 , es:
R <: ObjectTodas 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::getLonges compatible conSupplier<R>"Not a long"es compatible conREsto 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::getLonges compatible conSupplier<R>REsto 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!
withXTenemos:
El conjunto límite inicial, B 0 , es:
R <: ObjectF <: 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 conREsto 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::getLonges compatible conFFNuevamente, 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
Vsiguiente manera:Por el segundo límite en B 2 , la resolución de
Fdepende deR, entoncesV := {F, R}.Elegimos un subconjunto de
Vacuerdo con la regla:El único subconjunto de
Veso satisface esta propiedad es{R}.Usando el tercer límite (
String <: R), instanciamosR = Stringe incorporamos esto a nuestro conjunto de límites.Rahora está resuelto, y el segundo límite se convierte efectivamenteF <: Supplier<String>.Usando el segundo límite (revisado), instanciamos
F = Supplier<String>.Fahora está resuelto.Ahora que
Festá resuelto, podemos proceder con la reducción , utilizando la nueva restricción:TypeInference::getLonges compatible conSupplier<String>Longes 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)Consumerlugar 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
withXanterior (solo reemplaceLongconNumberyStringconInteger). 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
Consumerque 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,withpara el primero,withXpara el tercero). Solo hay un pequeño cambio que debe tener en cuenta:t::setNumberes compatible conConsumer<R>) se reducirá enR <: Numberlugar deNumber <: Rcomo 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::getLongpodrí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.