¿Por qué Java no puede inferir un supertipo?
type-inference (6)
Con la siguiente firma:
public <R> Test<T> with(Function<T, ? super R> getter, R returnValue)
Todos sus ejemplos se compilan, excepto el tercero, que requiere explícitamente que el método tenga dos variables de tipo.
La razón por la que su versión no funciona es porque las referencias de métodos de Java no tienen un tipo específico.
En cambio, tienen el tipo que se requiere en el contexto dado.
En su caso, se infiere que
R
es
Long
debido a
4L
, pero el captador no puede tener el tipo
Function<MyInterface,Long>
porque en Java, los tipos genéricos son invariables en sus argumentos.
Todos sabemos que Long extiende el número. Entonces, ¿por qué esto no se compila? Y cómo definir el método "con" para que el programa se compile sin ninguna conversión manual.
import java.util.function.Function;
public class Builder<T> {
static public interface MyInterface {
Number getNumber();
Long getLong();
}
public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue) {
return null;//TODO
}
public static void main(String[] args) {
// works:
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// works:
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// works:
new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
// works:
new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
// compilation error: Cannot infer ...
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
// compilation error: Cannot infer ...
new Builder<MyInterface>().with(MyInterface::getNumber, Long.valueOf(4));
// compiles but also involves typecast (and Casting Number to Long is not even safe):
new Builder<MyInterface>().with( myInterface->(Long) myInterface.getNumber(), 4L);
// compiles but also involves manual conversion:
new Builder<MyInterface>().with(myInterface -> myInterface.getNumber().longValue(), 4L);
// compiles (compiler you are kidding me?):
new Builder<MyInterface>().with(castToFunction(MyInterface::getNumber), 4L);
}
static <X, Y> Function<X, Y> castToFunction(Function<X, Y> f) {
return f;
}
}
- No se pueden inferir los argumentos de tipo para
<F, R> with(F, R)
- El tipo de getNumber () del tipo Builder.MyInterface es Number, esto es incompatible con el tipo de retorno del descriptor: Long
Para el caso de uso, consulte: ¿Por qué no se verifica el tipo de retorno lambda en tiempo de compilación?
El compilador de Java en general no es bueno para inferir tipos genéricos múltiples / anidados o comodines. A menudo no puedo obtener algo para compilar sin usar una función auxiliar para capturar o inferir algunos de los tipos.
Pero, ¿realmente necesita capturar el tipo exacto de
Function
como
F
?
Si no, tal vez lo siguiente funcione, y como puede ver, también parece funcionar con subtipos de
Function
.
import java.util.function.Function;
import java.util.function.UnaryOperator;
public class Builder<T> {
public interface MyInterface {
Number getNumber();
Long getLong();
}
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
return null;
}
// example subclass of Function
private static UnaryOperator<String> stringFunc = (s) -> (s + ".");
public static void main(String[] args) {
// works
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
// works
new Builder<String>().with(stringFunc, "s");
}
}
Esta expresión :
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
puede reescribirse como:
new Builder<MyInterface>().with(myInterface -> myInterface.getNumber(), 4L);
Teniendo en cuenta la firma del método:
public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
-
R
será inferido aLong
-
F
seráFunction<MyInterface, Long>
y pasa una referencia de método que se introducirá como
Function<MyInterface, Number>
Esta es la clave:
¿cómo debe predecir el compilador que realmente desea devolver
Long
de una función con dicha firma?
No hará la derrota por ti.
Como
Number
es una superclase de
Long
y
Number
no es necesariamente un
Long
(es por eso que no se compila), tendría que emitir explícitamente por su cuenta:
new Builder<MyInterface>().with(myInterface -> (Long) myInterface.getNumber(), 4L);
haciendo que
F
sea
Function<MyIinterface, Long>
o pase argumentos genéricos explícitamente durante la llamada al método como lo hizo:
new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
y sé que
R
se verá como
Number
y código compilará.
La clave de su error está en la declaración genérica del tipo de
F
:
F extends Function<T, R>
.
La declaración que no funciona es:
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
Primero, tiene un nuevo
Builder<MyInterface>
.
La declaración de la clase por lo tanto implica
T = MyInterface
.
Según su declaración de
with
,
F
debe ser una
Function<T, R>
, que es una
Function<MyInterface, R>
en esta situación.
Por lo tanto, el
getter
parámetros debe tomar
MyInterface
como parámetro (satisfecho por el método hace referencia a
MyInterface::getNumber
y
MyInterface::getLong
) y devolver
R
, que debe ser del mismo tipo que el segundo parámetro de la función.
Ahora, veamos si esto es válido para todos sus casos:
// T = MyInterface, F = Function<MyInterface, Long>, R = Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L explicitly widened to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().<Function<MyInterface, Number>, Number>with(MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Long
// F = Function<T, not R> violates definition, therefore compilation error occurs
// Compiler cannot infer type of method reference and 4L at the same time,
// so it keeps the type of 4L as Long and attempts to infer a match for MyInterface::getNumber,
// only to find that the types don''t match up
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
Puede "solucionar" este problema con las siguientes opciones:
// stick to Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// stick to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// explicitly convert the result of getNumber:
new Builder<MyInterface>().with(myInstance -> (Long) myInstance.getNumber(), 4L);
// explicitly convert the result of getLong:
new Builder<MyInterface>().with(myInterface -> (Number) myInterface.getLong(), (Number) 4L);
Más allá de este punto, es principalmente una decisión de diseño para la cual la opción reduce la complejidad del código para su aplicación en particular, así que elija lo que más le convenga.
La razón por la que no puede hacer esto sin emitir radica en lo siguiente, de la Especificación del lenguaje Java :
La conversión de boxeo trata las expresiones de un tipo primitivo como expresiones de un tipo de referencia correspondiente. Específicamente, las siguientes nueve conversiones se denominan conversiones de boxeo :
- De tipo booleano a tipo booleano
- De tipo byte a tipo Byte
- De tipo corto a tipo corto
- De tipo char a tipo Character
- De tipo int a tipo entero
- De tipo largo a tipo largo
- De tipo flotante a tipo flotante
- De tipo doble a tipo Doble
- Del tipo nulo al tipo nulo
Como puede ver claramente, no hay una conversión de box implícito de largo a Número, y la conversión de ampliación de Largo a Número solo puede ocurrir cuando el compilador está seguro de que requiere un Número y no un Largo.
Como hay un conflicto entre la referencia del método que requiere un Número y el 4L que proporciona un Long, el compilador (por alguna razón ???) no puede dar el salto lógico de que Long es un Número y deducir que
F
es una
Function<MyInterface, Number>
.
En cambio, logré resolver el problema editando ligeramente la firma de la función:
public <R> Builder<T> with(Function<T, ? super R> getter, R returnValue) {
return null;//TODO
}
Después de este cambio, ocurre lo siguiente:
// doesn''t work, as it should not work
new Builder<MyInterface>().with(MyInterface::getLong, (Number), 4L);
// works, as it always did
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// works, as it should work
new Builder<MyInterface>().with(MyInterface::getNumber, (Number)4L);
// works, as you wanted
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
Editar:
Después de pasar más tiempo en ello, es molestamente difícil hacer cumplir la seguridad de tipo basado en getter.
Aquí hay un ejemplo de trabajo que utiliza métodos de establecimiento para hacer cumplir la seguridad de tipos de un constructor:
public class Builder<T> {
static public interface MyInterface {
//setters
void number(Number number);
void Long(Long Long);
void string(String string);
//getters
Number number();
Long Long();
String string();
}
// whatever object we''re building, let''s say it''s just a MyInterface for now...
private T buildee = (T) new MyInterface() {
private String string;
private Long Long;
private Number number;
public void number(Number number)
{
this.number = number;
}
public void Long(Long Long)
{
this.Long = Long;
}
public void string(String string)
{
this.string = string;
}
public Number number()
{
return this.number;
}
public Long Long()
{
return this.Long;
}
public String string()
{
return this.string;
}
};
public <R> Builder<T> with(BiConsumer<T, R> setter, R val)
{
setter.accept(this.buildee, val); // take the buildee, and set the appropriate value
return this;
}
public static void main(String[] args) {
// works:
new Builder<MyInterface>().with(MyInterface::Long, 4L);
// works:
new Builder<MyInterface>().with(MyInterface::number, (Number) 4L);
// compile time error, as it shouldn''t work
new Builder<MyInterface>().with(MyInterface::Long, (Number) 4L);
// works, as it always did
new Builder<MyInterface>().with(MyInterface::Long, 4L);
// works, as it should
new Builder<MyInterface>().with(MyInterface::number, (Number)4L);
// works, as you wanted
new Builder<MyInterface>().with(MyInterface::number, 4L);
// compile time error, as you wanted
new Builder<MyInterface>().with(MyInterface::number, "blah");
}
}
Con la capacidad de escribir con seguridad para construir un objeto, con suerte en algún momento en el futuro podremos devolver un
objeto de datos inmutable
desde el generador (quizás agregando un método
toRecord()
a la interfaz y especificando el generador como un
Builder<IntermediaryInterfaceType, RecordType>
), por lo que ni siquiera tiene que preocuparse por la modificación del objeto resultante.
Honestamente, es una lástima que requiera tanto esfuerzo obtener un constructor flexible de campo con seguridad de tipo, pero probablemente sea imposible sin algunas características nuevas, generación de código o una cantidad molesta de reflexión.
La parte más interesante radica en la diferencia entre esas 2 líneas, creo:
// works:
new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
// compilation error: Cannot infer ...
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
En el primer caso, la
T
es explícitamente
Number
, por lo que
4L
también es un
Number
, no hay problema.
En el segundo caso,
4L
es un
Long
, entonces
T
es un
Long
, por lo que su función no es compatible, y Java no puede saber si se refería a
Number
o
Long
.
Parece que el compilador ha usado el valor 4L para decidir que R es largo, y getNumber () devuelve un número, que no es necesariamente un largo.
Pero no estoy seguro de por qué el valor tiene prioridad sobre el método ...