15.6. Tipos genéricos e sub-tipos

Como já vimos anteriormente, a relação de sub-tipo é extremamente importante em programação por objetos, tanto ao nível conceptual (lembrar Liskov substitution principle) como de implementação.

Quando trabalhamos com classes e interfaces não genéricos, a definição dessa relação é algo simples de determinar. T2 é sub-tipo de T1 se:

Relembre que a relação de sub-tipo é transitiva, ou seja, se T3 é sub-tipo de T2 e T2 é sub-tipo de T1, então T3 é sub-tipo de T1.

 

Quando trabalhamos com tipos genéricos, a relação de sub-tipo não é tão fácil de detectar pois não vai de encontro à nossa intuição imediata.

Vamos ver uma sucessão de exemplos que permita perceber as regras do compilador Java em relação a genéricos e subtipos. Considere as seguintes instruções num método main:

Se executarmos estas instruções, os objetos em memória estarão organizados e ligados da seguinte forma:

Repare que, como seria de esperar, as variáveis ljco1 e ljco2 contêm ambas uma referência à mesma lista. Então, as duas últimas instruções adicionam um objeto a essa lista (através da variável ljco1) e acedem a esse mesmo objeto logo de seguida (através da variável lcjo2). Por isso é que as variáveis jcoA e jcoB apontam ambas para o mesmo objeto do tipo JogoComObjetivo.

 

Considere agora este novo excerto em que temos uma lista lj do tipo ArrayList<Jogo>:

Relembre a assinatura do método add do interface genérico List<E> que a classe ArrayList<E> implementa – boolean add(E element) . Relembre também que numa invocação de um método, podemos instanciar um parâmetro de um dado tipo com um objeto de qualquer subtipo desse tipo. Então, como o tipo JogoNumeroExatoJogadas é subtipo de Jogo, e lj é do tipo ArrayList<Jogo>, a instrução lj.add(new JogoNumeroExatoJogadas("Covix", 8)); está correta.

Suponha agora que a seguir a essas três instruções tínhamos ainda uma quarta instrução:

Do ponto de vista de compatibilidade de tipos, a instrução JogoComObjetivo jcoB = ljco1.get(0); está correta e o compilador aceitá-la-ia. No entanto, isso seria desastroso pois o objeto devolvido por ljco1.get(0) é verdadeiramente do tipo JogoNumeroExatoJogadas, o qual é incompatível com JogoComObjetivo para efeitos de atribuição. Estes dois tipos são ambos subtipos de Jogo mas não têm qualquer relação de subtipo entre eles.

Então o que é que está mal aqui?

 

O problema está na atribuição feita na segunda instrução – ArrayList<Jogo> lj = ljco1; – pois o tipo de lcjo1 não é subtipo de ArrayList<Jogo>, ou seja,

Apesar de JogoComObjetivo ser subtipo de Jogo,

ArrayList<JogoComObjetivo> NÃO é subtipo de ArrayList<Jogo>

 

Se pensarmos um pouco, nem teria sentido que fosse:

 

Mais à frente neste texto veremos quais são exatamente as regras a aplicar para determinar que um tipo genérico é subtipo de outro.

 

15.6.1. O wildcard ?

Vamos agora supor que queremos um método de nome imprimeLista, que recebe uma lista qualquer e escreve no standard output a representação textual de todos os elementos dessa lista.

A ideia é poder invocar esse método para escrever o conteúdo de várias listas de elementos de tipos diversos:

A classe LinkedList, tal como a classe ArrayList, é uma implementação do interface List, ou seja, são ambas subtipos de List.

Vamos então fazer uma primeira tentativa de construção do método imprimeLista. Como queremos tratar listas de elementos de quaisquer tipos, será que a seguinte versão serve?

NÃO.

De acordo com o que vimos acima, embora String seja subtipo de Object, ArrayList<String> não é subtipo de List<Object>. O mesmo para LinkedList<JogoComObjetivo>. Então, o main acima teria erros de compilação nas duas instruções de invocação do método imprimeLista.

Esta versão do método só pode ser invocada sobre listas de Object! O seguinte estaria correto:

 

O tipo List<Object> representa listas contendo elementos do tipo Object ou de subtipos deste. Ora, as listas do tipo List<String> só podem conter elementos do tipo String.

O que nós queremos é representar listas quaisquer, seja qual for o tipo declarado para os seus elementos.

O tipo que representa qualquer lista denota-se por List<?>.

Neste contexto, o ? é chamado de wildcard.

 

A versão desejada para o método imprimeLista é então:

A utilização do tipo Object no ciclo for-each tem todo o sentido: qualquer que seja o tipo dos elementos da lista que instancia o parâmetro lista, ele será sempre um subtipo de Object, pois todos os tipos Java são subtipos de Object.

Esta nova versão do método já pode ser invocado dando como parâmetro uma lista de elementos declarados com qualquer tipo.

Como se torna óbvio, a instrução List<?> l = new ArrayList<String>(); é perfeitamente correta também.

 

No seguinte exemplo, verificamos que é possível fazer get numa lista da qual desconhecemos o tipo dos elementos:

No entanto, não podemos adicionar nada a uma lista assim definida, ou seja, o seguinte método tem um erro de compilação na invocação do método add:

Se pensarmos bem, esta proibição tem todo o sentido:

