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:
- Todas as outras coisas iguais, o compilador infere o tipo mais específico possível.
- No entanto, se ele puder encontrar uma substituição para um parâmetro de tipo que atenda a todos os requisitos, a compilação será bem-sucedida, por mais vaga que seja a substituição.
- Pois
with
existe uma substituição (reconhecidamente vaga) que satisfaz todos os requisitos em R
:Serializable
- Pois
withX
, a introdução do parâmetro de tipo adicional F
força o compilador a resolver R
primeiro, sem considerar a restrição F extends Function<T,R>
. R
resolve para (muito mais específico), o String
que significa que a inferência de F
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 a withX
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:
static Runnable x = () -> System.out.println();
Seu caso de uso não é diferente.
Outro motivo pelo qual eu seria cauteloso ao usar seu withX
método é o F
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 use T
, é 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 sua withX
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:
public class Builder<T> {
public final class With<R> {
private final Function<T,R> method;
private With(Function<T,R> method) {
this.method = method;
}
public Builder<T> of(R value) {
// TODO: Body of your old 'with' method goes here
return Builder.this;
}
}
public <R> With<R> with(Function<T,R> method) {
return new With<>(method);
}
}
Isso pode ser usado da seguinte maneira:
b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error
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:
- O primeiro método configura uma classe (
With
) que define o tipo com base na referência do método.
- O método scond (
of
) restringe o tipo de value
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
e withX
. 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
para Supplier
, por isso há menos tipos e parâmetros em jogo. Aqui está um trecho completo que reproduz o comportamento que você descreveu:
public class TypeInference {
static long getLong() { return 1L; }
static <R> void with(Supplier<R> supplier, R value) {}
static <R, F extends Supplier<R>> void withX(F supplier, R value) {}
public static void main(String[] args) {
with(TypeInference::getLong, "Not a long"); // Compiles
withX(TypeInference::getLong, "Also not a long"); // Does not compile
}
}
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:
with(TypeInference::getLong, "Not a long");
O conjunto de limites inicial, B 0 , é:
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 com Supplier<R>
"Not a long"
é compatível com R
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ção Serializable
), 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 com Supplier<R>
- Variáveis de entrada: nenhuma
- Variáveis de saída:
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:
withX(TypeInference::getLong, "Also not a long");
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:
Se m
é um método genérico e a chamada de método não fornece argumentos de tipo explícitas, uma expressão lambda explicitamente digitado ou uma expressão de referência método exacto para o qual o tipo de alvo correspondente (como derivado a partir da assinatura de m
) é um parâmetro de tipo m
.
Portanto, a restrição inicial definida para a inferência de aplicabilidade , C , é:
"Also not a long"
é compatível com R
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ção String
), 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 com F
- Variáveis de entrada:
F
- Variáveis de saída: nenhuma
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:
Dado um conjunto de variáveis de inferência a serem resolvidas, V
seja a união desse conjunto e todas as variáveis das quais depende a resolução de pelo menos uma variável neste conjunto.
Pelo segundo ligado em B 2 , a resolução de F
depende R
, por isso V := {F, R}
.
Escolhemos um subconjunto de V
acordo com a regra:
deixar { α1, ..., αn }
ser um subconjunto não vazio de variáveis não instanciadas em V
tal que i) para todos i (1 ≤ i ≤ n)
, se αi
depende da resolução de uma variável β
, em seguida, quer β
tem uma instanciação ou existe alguma j
tal que β = αj
; e ii) não existe um subconjunto apropriado não vazio de { α1, ..., αn }
com essa propriedade.
O único subconjunto V
que satisfaz essa propriedade é {R}
.
Usando o terceiro bound ( String <: R
), instanciamos R = String
e incorporamos isso em nosso conjunto de bound. R
agora está resolvido e o segundo limite se torna efetivamente F <: 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 com Supplier<String>
- ... reduz a
Long
é compatível com String
- ... que reduz a falso
... 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:
- Onde o tipo de valor é um subtipo do método return type (
Integer <: Number
)
- Onde a interface funcional é contravariante no tipo inferido (ou seja, em
Consumer
vez de Supplier
)
Em particular, 3 das invocações fornecidas se destacam como potencialmente sugerindo um comportamento do compilador 'diferente' ao descrito nas explicações:
t.lettBe(t::setNumber, "NaN"); // Does not compile :-)
t.letBeX(t::getNumber, 2); // !!! Does not compile :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)
O segundo desses 3 passará exatamente pelo mesmo processo de inferência que o withX
anterior (basta substituir Long
por Number
e String
com Integer
). 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:
- A restrição no primeiro parâmetro (
t::setNumber
é compatível com Consumer<R>
) será reduzida para, em R <: Number
vez de Number <: R
como para Supplier<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.