Por que é
public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}
mais rigorosa depois
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}
Este é um acompanhamento sobre Porque é lambda tipo de retorno não verificados em tempo de compilação . Eu encontrei usando o método withX()
como
.withX(MyInterface::getLength, "I am not a Long")
produz o erro tempo de compilação queria:
O tipo de getLength () a partir do tipo BuilderExample.MyInterface é longo, este é incompatível com o tipo de retorno do descritor: String
enquanto estiver usando o método with()
não funciona.
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 estendida
O exemplo a seguir mostra o comportamento diferente do método e do tipo de parâmetro fervida para baixo para um fornecedor. Além disso, mostra a diferença a um comportamento do consumidor para um parâmetro de tipo. E isso mostra que não faz uma diferença wether é 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 :-)
}
}
Esta é uma pergunta muito interessante. A resposta, eu tenho medo, é complicado.
tl; dr
Trabalhando a diferença envolve uma leitura bastante aprofundada do Java especificação inferência tipo , mas basicamente se resume a isto:
- Todas as outras coisas iguais, o infere do compilador o mais específico tipo que pode.
- No entanto, se pode encontrar uma substituição para um parâmetro de tipo que satisfaz todos os requisitos, então a compilação será bem sucedida, no entanto vaga a substituição acaba por ser.
- Para
with
existe um (reconhecidamente vaga) de substituição que preencha todos os requisitos sobreR
:Serializable
- Para
withX
, a introdução do tipo de parâmetros adicionaisF
forças o compilador para resolverR
primeiro, sem considerar a restriçãoF extends Function<T,R>
.R
resolve o (muito mais específico)String
que, em seguida, significa que a inferência deF
falhar.
Este último ponto é o mais importante, mas também o mais mão-ondulado. Eu não posso pensar em um melhor maneira concisa de fraseado, por isso, se você quiser mais detalhes, eu sugiro que você leia a explicação completa abaixo.
É este o comportamento desejado?
Vou sair em um membro aqui e dizer não .
Eu não estou sugerindo que há um erro na especificação, mais que (no caso de withX
) os projetistas da linguagem colocaram suas mãos para cima e disse "há algumas situações em que tipo de inferência fica muito difícil, por isso vamos falhar" . Mesmo que o comportamento do compilador em relação ao withX
parece ser o que você quiser, eu consideraria que para ser um efeito colateral incidental da especificação atual, em vez de uma decisão de projeto destina-se positivamente.
Isto é importante, porque informa a pergunta que eu deveria contar com esse comportamento em meu projeto de candidatura? Eu diria que você não deve, porque você não pode garantir que as futuras versões da linguagem continuará a se comportar dessa maneira.
Embora seja verdade que os designers de linguagem esforçar muito para não quebrar aplicativos existentes quando eles atualizar sua especificação / design / compilador, o problema é que o comportamento que você quer contar com é aquele em que o compilador atualmente falhar (ou seja, não uma aplicação existente ). Atualizações langauge transformar código não compilar em compilar o código todo o tempo. Por exemplo, o seguinte código pode ser garantida não para compilar em Java 7, mas seria compilar em Java 8:
static Runnable x = () -> System.out.println();
O seu caso de uso não é diferente.
Outra razão eu seria cauteloso sobre a utilização do withX
método é o F
próprio parâmetro. Geralmente, um parâmetro de tipo genérico em um método (que não aparece no tipo de retorno) existe para ligar os tipos de várias partes da assinatura juntos. É dizer:
Eu não ligo para o que T
é, mas quer ter certeza de que onde quer que eu uso T
é o mesmo tipo.
Logicamente, então, esperamos que cada parâmetro do tipo a aparecer pelo menos duas vezes em uma assinatura do método, caso contrário, "ele 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 em um "comportamento desejado" um pouco mais forma seria a de dividir o seu with
método se 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);
}
}
Este pode, então, ser utilizado como se segue:
b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error
Isto não inclui um parâmetro de tipo estranho como o seu withX
faz. Ao quebrar o método em duas assinaturas, também exprime melhor a intenção de que você está tentando fazer, a partir de um ponto tipo de vista da segurança:
- Os primeiros conjuntos do método acima de uma classe (
With
) que define o tipo de base no método de referência. - O método scond (
of
) restringe o tipo devalue
ser compatível com o que você configurou anteriormente.
A única maneira de uma versão futura da língua seria capaz de compilar isso é se o pato-digitando completa implementado, o que parece improvável.
Uma nota final para fazer essa coisa toda irrelevante: Eu acho Mockito (e em particular a sua funcionalidade stubbing) pode, basicamente, já faz o que você está tentando alcançar com o seu "tipo de construtor genérico seguro". Talvez você poderia usar apenas que em vez disso?
O completo (ish) explicação
Eu estou indo para o trabalho através do procedimento de inferência de tipos para ambos with
e withX
. Isto é bastante longa, então levá-lo lentamente. Apesar de ser muito tempo, eu ainda deixei um monte de detalhes para fora. Você pode querer referir-se a especificação para mais detalhes (siga os links) para se convencer de que estou certo (I pode ter cometido um erro).
Além disso, para simplificar as coisas um pouco, eu vou usar um exemplo de código mais minimalista. 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
}
}
Trabalho de deixar passar o tipo de inferência aplicabilidade e inferência tipo de procedimento para cada invocação de método, por sua vez:
with
Nós temos:
with(TypeInference::getLong, "Not a long");
O conjunto ligado inicial, B 0 , é:
R <: Object
Todas as expressões de parâmetros são pertinentes à aplicabilidade .
Assim, o conjunto de constrangimento inicial de inferência aplicabilidade , C , é:
TypeInference::getLong
é compatível comSupplier<R>
"Not a long"
é compatível comR
Isto reduz a ligado conjunto B 2 de:
R <: Object
(a partir de B 0 )Long <: R
(A partir da primeira restrição)String <: R
(A partir da segunda restrição)
Uma vez que este não contém o 'limite falsa ', e (presumo) resolução da R
sucede (dando Serializable
), então a invocação é aplicável.
Então, vamos passar para tipo invocação inferência .
O novo conjunto de restrição, C , com associados de entrada e de saída variáveis, é:
TypeInference::getLong
é compatível comSupplier<R>
- Variáveis de entrada: nenhum
- variáveis de saída:
R
Esta não contém interdependências entre entrada e de saída variáveis, de modo que pode ser reduzido em um único passo, e o conjunto consolidada final, B 4 , é o mesmo que B 2 . Assim, resolução sucede como antes, eo compilador respira um suspiro de alívio!
withX
Nós temos:
withX(TypeInference::getLong, "Also not a long");
O conjunto ligado inicial, B 0 , é:
R <: Object
F <: Supplier<R>
Apenas a segunda expressão parâmetro é pertinentes à aplicabilidade . O primeiro ( TypeInference::getLong
) não é, porque satisfaz a 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 dem
) é um parâmetro de tipom
.
Assim, o conjunto de constrangimento inicial de inferência aplicabilidade , C , é:
"Also not a long"
é compatível comR
Isto reduz a ligado conjunto B 2 de:
R <: Object
(a partir de B 0 )F <: Supplier<R>
(a partir de B 0 )String <: R
(A partir da restrição)
Novamente, uma vez que este não contém o 'obrigado falsa ', e resolução de R
sucesso (dando String
), então a invocação é aplicável.
Tipo de invocação inferência mais uma vez ...
Desta vez, o novo conjunto de restrição, C , com associados de entrada e de saída variáveis, é:
TypeInference::getLong
é compatível comF
- variáveis de entrada:
F
- Variáveis de saída: nenhum
- variáveis de entrada:
Novamente, não temos interdependências entre entrada e saída variáveis. 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 o nosso conjunto vinculado B 2 .
Nós determinamos um subconjunto
V
da seguinte forma:Dado um conjunto de variáveis de inferência para resolver, vamos
V
ser a união deste conjunto e todas as variáveis sobre as quais a resolução de pelo menos uma variável neste conjunto depende.Pelo segundo ligado em B 2 , a resolução de
F
dependeR
, por issoV := {F, R}
.Nós escolher um subconjunto de
V
acordo com a regra:deixar
{ α1, ..., αn }
ser um subconjunto não vazio de variáveis não instanciadas emV
tal que i) para todosi (1 ≤ i ≤ n)
, seαi
depende da resolução de uma variávelβ
, em seguida, querβ
tem uma instanciação ou existe algumaj
tal queβ = αj
; e ii) não existe nenhum subconjunto apropriado não-vazia de{ α1, ..., αn }
com esta propriedade.O único subconjunto de
V
que satisfaz esta propriedade é{R}
.Usando o terceiro obrigado (
String <: R
) instanciamosR = String
e incorporar isso em nosso conjunto vinculado.R
agora é resolvido, e a segunda ligada de forma eficaz se tornaF <: Supplier<String>
.Usando o (revisto) segundo ligado, instanciamos
F = Supplier<String>
.F
agora é resolvido.
Agora que F
está resolvido, podemos prosseguir com a redução , usando a nova restrição:
TypeInference::getLong
é compatível comSupplier<String>
- ... reduz-se
Long
é compatível comString
- ... o que reduz a falsa
... e ficamos com um erro do compilador!
Notas adicionais sobre o 'exemplo estendido'
O exemplo estendido na questão olha para alguns casos interessantes que não estão directamente abrangidos pelos trabalhos acima:
- Quando o tipo de valor é um subtipo do tipo de retorno do método (
Integer <: Number
) - Onde a interface funcional é contravariante no tipo inferido (ou seja,
Consumer
em vez deSupplier
)
Em particular, três das invocações dadas destaca como potencialmente sugerindo comportamento 'diferente' compilador 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 destes 3 vai passar exatamente o mesmo processo de inferência como withX
acima (apenas substituir Long
com Number
e String
com Integer
). Isso ilustra ainda uma outra razão pela qual você não deve contar com esse comportamento inferência de tipos falha para seu projeto de classe, como a falta de compilar aqui é provavelmente não um comportamento desejável.
Para os outros 2 (e na verdade qualquer uma das outras invocações envolvendo um Consumer
que deseja trabalhar através), o comportamento deve ser aparente se você trabalhar através do processo de inferência de tipos definidos por um dos métodos acima (ou seja, with
para o primeiro, withX
para o terceiro). Há apenas uma pequena mudança que você precisa para tomar nota de:
- A restrição no primeiro parâmetro (
t::setNumber
é compatível comConsumer<R>
) irá reduzir aR <: Number
vez deNumber <: R
como faz paraSupplier<R>
. Isto é descrito na documentação relacionada na redução.
Eu deixá-lo como um exercício para o leitor a obra carfully através de um dos procedimentos acima, armados com este pedaço de conhecimento adicional, para demonstrar a si mesmos exatamente por isso que uma invocação especial faz ou não compilar.