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
with
hay una sustitución (ciertamente vaga) que satisface todos los requisitos sobre R
:Serializable
- Para
withX
, la introducción del parámetro de tipo adicional F
obliga al compilador a resolver R
primero, sin considerar la restricción F extends Function<T,R>
. R
se resuelve en (mucho más específico), lo String
que significa que la inferencia de F
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:
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 withX
método es el F
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 use T
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 su withX
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:
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 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:
- 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 value
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
y 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, Function
por Supplier
lo 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 m
es 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 V
siguiente manera:
Dado un conjunto de variables de inferencia para resolver, V
sea 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 F
depende de R
, entonces V := {F, R}
.
Elegimos un subconjunto de V
acuerdo con la regla:
dejemos que { α1, ..., αn }
sea un subconjunto no vacío de variables no desinstaladas de V
tal manera que i) para todos i (1 ≤ i ≤ n)
, si αi
depende de la resolución de una variable β
, entonces β
tiene una instanciación o hay algo j
así β = αj
; y ii) no existe un subconjunto adecuado no vacío de { α1, ..., αn }
esta propiedad.
El único subconjunto de V
eso satisface esta propiedad es {R}
.
Usando el tercer límite ( String <: R
), instanciamos R = String
e incorporamos esto a nuestro conjunto de límites. R
ahora está resuelto, y el segundo límite se convierte efectivamente F <: 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 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
Consumer
lugar 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 withX
anterior (solo reemplace Long
con Number
y String
con 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 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:
- La restricción en el primer parámetro (
t::setNumber
es compatible con Consumer<R>
) se reducirá en R <: Number
lugar de Number <: R
como 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.