Uma coleção de resumos, para ajudar os que precisam daquele empurrão.
Em Java iterar sobre uma coleção pode ser feito de muitas formas diferentes. Cabe-nos a nós encontrar a mais indicada para o nosso problema.
Relembro que ler a documentação começa a ser, cada vez mais, crucial para escrever bom código. A maior parte das operações que são necessárias já estão definidas, não vale a pena “reinventar a roda”.
Ao longo deste documento:
list
é um objeto do tipo List<ListElem>
ListElem
é um elemento da lista.De certeza já viste/escreveste um for
assim.
for (int i=0; i<list.size(); i++) {
ListElem l = list.get(i);
l.doStuff();
}
Este método funciona para a maior parte dos casos quando temos de iterar por um array (ArrayList), mas assume muito sobre o funcionamento da estrutura/classe.
Collections que implementem Iterable podem ser iteradas com este estilo de for
, chamado “foreach”.
for (ListElem l: list) {
l.doStuff();
}
Pode até ser lido, em linguagem natural, “For each ListElem l
in list do <this>”
Alternativamente, este código pode ser implementado da seguinte forma, recorrendo ao uso de um lambda (mais sobre estes numa secção mais à frente):
list.foreach(l -> l.doStuff());
Mas este for
tem, potencialmente, um problema: “Temos sempre de percorrer a lista toda,
visto que não temos a condição de paragem explícita”. [1]
Aqui entram os iteradores externos. Iterable, como já referi acima,
é uma interface, e esta garante que classes que a implementam têm o
método iterator()
que retorna um Iterator
sobre a collection.
Como podemos ver pelos javaDocs Iterator
implementa 3 métodos muito simples.
boolean hasNext()
Que retorna true
se o iterador não chegou ao fim da lista.
E next()
Que retorna um elemento da lista onde o iterador se encontra e avança o iterador para o próximo elemento. [2]
void remove()
Que remove da lista o último elemento que o next()
retornou.
Vamos então pôr isto em prática.
Iterator<ListElem> it = list.iterator();
while(it.hasNext()){
ListElem l = it.next();
l.doStuff();
if (l.isSomething()) {
it.remove();
}
}
Podemos então aqui alterar o código para que o ciclo acabe quando uma condição se verificar.
boolean flag = true;
Iterator<ListElem> it = list.iterator();
while (flag && it.hasNext()) {
ListElem l = it.next();
l.doStuff();
if (l.isSomething()) {
it.remove();
}
if (someCondition) {
flag = false;
}
}
Como cada classe Iterable
implementa o seu próprio método iterator()
podemos ter a certeza que estamos a
iterar de forma correta sobre a Collecion
(O foreach
também garante isto).
Os iteradoes internos tentam emular programação funcional para iterar sobre as Collections
.
Estas implementam (desde o Java 8) o método stream que retorna um Stream
da Collection
e sobre este podemos fazer uma imensas operações.
Importante notar que, como acontece em Programação Funcional, os streams apresentam Imutabilidade, ou seja,
enquanto que nos iteradores externos podíamos remover elementos da Collection
enquanto
iterávamos sobre estes, com streams isto não é possível. Podemos, no entanto, criar uma lista sem os elementos
que queremos remover e substituímos a lista antiga com a nova.
(Nota: A lista é “imutável” apenas no sentido em não é possível alterar que elementos que a lista original tem, mas podemos alterar os objetos nela contidos e isto vai afetar a lista original, bem como todas as instâncias do objeto em questão. Vou tentar explicar isto melhor com alguns exemplos mais a frente)
A estrutura usual de uma iteração usando stream
é a seguinte:
list.stream()
.operacoes_sobre_a_estrutura()
.converter_de_stream_para_o_tipo_necessario();
Não vale a pena listar todas as operações mas vou apresentar alguns exemplos.
Um caso muito frequente é querermos transformar uma lista de A
s numa lista de B
s.
Assumindo que ListElem
implementa int getId()
, podemos converter uma lista de ListElem
numa lista de Integer
.
List<Integer> ids = list.stream()
/*1*/.map(l -> l.getId())
/*2*/.collect(Collectors.toList);
Analisando passo a passo:
l -> l.getId()
, por exemplo, quer dizer: “para cada l
chama e guarda o resultado de getId()
como elemento da lista”, no contexto do map
. [3]Stream<Integer>
e nós precisamos de uma List<Integer>
. Para chamar o método collect()
temos de lhe passar
o Collector que este deve usar. [4]Ficamos assim com uma lista com os Ids, esta nova lista independente da original.
Outra das aplicações mais frequentes de streams é a filtragem de uma lista.
Assumindo que ListElem
implementa int getValue()
, podemos então filtrar todos os elementos com valor inferior
a x
.
public List<ListElem> getAbove(int x){
return this.list.stream()
.filter(l -> l.getValue() > x)
.collect(Collectors.toList());
}
Este método irá então retornar uma lista dos ListElem
com valor superior a x
mas atenção!, pode, se ListElem
não for
imutável, ter o defeito de não garantir o encapsulamento da classe que implementa este método.
Podemos, no entanto, resolver este problema facilmente, usando o map
.
public List<ListElem> getAbove(int x){
return this.list.stream()
.filter(l -> l.getValue() > x)
.map(l -> l.clone())
.collect(Collectors.toList());
}
Quando o lambda que passamos a um destes métodos apenas chama outro método, como é o exemplo do l -> l.getId()
podemos
utilizar uma Method Reference
com a seguinte sintaxe: <Class>::<method>
Olhando para o Exemplo 1 novamente, o código sofreria a seguinte alteração.
List<Integer> ids = list.stream()
.map(ListElem::getId)
.collect(Collectors.toList);
Por vezes o código que temos de implementar é muito complexo para ser escrito numa só linha. Nestes casos podemos, “expandir” o lambda para que seja mais legível o que estamos a fazer.
public List<ListElem> getAbove(int x) {
return this.list.stream()
.filter(l -> {
int i = l.getValue();
if (someCondition(i)) {
return true;
} else {
if (someOtherCondition(i)) {
return false;
} else {
return true;
}
}
})
.collect(Collectors.toList());
}
if
que faça break
para sair da lista antes de a percorrer toda
mas os stores são contra isto, justificando que fica menos legível. (Pessoalmente acho que depende e tem de ser visto caso a caso)
for (ListElem l: list) {
if (someCondition()) break;
l.doStuff();
}
E
no tipo de retorno do next()
deve-se a este depender do tipo de Iterator
quando este é declarado.
Por exemplo, o next()
de um Iterator<String>
vai retornar String
. A isto chamam-se genéricos e saem muito
fora do ambito do que é esperado nesta disciplina. (Logo é uma cena fixe de pesquisar quando tiveres tempo ;) )map
como o mapToInt que retorna um IntStream em
vez de um Stream
normal. Sobre este podemos fazer somatório, médias, etc.
int totalValue = list.stream()
.mapToInt(l -> l.getId())
.sum();
Collectors
e dos Streams
é muito extensa e depende de muitas classes. No entanto está cheia de
exemplos que ajudam a sua compreensão