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:
- Todas las demás cosas iguales, el compilador infiere el tipo más específico que puede.
- Sin embargo, si puede encontrar una sustitución para un parámetro de tipo que satisfaga todos los requisitos, la compilación tendrá éxito. vaga que resulte la sustitución.
- Porque
withhay una sustitución (ciertamente vaga) que satisface todos los requisitos sobre R:Serializable
- Para
withX, la introducción del parámetro de tipo adicional Fobliga al compilador a resolver Rprimero, sin considerar la restricción F extends Function<T,R>. Rse resuelve en (mucho más específico), lo Stringque significa que la inferencia de Ffalla.
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:
static Runnable x = () -> System.out.println();
Su caso de uso no es diferente.
Otra razón por la que sería cauteloso al usar su withXmétodo es el Fpará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 use Tes 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 su withXsolo 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:
public class Builder<T> {
public final class With<R> {
private final Function<T,R> method;
private With(Function<T,R> method) {
this.method = method;
}
public Builder<T> of(R value) {
// TODO: Body of your old 'with' method goes here
return Builder.this;
}
}
public <R> With<R> with(Function<T,R> method) {
return new With<>(method);
}
}
Esto se puede usar de la siguiente manera:
b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error
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:
- El primer método configura una clase (
With) que define el tipo en función de la referencia del método.
- El segundo método (
of) restringe el tipo de valuecompatibilidad 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 withy withX. 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, Functionpor Supplierlo que hay menos tipos y parámetros en juego. Aquí hay un fragmento completo que reproduce el comportamiento que describió:
public class TypeInference {
static long getLong() { return 1L; }
static <R> void with(Supplier<R> supplier, R value) {}
static <R, F extends Supplier<R>> void withX(F supplier, R value) {}
public static void main(String[] args) {
with(TypeInference::getLong, "Not a long"); // Compiles
withX(TypeInference::getLong, "Also not a long"); // Does not compile
}
}
Analicemos la inferencia de aplicabilidad de tipo y el procedimiento de inferencia de tipo para cada invocación de método:
with
Tenemos:
with(TypeInference::getLong, "Not a long");
El conjunto límite inicial, B 0 , es:
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 con Supplier<R>
"Not a long" es compatible con R
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 (dar Serializable), 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 con Supplier<R>
- Variables de entrada: ninguna
- Variables de salida:
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:
withX(TypeInference::getLong, "Also not a long");
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:
Si mes un método genérico y la invocación del método no proporciona argumentos de tipo explícito, una expresión lambda tipada explícitamente o una expresión de referencia de método exacta para la cual el tipo de destino correspondiente (derivado de la firma de m) es un parámetro de tipo m.
Por lo tanto, la restricción inicial establecida para la inferencia de aplicabilidad , C , es:
"Also not a long" es compatible con R
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 (donaciones String), 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 con F
- Variables de entrada:
F
- Variables de salida: ninguna
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 Vsiguiente manera:
Dado un conjunto de variables de inferencia para resolver, Vsea la unión de este conjunto y todas las variables de las que depende la resolución de al menos una variable en este conjunto.
Por el segundo límite en B 2 , la resolución de Fdepende de R, entonces V := {F, R}.
Elegimos un subconjunto de Vacuerdo con la regla:
dejemos que { α1, ..., αn }sea un subconjunto no vacío de variables no desinstaladas de Vtal manera que i) para todos i (1 ≤ i ≤ n), si αidepende de la resolución de una variable β, entonces βtiene una instanciación o hay algo jasí β = αj; y ii) no existe un subconjunto adecuado no vacío de { α1, ..., αn }esta propiedad.
El único subconjunto de Veso satisface esta propiedad es {R}.
Usando el tercer límite ( String <: R), instanciamos R = Stringe incorporamos esto a nuestro conjunto de límites. Rahora está resuelto, y el segundo límite se convierte efectivamente F <: 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::getLong es compatible con Supplier<String>
- ... reduce a
Long es compatible con String
- ... que se reduce a falso
... 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:
- Donde el tipo de valor es un subtipo del método return type (
Integer <: Number)
- Donde la interfaz funcional es contravariante en el tipo inferido (es decir, en
Consumerlugar de Supplier)
En particular, 3 de las invocaciones dadas se destacan por sugerir potencialmente un comportamiento de compilador 'diferente' al descrito en las explicaciones:
t.lettBe(t::setNumber, "NaN"); // Does not compile :-)
t.letBeX(t::getNumber, 2); // !!! Does not compile :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)
El segundo de estos 3 pasará exactamente por el mismo proceso de inferencia que el withXanterior (solo reemplace Longcon Numbery Stringcon Integer). 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:
- La restricción en el primer parámetro (
t::setNumber es compatible con Consumer<R> ) se reducirá en R <: Numberlugar de Number <: Rcomo lo hace para Supplier<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.