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:
T1
e T2
são classes e T2
é sub-classe (extends) de T1
;T2
é uma classe e T1
é um interface e T2
implementa (implements) T1
;T1
e T2
são interfaces e T2
é sub-interface (extends) de T1
;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
:
ArrayList<JogoComObjetivo> ljco1 = new ArrayList<JogoComObjetivo>();
ArrayList<JogoComObjetivo> ljco2 = ljco1;
JogoComObjetivo jcoA = new JogoComObjetivo("Ramboa", 30);
ljco2.add(jcoA);
JogoComObjetivo jcoB = ljco1.get(0);
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>
:
xxxxxxxxxx
ArrayList<JogoComObjetivo> ljco1 = new ArrayList<JogoComObjetivo>();
ArrayList<Jogo> lj = ljco1;
lj.add(new JogoNumeroExatoJogadas("Covix", 8));
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:
xxxxxxxxxx
ArrayList<JogoComObjetivo> ljco1 = new ArrayList<JogoComObjetivo>();
ArrayList<Jogo> lj = ljco1;
lj.add(new JogoNumeroExatoJogadas("Covix", 8));
JogoComObjetivo jcoB = ljco1.get(0);
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 deJogo
,
ArrayList<JogoComObjetivo>
NÃO é subtipo deArrayList<Jogo>
Se pensarmos um pouco, nem teria sentido que fosse:
Uma ArrayList<Jogo>
é uma lista que pode conter objetos do tipo Jogo
e de qualquer dos seus subtipos (como JogoComObjetivo
e JogoNumeroExatoJogadas
);
Uma ArrayList<JogoComObjetivo>
é uma lista que pode conter objetos do tipo JogoComObjetivo
e de qualquer dos seus subtipos (para já não está nenhum definido);
Quando fazemos uma atribuição estamos a dizer que o objeto à direita da atribuição consegue fazer tudo o que se espera do tipo da variável que está à esquerda.
ArrayList<Jogo> lj = ljco1
estaríamos a dizer que o objeto referenciado por lcjo1
consegue fazer tudo o que uma lista de Jogo
consegue, como por exemplo, conter objetos dos tipos Jogo
, JogoComObjetivo
e JogoNumeroExatoJogadas
. ljco1
só pode conter objetos do tipo JogoComObjetivo
...
Mais à frente neste texto veremos quais são exatamente as regras a aplicar para determinar que um tipo genérico é subtipo de outro.
?
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:
xpublic static void main( String[] args) {
ArrayList<String> l1 = new ArrayList<String>();
l1.add("Primeiro");
l1.add("Segundo");
LinkedList<JogoComObjetivo> l2 = new LinkedList<JogoComObjetivo>();
l2.add(new JogoComObjetivo("Ramboa", 30));
l2.add(new JogoComObjetivo("Limbo", 50));
imprimeLista(l1);
imprimeLista(l2);
}
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?
xxxxxxxxxx
static void imprimeLista (List<Object> lista) {
for( Object o: lista) {
System.out.println(o);
}
}
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:
xxxxxxxxxx
public static void main( String[] args) {
ArrayList<Object> l1 = new ArrayList<Object>();
l1.add("Primeiro");
l1.add("Segundo");
l1.add(new JogoComObjetivo("Ramboa", 30));
l1.add(new JogoComObjetivo("Limbo", 50));
imprimeLista(l1);
}
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:
xxxxxxxxxx
static void imprimeLista (List<?> lista) {
for( Object o: lista) {
System.out.println(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.
xxxxxxxxxx
public static void main( String[] args) {
ArrayList<String> l1 = new ArrayList<String>();
l1.add("Primeiro");
l1.add("Segundo");
LinkedList<JogoComObjetivo> l2 = new LinkedList<JogoComObjetivo>();
l2.add(new JogoComObjetivo("Ramboa", 30));
l2.add(new JogoComObjetivo("Limbo", 50));
ArrayList<Object> l3 = new ArrayList<Object>();
l3.add("Outra");
l3.add(new JogoComObjetivo("X-Ray", 600));
l3.add(new Jogador("Pedro"));
imprimeLista(l1);
imprimeLista(l2);
imprimeLista(l3);
}
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:
xxxxxxxxxx
static Object primeiroElementoOuNull (List<?> lista) {
if(!lista.isEmpty()){
return lista.get(0);
} else {
return null;
}
}
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
:
xxxxxxxxxx
static void adicionaObjeto (List<?> lista, Object novo) {
if(novo != null) {
lista.add(novo);
}
}
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<?>
:
E set (int index, E element)
, que substitui o elemento na posição index
por element
;boolean add (E e)
, que adiciona e
no fim da lista;add
e addAll
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
.
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:
xxxxxxxxxx
public static void main( String[] args) {
List<Integer> l1 = new ArrayList<Integer>();
... // adicionar valores a l1
LinkedList<Double> l2 = new LinkedList<Double>();
... // adicionar valores a l2
List<Number> l3 = new ArrayList<Number>();
... // adicionar valores a l3
System.out.println(somaDeLista(l1));
System.out.println(somaDeLista(l2));
System.out.println(somaDeLista(l3));
}
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?
xxxxxxxxxx
static double somaDeLista(List<Number> lista) {
double soma = 0.0;
for( Number n: lista) {
soma += n.doubleValue();
}
return soma;
}
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?
xxxxxxxxxx
static double somaDeLista(List<?> lista) {
double soma = 0.0;
for( Number n: lista) {
soma += n.doubleValue();
}
return soma;
}
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 deT
(que, como já sabe, incluiemT
).Por exemplo,
List<? extends Number>
representa qualquer lista de elementos cujo tipo seja subtipo deNumber
.
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:
xxxxxxxxxx
static double somaDeLista(List<? extends Number> lista) {
double soma = 0.0;
for( Number n: lista) {
soma += n.doubleValue();
}
return soma;
}
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 queList<? extends Object>
já que todos os tipos Java são subtipos deObject
, incluindo o próprioObject
.
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 deT
(que, como já sabe, incluiemT
).Por exemplo,
List<? super Integer>
representa qualquer lista de elementos cujo tipo seja supertipo deInteger
como, por exemplo, listas deInteger
, listas deNumber
e listas deObject
.
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).
O parâmetro que representa a lista origem será então do tipo List<? extends Number>
Qual o tipo para o parâmetro que representa a lista destino? Este deverá ser tal que possa receber, sem qualquer problema, os valores da lista origem. Vamos pôr várias hipóteses e verificar que apenas uma serve o nosso objetivo:
List<Object>
: desta forma, estaríamos a limitar a utilização do método a listas destino cujos elementos fossem exatamente do tipo Object
(por exemplo List<Object>
, ArrayList<Object>
, etc) que não é o que queremos;List<?>
: este não serve porque haveria o perigo de ser instanciado com uma lista de elementos de tipo incompatível com numéricos (por exemplo, List<Jogo>
, List<String>
, etc);List<Number>
: aqui o problema é o mesmo que na primeira opção – o parâmetro só poderia ser instanciado por listas cujos elementos fossem exatamente do tipo Number
;List<? extends Number>
: com esta hipótese, a seguinte situação indesejável seria possível do ponto de vista dos tipos: a lista origem ser instanciada por uma lista de Double
e a lista destino por uma lista de Integer
, o que claramente é errado;List<? super Number>
: Finalmente, esta hipótese é a única correta pois qualquer lista cujos elementos sejam de um supertipo de Number
(que inclui Number
) pode receber os elementos de qualquer lista cujos elementos sejam de um subtipo de Number
(que inclui Number
).Obtemos então o seguinte método, du uso muito geral como desejávamos, que faz uso dos dois tipos de bounded wildcards:
xxxxxxxxxx
static void copiarLista( List<? extends Number> origem,
List<? super Number> destino) {
destino.clear();
for (Number elem: origem){
destino.add(elem);
}
}
Verifique que é possível adicionar elementos do tipo Number
a uma lista cujos elementos são de um supertipo de Number
.
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 deA2<T2>
se se verificar:
A1
é subtipo deA2
euma das seguintes, consoante a forma de
T2
:
T1
=T2
(seT2
não contém bounded wildcards)T1
é subtipo deU
(seT2
é da forma? extends U
)U
é subtipo deT1
(seT2
é da forma? super U
)
Apliquemos agora estas regras a alguns casos de exemplo, como exercício.
ArrayList<String>
é subtipo de List<String>
porque
ArrayList
é subtipo de List
eString
= String
ArrayList<Integer>
não é subtipo de List<Number>
porque
ArrayList
seja subtipo de List
,Integer
é diferente de Number
ArrayList<JogoComObjetivo>
é subtipo de List<? extends Jogo>
porque
ArrayList
é subtipo de List
eJogoComObjetivo
é subtipo de Jogo
List<List<Integer>>
não é subtipo de Collection<List<Number>>
porque
List
seja subtipo de Collection
,List<Integer>
é diferente de List<Number>
List<List<Integer>>
não é subtipo de Collection<? extends List<Number>>
porque
embora List
seja subtipo de Collection
,
List<Integer>
não é subtipo de List<Number>
porque
List
seja subtipo de List
,Integer
é diferente de Number
List<List<Integer>>
é subtipo de Collection<? extends List<? extends Number>>
porque
List
é subtipo de Collection
e
List<Integer>
é subtipo de List<? extends Number>
porque
List
é subtipo de List
eInteger
é subtipo de Number
Anterior: 15.5. Herança de contratos versus redefinição
Seguinte: 15.7. Métodos genéricos