Por que o Java não pode inferir um supertipo?

19

Todos sabemos que Long se estende Number. Então, por que isso não compila?

E como definir o método withpara que o programa seja compilado sem nenhuma conversão 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;
  }

}

  • Não é possível inferir argumento (s) de tipo para <F, R> with(F, R)
  • O tipo de getNumber () do tipo Builder.MyInterface é Number, isso é incompatível com o tipo de retorno do descritor: Long

Para o caso de uso, consulte: Por que o tipo de retorno lambda não é verificado no tempo de compilação

jukzi
fonte
Você pode postar MyInterface?
Maurice Perry
seu já dentro da classe
jukzi
Hmm, tentei, <F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue)mas consegui java.lang.Number cannot be converted to java.lang.Long), o que é surpreendente, porque não vejo de onde o compilador obtém a ideia de que o valor de retorno getterprecisará ser convertido returnValue.
jingx
@jukzi OK. Desculpe, eu perdi isso.
Maurice Perry
Mudar Number getNumber()para <A extends Number> A getNumber()faz as coisas funcionarem. Não faço ideia se é isso que você queria. Como outros já disseram, a questão é que MyInterface::getNumberpoderia ser uma função que retorna, Doublepor exemplo, e não Long. Sua declaração não permite que o compilador restrinja o tipo de retorno com base em outras informações presentes. Ao usar um tipo de retorno genérico, você permite que o compilador faça isso; portanto, ele funciona.
Giacomo Alzetta

Respostas:

9

Esta expressão:

new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

pode ser reescrito como:

new Builder<MyInterface>().with(myInterface -> myInterface.getNumber(), 4L);

Considerando a assinatura do método:

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R será inferido para Long
  • F será Function<MyInterface, Long>

e você passa uma referência de método que será inferida como Function<MyInterface, Number> Esta é a chave - como o compilador deve prever que você realmente deseja retornar Longde uma função com essa assinatura? Não fará o downcasting para você.

Desde que Numberé superclasse de LongeNumber não é necessariamente um Long(é por isso que não é compilado) - você teria que lançar explicitamente por conta própria:

new Builder<MyInterface>().with(myInterface -> (Long) myInterface.getNumber(), 4L);

fazer F para ser Function<MyIinterface, Long>ou passar argumentos genéricos explicitamente durante a chamada do método, como você fez:

new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);

e saber Rserá visto como Numbere o código será compilado.

michalk
fonte
Isso é um typecast interessante. Mas ainda estou procurando uma definição do método "with" que fará o chamador compilar sem nenhuma conversão. De qualquer forma, obrigado por essa idéia.
Jukzi # 14/19
@jukzi Você não pode. Não importa como withestá escrito. Você MJyInterface::getNumbertem o tipo Function<MyInterface, Number>so R=Numbere também R=Longo outro argumento (lembre-se de que os literais Java não são polimórficos!). Nesse ponto, o compilador para porque nem sempre é possível converter a Numberem a Long. A única maneira de corrigir isso é para mudar MyInterfacepara usar <A extends Number> Numbercomo tipo de retorno, isso faz com que o compilador tem R=Ae, em seguida, R=Longe uma vez que A extends Numberele pode substituirA=Long
Giacomo Alzetta
4

A chave para o seu erro está na declaração genérica do tipo de F: F extends Function<T, R>. A afirmação que não funciona é: new Builder<MyInterface>().with(MyInterface::getNumber, 4L);Primeiro, você tem um novo Builder<MyInterface>. A declaração da classe, portanto, implica T = MyInterface. Conforme sua declaração de with, Fdeve ser um Function<T, R>, que é um Function<MyInterface, R>nessa situação. Portanto, o parâmetro getterdeve usar um MyInterfaceparâmetro as (satisfeito pelas referências ao método MyInterface::getNumbere MyInterface::getLong) e return R, que deve ser do mesmo tipo que o segundo parâmetro da função with. Agora, vamos ver se isso vale para todos os seus 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);

Você pode "corrigir" esse problema com as seguintes opções:

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

Além desse ponto, é principalmente uma decisão de design para qual opção reduz a complexidade do código para seu aplicativo em particular; portanto, escolha o que melhor se adequa a você.

O motivo pelo qual você não pode fazer isso sem a transmissão está no seguinte, na Java Language Specification :

A conversão de boxe trata expressões de um tipo primitivo como expressões de um tipo de referência correspondente. Especificamente, as nove conversões a seguir são chamadas de conversões de boxe :

  • Do tipo booleano para o tipo booleano
  • Do tipo byte para o tipo Byte
  • Do tipo curto para o tipo Curto
  • Do tipo char para o tipo Character
  • Do tipo int para o tipo Inteiro
  • Do tipo longo para o tipo Longo
  • Do tipo float para o tipo Float
  • Do tipo double para o tipo Double
  • Do tipo nulo para o tipo nulo

