¿Por qué Java no puede inferir un supertipo?


19

Todos sabemos que Long se extiende Number. Entonces, ¿por qué esto no se compila?

¿Y cómo definir el método withpara 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 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?


¿Puedes publicar MyInterface?
Maurice Perry

ya está dentro de la clase
jukzi

Hmm, intenté <F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue)pero obtuve java.lang.Number cannot be converted to java.lang.Long), lo cual es sorprendente porque no veo de dónde saca el compilador la idea de que el valor de retorno gettertendrá que convertirse returnValue.
jingx

@jukzi OK. Lo siento, me perdí eso.
Maurice Perry el

Cambiar Number getNumber()a <A extends Number> A getNumber()hace que las cosas funcionen. No tengo idea si esto es lo que querías. Como otros han dicho, el problema es que MyInterface::getNumberpodría ser una función que regrese, Doublepor ejemplo, y no Long. Su declaración no permite que el compilador limite el tipo de retorno en función de otra información presente. Al usar un tipo de retorno genérico, permite que el compilador lo haga, por lo tanto, funciona.
Giacomo Alzetta

Respuestas:


9

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 a Long
  • F estarán Function<MyInterface, Long>

y pasa una referencia de método que se introducirá ya que Function<MyInterface, Number>esta es la clave: ¿cómo debe predecir el compilador que realmente desea regresar Longde una función con dicha firma? No hará la derrota por ti.

Como Numberes una superclase de Longy Numberno es necesariamente una 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 Fque 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 know Rse verá como Numbery se compilará el código.


Eso es un tipo de letra interesante. Pero aún estoy buscando una definición del método "con" que hará que la persona que llama compile sin ningún tipo de conversión. De todos modos, gracias por esa idea.
jukzi

@jukzi No puedes. No importa cómo withesté escrito tu. Tienes que MJyInterface::getNumbertiene el tipo Function<MyInterface, Number>de modo R=Numbery luego también tienes R=Longdesde el otro argumento (recordar que los literales de Java no son polimórficos!). En este punto, el compilador se detiene porque no siempre es posible convertir Numbera a Long. La única forma de solucionar esto es cambiar MyInterfacepara usar <A extends Number> Numbercomo tipo de retorno, esto hace que el compilador tenga R=Ay luego, R=Longy ya A extends Numberque puede sustituirA=Long
Giacomo Alzetta

4

La clave de su error está en la declaración genérica del tipo de F: F extends Function<T, R>. La afirmación que no funciona es: new Builder<MyInterface>().with(MyInterface::getNumber, 4L);Primero, tienes una nueva Builder<MyInterface>. La declaración de la clase por lo tanto implica T = MyInterface. Según su declaración de with, Fdebe ser un Function<T, R>, que es un Function<MyInterface, R>en esta situación. Por lo tanto, el parámetro getterdebe tomar un MyInterfaceparámetro como (satisfecho con las referencias del método MyInterface::getNumbery MyInterface::getLong), y devolver R, que debe ser del mismo tipo que el segundo parámetro de la función with. 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 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 existe 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 Fes un 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 él, es molestamente difícil hacer cumplir la seguridad de tipo basado en captadores. Aquí hay un ejemplo de trabajo que utiliza métodos de establecimiento para hacer cumplir la seguridad de tipo 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 del generador (quizás agregando un toRecord()método a la interfaz y especificando el generador como a Builder<IntermediaryInterfaceType, RecordType>), para que ni siquiera tenga que preocuparse por la modificación del objeto resultante. Honestamente, es una vergüenza absoluta que requiera tanto esfuerzo obtener un generador de campo flexible 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.


Gracias por todo su trabajo, pero no veo ninguna mejora para evitar la conversión manual. Mi ingenuo entendimiento es que un compilador debería poder inferir (es decir, encasillar) todo lo que un humano puede.
jukzi

@jukzi Mi ingenua comprensión es la misma, pero por alguna razón no funciona de esa manera. Sin embargo, he encontrado una solución que prácticamente logra el efecto deseado
Avi

Gracias de nuevo. Pero su nueva propuesta es demasiado amplia (consulte stackoverflow.com/questions/58337639 ) ya que permite la compilación de ".with (MyInterface :: getNumber," NO SOY UN NÚMERO ")";
jukzi

su oración "El compilador no puede inferir el tipo de referencia de método y 4L al mismo tiempo" es genial. Pero solo lo quiero al revés. el compilador debe intentar Number basado en el primer parámetro y ampliarlo a Number del segundo parámetro.
jukzi

1
WHAAAAAAAAAAAT? ¿Por qué BiConsumer funciona según lo previsto mientras que Function no? No entiendo la idea. Admito que este es exactamente el tipo de letra que quería, pero desafortunadamente no funciona con getters. POR QUÉ POR QUÉ
jukzi

1

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 ...


0

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 necesitas capturar el tipo exacto de Functioncomo 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");

    }
}

"with (MyInterface :: getNumber," NOT A NUMBER ")" no debe compilarse
jukzi

0

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, el Tes explícitamente Number, por 4Llo que también es un Number, no hay problema. En el segundo caso, 4Les a Long, también lo Tes Long, por lo que su función no es compatible y Java no puede saber si quiso decir Numbero no Long.


0

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, Rse infiere que se Longdebe 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.


Su código compilaría lo with( getNumber,"NO NUMBER")que no se desea. Además, no es cierto que los genéricos son siempre invariables (ver stackoverflow.com/a/58378661/9549750 de demostrar que los genéricos de los emisores se comportan excepto los de captadores)
jukzi

@jukzi Ah, mi solución ya fue propuesta por Avi. Demasiado... :-). Por cierto, es cierto que podemos asignar a Thing<Cat>a una Thing<? extends Animal>variable, pero para una covarianza real, esperaría que unThing<Cat> se pueda asignar a a Thing<Animal>. Otros lenguajes, como Kotlin, permiten definir variables de tipo covariable y contravariante.
Hoopje
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.