Desculpas pelo título ruim. Eu apreciaria alguma ajuda para elaborar um melhor, mas nem sei como descrever meu problema.
Digamos que eu tenha os seguintes tipos.
interface M<K, V> {}
interface F<I, O> {}
E digamos que eu crie uma classe que implemente ambas as interfaces.
record A<A1, A2> (A1 a1, A2 a2)
implements
M<A1, A2>,
F<A1, A2>
{}
Ok, legal. Agora posso usar A
em qualquer lugar que eu usaria de outra forma M
ou F
.
Agora aqui está meu problema. Estou tentando fazer o seguinte, e falhando com erros do compilador.
public class DoublyNestedGenerics
{
interface M<K, V> {}
interface F<I, O> {}
record A <A1, A2> (A1 a1, A2 a2)
implements
M<A1, A2>,
F<A1, A2>
{}
private
static
<
D1,
D2,
D extends
M<D1, D2>
& F<D1, D2>
>
D
canItWork(final D1 d1, final D2 d2)
{
// return new A(d1, d2);
throw new UnsupportedOperationException();
}
}
Ok, então tudo compila. Mas se eu descomentar o retorno e substituir a exceção, recebo o seguinte erro de compilação.
DoublyNestedGenerics.java:28: warning: [rawtypes] found raw type: A
return new A(d1, d2);
^
missing type arguments for generic class A<A1,A2>
where A1,A2 are type-variables:
A1 extends Object declared in record A
A2 extends Object declared in record A
Ok, faz sentido. Deixe-me mudar o retorno para ser isso.
return new A<>(d1, d2);
Pressionar compilar me dá o seguinte erro do compilador.
DoublyNestedGenerics.java:28: error: incompatible types: cannot infer type arguments for A<>
return new A<>(d1, d2);
^
reason: no instance(s) of type variable(s) A1,A2 exist so that A<A1,A2> conforms to D
where A1,A2,D,D1,D2 are type-variables:
A1 extends Object declared in record A
A2 extends Object declared in record A
D extends M<D1,D2>,F<D1,D2> declared in method <D1,D2,D>canItWork(D1,D2)
D1 extends Object declared in method <D1,D2,D>canItWork(D1,D2)
D2 extends Object declared in method <D1,D2,D>canItWork(D1,D2)
1 error
Não entendi completamente o erro, então decidi simplificar o problema.
Eu alterei o método para usar alguns tipos hardcoded, em vez de apenas D1
and D2
. Aqui está a aparência do novo método.
private
static
<
D extends
M<Integer, Integer>
& F<Integer, Integer>
>
D
canItWorkAttempt2(final Integer d1, final Integer d2)
{
// return new A<>(d1, d2);
throw new UnsupportedOperationException();
}
Trocar os comentários e pressionar compilar me deu o seguinte erro do compilador.
DoublyNestedGenerics.java:44: error: incompatible types: cannot infer type arguments for A<>
return new A<>(d1, d2);
^
reason: no instance(s) of type variable(s) A1,A2 exist so that A<A1,A2> conforms to D
where A1,A2,D are type-variables:
A1 extends Object declared in record A
A2 extends Object declared in record A
D extends M<Integer,Integer>,F<Integer,Integer> declared in method <D>canItWorkAttempt2(Integer,Integer)
1 error
Ok, muito menor e, portanto, mais fácil de analisar.
Uma coisa que me chamou a atenção foi que dizia A1 extends Object declared in record A
.
Bem, isso não está certo -- deveria ser Integer
, não Object
. Talvez a inferência precise de ajuda. Então, alterei o retorno para ser isto.
return new A<Integer, Integer>(d1, d2);
O que resultou no seguinte erro do compilador.
DoublyNestedGenerics.java:44: error: incompatible types: A<Integer,Integer> cannot be converted to D
return new A<Integer, Integer>(d1, d2);
^
where D is a type-variable:
D extends M<Integer,Integer>,F<Integer,Integer> declared in method <D>canItWorkAttempt2(Integer,Integer)
1 error
Progresso! Agora comecei a ter suspeitas e, para confirmá-las, decidi simplificar significativamente meu problema. Criei a seguinte classe.
record N (Integer n1, Integer n2)
implements
M<Integer, Integer>,
F<Integer, Integer>
{}
Compilado sem problemas. Lindo, vamos tentar mudar o tipo de retorno para usar isto em vez disso. Aqui está o que eu mudei para o retorno.
return new N(d1, d2);
Com grandes esperanças, apertei o botão de compilar.
DoublyNestedGenerics.java:50: error: incompatible types: N cannot be converted to D
return new N(d1, d2);
^
where D is a type-variable:
D extends M<Integer,Integer>,F<Integer,Integer> declared in method <D>canItWorkAttempt2(Integer,Integer)
1 error
Muito decepcionante. Mas outra coisa me ocorreu. No erro, dizia where D is a type-variable: D extends M<Integer,Integer>,F<Integer,Integer>
.
Eles usaram a palavra extends . Por desespero, tentei adicionar os seguintes tipos.
interface C<C1, C2> extends M<C1, C2>, F<C1, C2> {}
record N2 (Integer n1, Integer n2) implements C<Integer, Integer> {}
E então, a partir daí, mudei meu retorno para dizer isso.
final C<Integer, Integer> blah = new N2(d1, d2);
return blah;
Então pressionei compilar.
DoublyNestedGenerics.java:55: error: incompatible types: C<Integer,Integer> cannot be converted to D
return blah;
^
where D is a type-variable:
D extends M<Integer,Integer>,F<Integer,Integer> declared in method <D>canItWorkAttempt2(Integer,Integer)
1 error
Neste ponto, estou irritado.
A mensagem de erro está de alguma forma me dizendo que C<Integer,Integer> cannot be converted to D
. E também está me dizendo que D is a type-variable
, especificamente que D extends M<Integer,Integer>,F<Integer,Integer>
QUE É EXATAMENTE O QUE C FAZ . E ainda assim, ainda não funciona?
O que me traz aqui. O que estou esquecendo? E, novamente, gostaria de ajuda para elaborar um título melhor, se alguém estiver disposto a fazer sugestões.
Por fim, aqui está o código completo, caso tenha sido mais difícil de acompanhar.
public class DoublyNestedGenerics
{
interface M<K, V> {}
interface F<I, O> {}
record A <A1, A2> (A1 a1, A2 a2)
implements
M<A1, A2>,
F<A1, A2>
{}
record N (Integer n1, Integer n2)
implements
M<Integer, Integer>,
F<Integer, Integer>
{}
interface C<C1, C2> extends M<C1, C2>, F<C1, C2> {}
record N2 (Integer n1, Integer n2) implements C<Integer, Integer> {}
private
static
<
D1,
D2,
D extends
M<D1, D2>
& F<D1, D2>
>
D
canItWork(final D1 d1, final D2 d2)
{
// return new A<>(d1, d2);
throw new UnsupportedOperationException();
}
private
static
<
D extends
M<Integer, Integer>
& F<Integer, Integer>
>
D
canItWorkAttempt2(final Integer d1, final Integer d2)
{
final C<Integer, Integer> blah = new N2(d1, d2);
return blah;
// return new N(d1, d2);
// throw new UnsupportedOperationException();
}
}
O que você está esquecendo é que
D
não é parametrizado pelocanItWork
método. É parametrizado pelo chamador do método. Você está certo queA
atende aos limites deD
, mas qualquer outro tipo também poderia. OcanItWork
método não pode saber qual tipo o chamador espera.Examine o seguinte:
Aqui, o chamador de
test
espera uma instância deOtherFooBar<String, Stirng>
mas obterá uma instância deFooBar<String, String>
em vez disso. Esses dois tipos são incompatíveis. Esse cenário é o motivoreturn new FooBar<T, U>(t, u)
da falha na compilação.A
OtherFooBar<String, String> _ = test("Hello", "World!");
linha é perfeitamente legal de acordo com a Java Language Specification . Desse fato, segue-se que a implementação detest
não pode ser permitida para compilar. Caso contrário, você acabaria com um código que não é seguro para tipos, o que anula a principal razão para ter uma linguagem estaticamente tipada (como Java) em primeiro lugar. Então, como há pelo menos um caso em quereturn new FooBar<T, U>(t, u);
resultaria em código inseguro para tipos, ele deve ser rejeitado incondicionalmente (ou seja, falha na compilação).Elenco não verificado
Você pode compilar o exemplo acima (com um aviso) alterando
test
para converter o resultado:Mas há uma razão que dá um aviso, pois agora você pode facilmente, e irá, sem nenhuma outra alteração no exemplo, executar um em
ClassCastException
tempo de execução. Pior, o cast é implícito para o chamador, fazendo parecer que o código é seguro, mesmo que não seja. Você só deve usar casts não verificados quando souber que ele terá sucesso em todos os casos (há momentos em que sabemos mais do que o compilador, e é por isso que recursos como casting existem).O problema central aqui é que a inferência de tipo do Java não reconhece automaticamente que A<Integer, Integer> (ou N ou N2) é uma instância válida da variável de tipo D, mesmo que D estenda M<Integer, Integer> e F<Integer, Integer>. A principal conclusão é que os genéricos do Java são invariantes, o que significa que mesmo que C<Integer, Integer> estenda M<Integer, Integer> e F<Integer, Integer>, isso não significa automaticamente que uma instância de C<Integer, Integer> pode ser atribuída a uma variável de tipo D restrita a M<Integer, Integer> e F<Integer, Integer>.
A inferência de tipos tem dificuldades com tipos de interseção Quando você declara:
<D estende M<D1, D2> e F<D1, D2>>
Java tenta inferir D no local da chamada, mas não "sabe" inerentemente que A<D1, D2> deve ser a instância a ser retornada. Mesmo que A<D1, D2> implemente ambas as interfaces, o sistema genérico do Java precisa de uma correspondência exata de tipo para D.
Falta de correspondência direta de tipos
retornar novo A<>(d1, d2);
Aqui, A<D1, D2> está sendo retornado, mas Java não reconhece que A<D1, D2> é uma correspondência exata para D, porque D é uma variável de tipo e não uma classe explicitamente nomeada.
A variável de tipo D não é um tipo concreto A restrição:
D estende M<Inteiro, Inteiro> e F<Inteiro, Inteiro>
Significa que D pode ser qualquer tipo que implemente ambas as interfaces. Mas como D não é um tipo concreto, Java não sabe que A<Integer, Integer> é uma implementação concreta aceitável para D.
Em vez de depender de genéricos para D, você pode forçar o Java a reconhecer um tipo de retorno concreto.
Solução 1: Use conversão explícita
Modifique a instrução return para converter explicitamente:
ou para N:
Isso funciona porque você, como programador, sabe que A<Integer, Integer> satisfaz os limites de D, mas o sistema de tipos do Java não infere isso automaticamente.
Solução 2: Alterar a assinatura do método
Em vez de deixar D ser uma variável de tipo, retorne um tipo de interface concreto:
Isso funciona porque agora o método tem um tipo de retorno fixo, que se alinha com o que o Java espera.
Solução 3: Defina um método de fábrica
Se você realmente precisa manter D, você pode adicionar um método de fábrica para impor restrições de tipo:
Isso ajuda o Java a reconhecer que A<D1, D2> é um candidato válido para D