¿Por qué Java no puede inferir un supertipo?




type-inference (4)

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");

    }
}

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 .





type-inference