Porque é
public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}
mais rigoroso então
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}
Este é um acompanhamento de Por que o tipo de retorno lambda não é verificado no momento da compilação . Eu descobri usando o método withX()
como
.withX(MyInterface::getLength, "I am not a Long")
produz o erro de tempo de compilação desejado:
O tipo de getLength () do tipo BuilderExample.MyInterface é longo, isso é incompatível com o tipo de retorno do descritor: String
enquanto estiver usando o método with()
não.
exemplo completo:
import java.util.function.Function;
public class SO58376589 {
public static class Builder<T> {
public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
return this;
}
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
return this;
}
}
static interface MyInterface {
public Long getLength();
}
public static void main(String[] args) {
Builder<MyInterface> b = new Builder<MyInterface>();
Function<MyInterface, Long> getter = MyInterface::getLength;
b.with(getter, 2L);
b.with(MyInterface::getLength, 2L);
b.withX(getter, 2L);
b.withX(MyInterface::getLength, 2L);
b.with(getter, "No NUMBER"); // error
b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
b.withX(getter, "No NUMBER"); // error
b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
}
}
javac SO58376589.java
SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
b.with(getter, "No NUMBER"); // error
^
required: Function<MyInterface,R>,R
found: Function<MyInterface,Long>,String
reason: inference variable R has incompatible bounds
equality constraints: Long
lower bounds: String
where R,T are type-variables:
R extends Object declared in method <R>with(Function<T,R>,R)
T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
b.withX(getter, "No NUMBER"); // error
^
required: F,R
found: Function<MyInterface,Long>,String
reason: inference variable R has incompatible bounds
equality constraints: Long
lower bounds: String
where F,R,T are type-variables:
F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
R extends Object declared in method <R,F>withX(F,R)
T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
b.withX(MyInterface::getLength, "No NUMBER"); // error
^
(argument mismatch; bad return type in method reference
Long cannot be converted to String)
where R,F,T are type-variables:
R extends Object declared in method <R,F>withX(F,R)
F extends Function<T,R> declared in method <R,F>withX(F,R)
T extends Object declared in class Builder
3 errors
Exemplo estendido
O exemplo a seguir mostra o comportamento diferente do método e do parâmetro de tipo resumido em um Fornecedor. Além disso, mostra a diferença no comportamento do consumidor para um parâmetro de tipo. E mostra que não faz diferença se é um consumidor ou fornecedor para um parâmetro de método.
import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference {
Number getNumber();
void setNumber(Number n);
@FunctionalInterface
interface Method<R> {
TypeInference be(R r);
}
//Supplier:
<R> R letBe(Supplier<R> supplier, R value);
<R, F extends Supplier<R>> R letBeX(F supplier, R value);
<R> Method<R> let(Supplier<R> supplier); // return (x) -> this;
//Consumer:
<R> R lettBe(Consumer<R> supplier, R value);
<R, F extends Consumer<R>> R lettBeX(F supplier, R value);
<R> Method<R> lett(Consumer<R> consumer);
public static void main(TypeInference t) {
t.letBe(t::getNumber, (Number) 2); // Compiles :-)
t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
t.letBe(t::getNumber, 2); // Compiles :-)
t.lettBe(t::setNumber, 2); // Compiles :-)
t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
t.lettBe(t::setNumber, "NaN"); // Does not compile :-)
t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
t.letBeX(t::getNumber, 2); // !!! Does not compile :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)
t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)
t.let(t::getNumber).be(2); // Compiles :-)
t.lett(t::setNumber).be(2); // Compiles :-)
t.let(t::getNumber).be("NaN"); // Does not compile :-)
t.lett(t::setNumber).be("NaN"); // Does not compile :-)
}
}
java
generics
lambda
type-inference
jukzi
fonte
fonte
javac
uma ferramenta bruta ou uma compilação como Gradle ou Maven?Respostas:
Esta é uma pergunta realmente interessante. Receio que a resposta seja complicada.
tl; dr
Descobrir a diferença envolve uma leitura bastante aprofundada da especificação de inferência de tipo do Java , mas basicamente se resume a isso:
with
existe uma substituição (reconhecidamente vaga) que satisfaz todos os requisitos emR
:Serializable
withX
, a introdução do parâmetro de tipo adicionalF
força o compilador a resolverR
primeiro, sem considerar a restriçãoF extends Function<T,R>
.R
resolve para (muito mais específico), oString
que significa que a inferência deF
falhas.Este último ponto é o mais importante, mas também o mais ondulado. Não consigo pensar em uma maneira melhor e concisa de redigir, portanto, se você quiser mais detalhes, sugiro que leia a explicação completa abaixo.
Esse comportamento é pretendido?
Vou sair do ramo aqui e dizer não .
Não estou sugerindo que haja um bug na especificação, mais do que (no caso
withX
) os designers de linguagem levantaram as mãos e disseram "há algumas situações em que a inferência de tipos fica muito difícil, então apenas falhamos" . Embora o comportamento do compilador em relação awithX
pareça ser o que você deseja, consideraria esse um efeito colateral incidental da especificação atual, em vez de uma decisão de design com intenção positiva.Isso é importante, porque informa a pergunta Devo confiar nesse comportamento no design do meu aplicativo? Eu argumentaria que você não deveria, porque você não pode garantir que versões futuras do idioma continuem a se comportar dessa maneira.
Embora seja verdade que os designers de linguagem se esforçam muito para não quebrar os aplicativos existentes quando atualizam suas especificações / design / compilador, o problema é que o comportamento no qual você deseja confiar é aquele em que o compilador falha atualmente (ou seja, não é um aplicativo existente ). As atualizações do Langauge transformam o código de não compilação em código de compilação o tempo todo. Por exemplo, o código a seguir pode ser garantido para não compilar no Java 7, mas seria compilado no Java 8:
Seu caso de uso não é diferente.
Outro motivo pelo qual eu seria cauteloso ao usar seu
withX
método é oF
próprio parâmetro. Geralmente, existe um parâmetro de tipo genérico em um método (que não aparece no tipo de retorno) para vincular os tipos de várias partes da assinatura. Está dizendo:Não me importo com o que
T
é, mas quero ter certeza de que, onde quer que eu useT
, é do mesmo tipo.Logicamente, então, esperamos que cada parâmetro de tipo apareça pelo menos duas vezes em uma assinatura de método, caso contrário "não está fazendo nada".
F
em suawithX
só aparece uma vez na assinatura, o que me sugere a utilização de um parâmetro de tipo não em linha com a intenção deste recurso da língua.Uma implementação alternativa
Uma maneira de implementar isso de uma maneira um pouco mais "comportamental" seria dividir seu
with
método em uma cadeia de 2:Isso pode ser usado da seguinte maneira:
Isso não inclui um parâmetro de tipo estranho como o seu
withX
. Ao dividir o método em duas assinaturas, ele também expressa melhor a intenção do que você está tentando fazer, do ponto de vista da segurança de tipo:With
) que define o tipo com base na referência do método.of
) restringe o tipo devalue
para ser compatível com o que você configurou anteriormente.A única maneira de uma versão futura da linguagem conseguir compilar isso é se a digitação completa for implementada, o que parece improvável.
Uma observação final para tornar tudo irrelevante: acho que o Mockito (e em particular sua funcionalidade de stubbing) pode já fazer o que você está tentando alcançar com o seu "construtor genérico seguro de tipo". Talvez você possa usar isso em vez disso?
A explicação completa (ish)
Vou trabalhar no procedimento de inferência de tipo para ambos
with
ewithX
. Isso é bastante longo, então vá devagar. Apesar de longo, ainda deixei muitos detalhes de fora. Você pode consultar a especificação para obter mais detalhes (siga os links) para se convencer de que estou certo (posso ter cometido um erro).Além disso, para simplificar um pouco as coisas, vou usar um exemplo de código mais mínimo. A principal diferença é que ele alterna para fora
Function
paraSupplier
, por isso há menos tipos e parâmetros em jogo. Aqui está um trecho completo que reproduz o comportamento que você descreveu:Vamos trabalhar com o procedimento de inferência de aplicabilidade e procedimento de inferência de tipo para cada invocação de método, por sua vez:
with
Nós temos:
O conjunto de limites inicial, B 0 , é:
R <: Object
Todas as expressões de parâmetro são pertinentes à aplicabilidade .
Portanto, a restrição inicial definida para a inferência de aplicabilidade , C , é:
TypeInference::getLong
é compatível comSupplier<R>
"Not a long"
é compatível comR
Isso reduz ao conjunto limitado B 2 de:
R <: Object
(de B 0 )Long <: R
(da primeira restrição)String <: R
(a partir da segunda restrição)Como isso não contém o limite ' false ' e (presumo) a resolução de
R
êxito (doaçãoSerializable
), a invocação é aplicável.Então, passamos à inferência do tipo de chamada .
O novo conjunto de restrições, C , com variáveis de entrada e saída associadas , é:
TypeInference::getLong
é compatível comSupplier<R>
R
Isso não contém interdependências entre as variáveis de entrada e saída , portanto, pode ser reduzido em uma única etapa, e o conjunto de limites final, B 4 , é o mesmo que B 2 . Portanto, a resolução é bem-sucedida como antes, e o compilador dá um suspiro de alívio!
withX
Nós temos:
O conjunto de limites inicial, B 0 , é:
R <: Object
F <: Supplier<R>
Somente a segunda expressão de parâmetro é pertinente à aplicabilidade . O primeiro (
TypeInference::getLong
) não é, porque atende à seguinte condição:Portanto, a restrição inicial definida para a inferência de aplicabilidade , C , é:
"Also not a long"
é compatível comR
Isso reduz ao conjunto limitado B 2 de:
R <: Object
(de B 0 )F <: Supplier<R>
(de B 0 )String <: R
(da restrição)Novamente, como isso não contém o limite ' false ' e a resolução de
R
êxito (doaçãoString
), a invocação é aplicável.Inferência do tipo de chamada mais uma vez ...
Desta vez, o novo conjunto de restrições, C , com variáveis de entrada e saída associadas , é:
TypeInference::getLong
é compatível comF
F
Novamente, não temos interdependências entre variáveis de entrada e saída . No entanto, desta vez, não é uma variável de entrada (
F
), por isso temos de resolver isso antes de tentar a redução . Então, começamos com nosso conjunto vinculado B 2 .Determinamos um subconjunto da
V
seguinte maneira:Pelo segundo ligado em B 2 , a resolução de
F
dependeR
, por issoV := {F, R}
.Escolhemos um subconjunto de
V
acordo com a regra:O único subconjunto
V
que satisfaz essa propriedade é{R}
.Usando o terceiro bound (
String <: R
), instanciamosR = String
e incorporamos isso em nosso conjunto de bound.R
agora está resolvido e o segundo limite se torna efetivamenteF <: Supplier<String>
.Usando o segundo limite (revisado), instanciamos
F = Supplier<String>
.F
agora está resolvido.Agora que
F
está resolvido, podemos prosseguir com a redução , usando a nova restrição:TypeInference::getLong
é compatível comSupplier<String>
Long
é compatível comString
... e recebemos um erro do compilador!
Notas adicionais sobre o 'Exemplo estendido'
O exemplo estendido da pergunta examina alguns casos interessantes que não são diretamente abordados pelos trabalhos acima:
Integer <: Number
)Consumer
vez deSupplier
)Em particular, 3 das invocações fornecidas se destacam como potencialmente sugerindo um comportamento do compilador 'diferente' ao descrito nas explicações:
O segundo desses 3 passará exatamente pelo mesmo processo de inferência que o
withX
anterior (basta substituirLong
porNumber
eString
comInteger
). Isso ilustra outro motivo pelo qual você não deve confiar nesse comportamento de inferência de tipo com falha para o design de sua classe, pois a falha na compilação aqui provavelmente não é um comportamento desejável.Para os outros 2 (e, de fato, qualquer uma das outras invocações que envolvam uma que
Consumer
você deseja trabalhar), o comportamento deve ser aparente se você trabalhar com o procedimento de inferência de tipo estabelecido para um dos métodos acima (ou seja,with
pela primeira vez,withX
pela terceiro). Há apenas uma pequena alteração que você precisa observar:t::setNumber
é compatível comConsumer<R>
) será reduzida para, emR <: Number
vez deNumber <: R
como paraSupplier<R>
. Isso está descrito na documentação vinculada sobre redução.Deixo como exercício para o leitor trabalhar com cuidado um dos procedimentos acima, munido desse conhecimento adicional, para demonstrar a si mesmos exatamente por que uma chamada específica é compilada ou não.
fonte
TypeInference::getLong
poderia implementarSupplier<Long>
ouSupplier<Serializable>
ouSupplier<Number>
etc, mas crucialmente ele pode implementar apenas um deles (como qualquer outra classe)! Isso é diferente de todas as outras expressões, onde os tipos implementados são todos conhecidos antecipadamente, e o compilador apenas precisa descobrir se um deles atende aos requisitos de restrição.