Limite superior do tipo de retorno genérico - interface vs. classe - código surpreendentemente válido

171

Este é um exemplo do mundo real de uma API de biblioteca de terceiros, mas simplificado.

Compilado com o Oracle JDK 8u72

Considere estes dois métodos:

<X extends CharSequence> X getCharSequence() {
    return (X) "hello";
}

<X extends String> X getString() {
    return (X) "hello";
}

Ambos relatam um aviso de "elenco não verificado" - eu entendo o porquê. O que me deixa perplexo é por que posso ligar

Integer x = getCharSequence();

e compila? O compilador deve saber que Integernão implementa CharSequence. A chamada para

Integer y = getString();

dá um erro (como esperado)

incompatible types: inference variable X has incompatible upper bounds java.lang.Integer,java.lang.String

Alguém pode explicar por que esse comportamento seria considerado válido? Como isso seria útil?

O cliente não sabe que esta chamada é insegura - o código do cliente é compilado sem aviso. Por que a compilação não alertou sobre isso / emitiu um erro?

Além disso, como é diferente deste exemplo:

<X extends CharSequence> void doCharSequence(List<X> l) {
}

List<CharSequence> chsL = new ArrayList<>();
doCharSequence(chsL); // compiles

List<Integer> intL = new ArrayList<>();
doCharSequence(intL); // error

Tentar passar List<Integer>dá um erro, como esperado:

method doCharSequence in class generic.GenericTest cannot be applied to given types;
  required: java.util.List<X>
  found: java.util.List<java.lang.Integer>
  reason: inference variable X has incompatible bounds
    equality constraints: java.lang.Integer
    upper bounds: java.lang.CharSequence

Se isso é relatado como um erro, por que Integer x = getCharSequence();não?

Adam Michalik
fonte
15
interessante! lançando sobre os LHS Integer x = getCharSequence();vai compilar, mas lançando sobre o RHS Integer x = (Integer) getCharSequence();falha compilação
flocos
Qual versão do compilador java você está usando? Especifique esta informação na pergunta.
Federico Peralta Schaffner
O @FedericoPeraltaSchaffner não consegue entender por que isso importa - essa é uma pergunta diretamente sobre o JLS.
Boris the Spider
@BoristheSpider Porque o mecanismo do tipo de inferência mudou para java8
Federico Peralta Schaffner
1
@FedericoPeraltaSchaffner - Marquei a pergunta já com [java-8], mas adicionei a versão do compilador no post agora.
9788 Adam Thomson #

Respostas:

184

CharSequenceé um interface. Portanto, mesmo SomeClassque não implemente CharSequence, seria perfeitamente possível criar uma classe

class SubClass extends SomeClass implements CharSequence

Portanto você pode escrever

SomeClass c = getCharSequence();

porque o tipo inferido Xé o tipo de interseção SomeClass & CharSequence.

Isso é um pouco estranho, Integerporque Integeré final, mas finalnão desempenha nenhum papel nessas regras. Por exemplo, você pode escrever

<T extends Integer & CharSequence>

Por outro lado, Stringnão é um interface, portanto, seria impossível estender SomeClasspara obter um subtipo de String, porque o java não suporta herança múltipla para classes.

Com o Listexemplo, você precisa lembrar que os genéricos não são covariantes nem contravariantes. Isso significa que se Xé um subtipo de Y, List<X>não é um subtipo nem um supertipo de List<Y>. Como Integernão implementa CharSequence, você não pode usar List<Integer>no seu doCharSequencemétodo.

Você pode, no entanto, fazer isso para compilar

<T extends Integer & CharSequence> void foo(List<T> list) {
    doCharSequence(list);
}  

Se você tem um método que retorna um List<T>como este:

static <T extends CharSequence> List<T> foo() 

você pode fazer

List<? extends Integer> list = foo();

Novamente, isso ocorre porque o tipo inferido é Integer & CharSequencee esse é um subtipo de Integer.

Os tipos de interseção ocorrem implicitamente quando você especifica vários limites (por exemplo <T extends SomeClass & CharSequence>).

Para obter mais informações, aqui está a parte do JLS, na qual explica como os limites de tipo funcionam. Você pode incluir várias interfaces, por exemplo

<T extends String & CharSequence & List & Comparator>

mas apenas o primeiro limite pode ser uma não interface.

Paul Boddington
fonte
62
Eu não tinha idéia de que você poderia colocar um &na definição genérica. 1
flakes
13
@flkes Você pode colocar mais de um, mas apenas o primeiro argumento pode ser uma não interface. <T extends String & List & Comparator>está ok, mas <T extends String & Integer>não está, porque Integernão é uma interface.
Paul Boddington
7
@PaulBoddington Existe algum uso prático para esses métodos. Por exemplo, se o tipo não for realmente usado para dados armazenados. Exemplos para isso são Collections.emptyList()bem como Optional.empty(). Eles retornam implementações de uma interface genérica, mas não armazenam nada.
precisa saber é o seguinte
6
E ninguém diz que uma classe finalem tempo de compilação estará finalem tempo de execução.
Holger
7
@Federico Peralta Schaffner: o ponto aqui é que o método getCharSequence()promete retornar o que Xo chamador precisa, que inclui retornar um tipo de extensão Integere implementação, CharSequencese o chamador precisar e, de acordo com essa promessa, é correto permitir atribuir o resultado Integer. É o método getCharSequence()que está quebrado, pois não cumpre sua promessa, mas isso não é culpa do compilador.
Holger
59

O tipo que é deduzido pelo seu compilador antes da atribuição para Xé Integer & CharSequence. Esse tipo parece estranho, porque Integeré final, mas é um tipo perfeitamente válido em Java. Em seguida, é convertido em Integer, o que é perfeitamente aceitável.

Há exatamente um valor possível para o Integer & CharSequencetipo: null. Com a seguinte implementação:

<X extends CharSequence> X getCharSequence() {
    return null;
}

A seguinte atribuição funcionará:

Integer x = getCharSequence();

Devido a esse valor possível, não há razão para que a atribuição esteja errada, mesmo que seja obviamente inútil. Um aviso seria útil.

O verdadeiro problema é a API, não o site de chamada

De fato, escrevi recentemente sobre esse anti-padrão de design de API . Você (quase) nunca deve projetar um método genérico para retornar tipos arbitrários, porque você (quase) nunca pode garantir que o tipo inferido será entregue. Uma exceção são métodos como Collections.emptyList(), no caso em que o vazio da lista (e apagamento de tipo genérico) é a razão pela qual qualquer inferência para <T>funcionará:

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}
Lukas Eder
fonte