Como você pode ver claramente, não há conversão implícita de box de long para Number, e a conversão de ampliação de Long para Number só pode ocorrer quando o compilador tiver certeza de que exige um Número e não um Long. Como existe um conflito entre a referência do método que requer um Número e o 4L que fornece um Long, o compilador (por algum motivo ???) não consegue dar o salto lógico que Long é um Número e deduz que Fé umFunction<MyInterface, Number> .

Em vez disso, consegui resolver o problema editando ligeiramente a assinatura da função:

public <R> Builder<T> with(Function<T, ? super R> getter, R returnValue) {
  return null;//TODO
}

Após essa alteração, ocorre o seguinte:

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

Edit:
Depois de gastar mais tempo com ele, é irritantemente difícil impor a segurança do tipo baseado em getter. Aqui está um exemplo de trabalho que usa métodos setter para impor a segurança de tipo de um construtor:

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

Com a capacidade segura de digitar para construir um objeto, esperamos que, em algum momento no futuro, possamos retornar um objeto de dados imutáveis do construtor (talvez adicionando um toRecord()método à interface e especificando o construtor como a Builder<IntermediaryInterfaceType, RecordType>), para que você nem precise se preocupar com o objeto resultante sendo modificado. Honestamente, é uma pena absoluta que exija muito esforço para obter um construtor flexível em campo com segurança de tipo, mas provavelmente é impossível sem alguns novos recursos, geração de código ou uma quantidade irritante de reflexão.

Avi
fonte
Obrigado por todo o seu trabalho, mas não vejo nenhuma melhoria no sentido de evitar o typecast manual. Meu entendimento ingênuo é que um compilador deve ser capaz de inferir (isto é, tipecast) tudo o que um humano pode.
Jukzi # 14/19
@jukzi Meu entendimento ingênuo é o mesmo, mas por qualquer motivo, não funciona dessa maneira. Eu vim com uma solução que praticamente alcança o efeito desejado, embora
Avi
Obrigado novamente. Mas sua nova proposta é muito ampla (consulte stackoverflow.com/questions/58337639 ), pois permite a compilação de ".with (MyInterface :: getNumber," EU NÃO SOU UM NÚMERO ")";
Jukzi # 14/19
sua frase "O compilador não pode inferir o tipo de referência do método e 4L ao mesmo tempo" é legal. Mas eu só quero o contrário. o compilador deve tentar Number com base no primeiro parâmetro e fazer um alargamento para Number do segundo parâmetro.
Jukzi # 14/19
11
WHAAAAAAAAAAAT? Por que o BiConsumer funciona como pretendido, enquanto o Function não? Eu não entendi a idéia. Admito que essa é exatamente a segurança que eu queria, mas infelizmente não funciona com getters. PORQUE PORQUE PORQUE.
Jukzi # 14/19
1

Parece que o compilador usou o valor 4L para decidir que R é longo e getNumber () retorna um número, que não é necessariamente um longo.

Mas não sei por que o valor tem precedência sobre o método ...

Rick
fonte
0

O compilador Java geralmente não é bom para inferir vários tipos genéricos / curinga ou curingas. Frequentemente, não consigo compilar algo sem usar uma função auxiliar para capturar ou inferir alguns dos tipos.

Mas, você realmente precisa capturar o tipo exato de Functionas F? Caso contrário, talvez os seguintes trabalhos, e como você pode ver, também funcionem com os 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");

    }
}
machfour
fonte
"with (MyInterface :: getNumber," NOT A NUMBER ")" não deve ser compilado
jukzi 15/10/1919
0

A parte mais interessante está na diferença entre essas duas linhas, eu acho:

// works:
new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
// compilation error: Cannot infer ...
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

No primeiro caso, o Té explicitamente Number, 4Ltambém Numbernão é um problema. No segundo caso, 4Lé a Long, também Té a Long, portanto sua função não é compatível e Java não pode saber se você quis dizer Numberou Long.

njzk2
fonte
0

Com a seguinte assinatura:

public <R> Test<T> with(Function<T, ? super R> getter, R returnValue)

todos os seus exemplos são compilados, exceto o terceiro, que exige explicitamente que o método tenha duas variáveis ​​de tipo.

O motivo de sua versão não funcionar é porque as referências de método do Java não possuem um tipo específico. Em vez disso, eles têm o tipo necessário no contexto fornecido. No seu caso, Ré inferido que é Longpor causa do 4L, mas o getter não pode ter o tipo Function<MyInterface,Long>porque em Java, tipos genéricos são invariantes em seus argumentos.

Hoopje
fonte
Seu código compilaria o with( getNumber,"NO NUMBER")que não é desejado. Também não é verdade que os genéricos são sempre invariante (veja stackoverflow.com/a/58378661/9549750 para uma prova que os genéricos de setters se comportam outro, em seguida, aqueles de getters)
jukzi
@ jukzi Ah, minha solução já foi proposta pela Avi. Que pena... :-). A propósito, é verdade que podemos atribuir a Thing<Cat>a uma Thing<? extends Animal>variável, mas, por covariância real, eu esperaria que a Thing<Cat>pudesse ser atribuído a a Thing<Animal>. Outros idiomas, como o Kotlin, permitem definir variáveis ​​do tipo co- e contravariante.
21419 Hoopje