O compilador só sabe que o parâmetro lista é uma lista; não conhece o tipo dos elementos dessa lista. O tipo real desses elementos só será conhecido em tempo de execução, e irá variar de invocação para invocação.

Então, podíamos estar a adicionar a lista um objeto cujo tipo é incompatível com o tipo real dos seus elementos. Isso daria inevitavelmente origem a um erro de execução.

Para evitar este tipo de erros, o compilador proíbe a adição de elementos a uma lista declarada com o tipo List<?>. Na verdade, é mais geral que isso: proíbe a invocação sobre métodos que tenham algum parâmetro do tipo dos elementos da lista.

Por exemplo, o compilador não deixa que sejam invocados os seguintes métodos sobre uma lista declarada com o tipo List<?>:

em que E é a variável de tipo usada na API do tipo List<E>. São precisamente os métodos que têm algum parâmetro do tipo E.

 

15.6.2. Bounded wildcards

Vamos agora supor que queremos um método de nome somaDeLista, que recebe uma lista de valores numéricos e devolve a soma dos elementos dessa lista.

A ideia é poder invocar esse método para obter a soma de várias listas valores numéricos:

Relembre que Integer e Double são as classes wrapper dos tipos primitivos int e double, respetivamente. Number é uma classe abstrata que é superclasse de Integer, Double, Float, etc.

Vamos então fazer uma primeira tentativa de construção do método somaDeLista. Como queremos tratar listas de elementos de quaisquer subtipos de Number, será que a seguinte versão serve?

Nesta fase já deve adivinhar a resposta: NÃO.

As duas primeiras invocações de somaDeLista no main acima provocam erro de compilação. Já sabemos porquê: List<Integer> e LinkedList<Double> não são subtipos de List<Number> (embora Integer e Double sejam subtipos de Number).

Segunda tentativa. Vamos generalizar o tipo dos elementos da lista parâmetro usando o wildcard ?. Será que a seguinte versão já é o que queremos?

Com esta versão, as instruções de invocação do main já não têm erros de compilação, pois List<Integer> e LinkedList<Double> são subtipos de List<?> .

Mas… a própria função somaDeLista tem um erro na instrução for( Number n: lista) {.

Tem sentido que isso aconteça! O compilador não sabe qual é o tipo dos elementos de lista… por isso não pode aceitar que sejam usados como Number. Na verdade, de acordo com o tipo do parâmetro, esta função pode receber uma lista de elementos de qualquer tipo aquando da invocação.

Para que o compilador aceite que os elementos de lista sejam encarados como Number, há que garantir que, embora o seu tipo seja desconhecido, ele é de certeza algum subtipo de Number.

Usamos bounded wildcards para representar um qualquer subtipo de um tipo dado.

O bounded wildcard ? extends T representa todos os tipos de dados que sejam subtipos de T (que, como já sabe, incluiem T).

Por exemplo, List<? extends Number> representa qualquer lista de elementos cujo tipo seja subtipo de Number.

 

Isto leva-nos à nossa terceira, e última, tentativa. Generalizamos o tipo dos elementos da lista parâmetro, ao mesmo tempo que limitamos a um sub-conjunto de todos os tipos existentes:

Desta forma, o compilador, embora não saiba qual o tipo exato dos elementos de lista, tem a certeza que é um subtipo de Number e que, por isso, pode usar o tipo Number na instrução for-each e invocar sobre cada elemento o método doubleValue definido na classe Number.

Agora finalmente o método main acima já está livre de erros de compilação, bem como o próprio método somaDeLista.

Tal como anteriormente, não podemos adicionar elementos a uma lista definida com bounded wildcards pois não há forma de o compilador saber qual o tipo real dos elementos da lista parâmetro (só em tempo de execução).

 

Repare que List<?> é o mesmo que List<? extends Object> já que todos os tipos Java são subtipos de Object, incluindo o próprio Object.

 

Além da relação extends, podemos usar a relação super para definição de bounded wildcards.

O bounded wildcard ? super T representa todos os tipos de dados que sejam supertipos de T (que, como já sabe, incluiem T).

Por exemplo, List<? super Integer> representa qualquer lista de elementos cujo tipo seja supertipo de Integer como, por exemplo, listas de Integer, listas de Number e listas de Object.

 

Suponha que quer criar um método para copiar valores numéricos de uma lista para outra, seja qual for o seu tipo (Integer, Double, Float etc).

Obtemos então o seguinte método, du uso muito geral como desejávamos, que faz uso dos dois tipos de bounded wildcards:

Verifique que é possível adicionar elementos do tipo Number a uma lista cujos elementos são de um supertipo de Number.

 

15.6.3. Regras para determinar se um tipo genérico é subtipo de outro

Várias classes genéricas muito úteis da biblioteca do Java definem métodos onde são usados wildcards e bounded wildcards. Para os podermos usar nos nossos programas, é muito importante que saibamos invocá-los e, por isso, convém saber se os valores com que queremos instanciar os seus parâmetros são legais do ponto de vista de concordância de tipos.

Para sabermos se um tipo genérico é subtipo de outro, aplicamos as seguintes regras:

A1<T1> é subtipo de A2<T2> se se verificar:

 

Apliquemos agora estas regras a alguns casos de exemplo, como exercício.

 

 

 

 

 

 

 


 

Anterior: 15.5. Herança de contratos versus redefinição

Seguinte: 15.7. Métodos genéricos