Alura > Cursos de Programação > Cursos de Java > Conteúdos de Java > Primeiras aulas do curso Praticando Java: coleções e streams

Praticando Java: coleções e streams

Coleções e stream - Apresentação

Olá! Meu nome é Jacqueline Oliveira, sou engenheira de software e instrutora aqui na Alura.

Audiodescrição: Jaqueline se identifica como uma mulher de pele branca. Possui cabelos longos e alguns reflexos louros. Veste uma blusa verde. Ao fundo, há uma parede lisa e um armário à direita, iluminados em tons de azul.

O que aprenderemos?

Aprofundaremos nossos conhecimentos em programação em Java, praticando coleções e streams (fluxos). Nosso objetivo neste curso é trabalhar e entender:

Esses tópicos são extremamente relevantes, pois são recursos amplamente utilizados quando precisamos lidar com coleções de dados. Na maioria dos casos, precisaremos trabalhar com um grande volume de dados e, portanto, com coleções de dados.

Esperamos por você no próximo vídeo!

Coleções e stream - Coleções

Vamos aprofundar nosso conhecimento e prática sobre as coleções do Java. Antes disso, é importante entender por que utilizamos coleções ao invés de arrays comuns, que são estruturas conhecidas na programação.

Os arrays são basicamente uma construção de baixo nível, apresentando inflexibilidade em alguns aspectos, como no ajuste de tamanho. Eles não possuem operações para buscar, classificar ou adicionar elementos, além de terem tamanhos fixos, o que gera uma complexidade indesejada.

Por isso, damos prioridade ao uso das **coleções **do Java.

Conhecendo tipos de coleções

Vamos explorar um pouco sobre essas coleções, os tipos mais conhecidos, como são utilizadas e como programamos com elas.

Assim como os arrays, as coleções são estruturas de dados que armazenam múltiplos elementos. No Java, as interfaces principais que declaram esses métodos são:

Entenderemos a diferença entre as três e como decidimos por uma ou outra.

Trabalhando com Listas

Basicamente, a List permite ordenação e é útil quando queremos ordenar os elementos de uma coleção desse tipo. Ela também permite duplicidade.

As implementações principais da interface List são as classes:

Usamos ArrayList quando queremos realizar buscas rápidas. Já o LinkedList é preferido quando fazemos muitas inserções e remoções.

Dependendo do caso, optamos por um ou outro. O uso mais comum é o ArrayList.

As operações mais comuns ao utilizar esse tipo de coleção são:

Vamos praticar alguns exemplos. Acessando a IDE IntelliJ, já temos uma classe Principal e um método main. Dentro dele, simularemos a criação de uma lista de pessoas funcionárias.

Primeiro, vamos importar a classe List de java.util e declarar o tipo List, utilizando generics com os símbolos de maior e menor (<>) para indicar que é uma lista de String, pois guardaremos os nomes das pessoas funcionárias.

Nomearemos essa variável como funcionarios. A criação é feita pela classe ArrayList, com new ArrayList<>(). Também devemos importá-la.

import java.util.ArrayList;
import java.util.List;

public class Principal {
    public static void main(String[] args) {
        List<String> funcionarios = new ArrayList<>();
    }
}

Para adicionar itens à lista, utilizamos funcionarios.add() e inserimos o nome de uma pessoa funcionária, como "João". Podemos adicionar mais, como "Maria", e incluir repetidos, como outro "João".

public static void main(String[] args) {
        List<String> funcionarios = new ArrayList<>();
        public static void main(String[] args) {
        List<String> funcionarios = new ArrayList<>();
    }

Podemos imprimir a lista com System.out.println(funcionarios)

System.out.println(funcionarios);

Com isso, veremos "João", "Maria" e "João" novamente no terminal, separados por vírgulas. A lista permite duplicados e mantém a ordem de inserção.

[João, Maria, João]

Podemos alterar esse println() e realizar outras operações, como funcionarios.getFirst() para obter o primeiro elemento. Isso retornará "João".

System.out.println(funcionarios.getFirst());

Também podemos retornar um elemento em uma posição específica. O comando funcionarios.get(1) retornará "Maria", pois a primeira posição é sempre 0.

System.out.println(funcionarios.get(1));

Trabalhando com o Set

Agora, vamos analisar a diferença do List para o próximo tipo, o Set.

O Set é usado quando a ordenação não é o foco. Ele não permite duplicidade, então não poderíamos ter dois "João". As implementações do Set incluem:

O HashSet é o mais rápido, pois não se preocupa com ordenação e não permite duplicidade. O LinkedHashSet mantém a ordem de inserção, enquanto o TreeSet permite ordenação, mas é mais lento.

O HashSet é o mais utilizado por ser rápido e não se preocupar com a ordem.

Vamos criar um exemplo com Set, desta vez com produtos. A criação é semelhante à da lista.

Abaixo do último println(), declararemos um Set de String usando generics para produtos e utilizaremos new HashSet<>() para a implementação.

Set<String> produtos = new HashSet<>();

Para adicionar produtos, usaremos produtos.add(), como "Água", por exemplo.

produtos.add("Água");
produtos.add("Coca-cola");

Se tentarmos adicionar "Água" novamente e imprimir, não será exibido, pois não é permitida duplicidade. Vamos copiar e colar a linha referente à "Água" para verificar isso, adicionando em seguida um println() de produtos.

produtos.add("Água");
produtos.add("Coca-cola");

System.out.println(produtos);

Ao executar a aplicação, o terminal exibirá "Coca-Cola" e "Água".

[Coca-Cola, Água]

Ele não considera a ordem de inserção, pois primeiro inserimos "Água" e depois "Coca-Cola", mas não permitiu a repetição de "Água", mostrando apenas dois elementos.

Antes de inserir, há uma verificação para ver se o elemento já existe na lista, impedindo a inclusão novamente. É importante entender para que serve cada estrutura e qual utilizar em cada caso de aplicação.

Trabalhando com o Map

Por fim, vamos abordar a última estrutura, o Map. O Map utiliza a ideia de chave-valor, não permitindo valores duplicados. Temos um índice e um valor que compõem esse índice.

As principais implementações são:

O objetivo é semelhante ao do Set. O HashMap é o mais rápido e não considera a ordem. O LinkedHashMap mantém a ordem de inserção, e o TreeMap é ordenado pela chave. No Map, o primeiro item é a chave e o segundo é o valor.

Ele possui operações diferentes dos anteriores, como:

A sintaxe é um pouco diferente, mas similar.

Vamos abordar um exemplo no qual criaremos um novo mapa para verificar. Já fizemos isso com pessoas funcionárias e produtos — agora, faremos com clientes.

Abaixo do último prinln(), criaremos um Map de Integer e String, onde Integer será o código do cliente (a chave) e String o nome (o valor). Para inicializar, usaremos um HashMap.

Também importaremos Map de java.util.

Quando trabalhamos com coleções, não usamos os tipos primitivos. Em vez disso, usamos os Wrappers, classes que envolvem os tipos primitivos e adicionam funcionalidades extras. Essas classes encapsulam os tipos básicos e permitem que eles sejam usados em contextos onde objetos são necessários, como em listas e outras estruturas de coleção.

import java.util.Map;
public static void main(String[] args) {

    // Código omitido

    Map<Integer, String> clientes = new HashMap<>();
    
}

Para inserir, utilizaremos clientes.put() em vez de add(). Adicionaremos três clientes: clientes.put(1, "Maria"), clientes.put(2, "Marcos") e clientes.put(3, "Ana").

clientes.put(1, "Maria");
clientes.put(2, "Marcos");
clientes.put(3, "Ana");

Para visualizar o mapa, usaremos System.out.println(clientes).

System.out.println(clientes);

Ao executar, ele exibirá "Maria", "Marcos" e "Ana", sem considerar a ordenação, mostrando a chave e o valor rapidamente.

{1=Maria, 2=Marcos, 3=Ana}

Se inserirmos uma chave repetida, como clientes.put(1, "Ana"), ele sobrescreverá o valor anterior.

clientes.put(1, "Maria");
clientes.put(2, "Marcos");
clientes.put(1, "Ana");

Ao visualizar, teremos 1=Ana e 2=Marcos. Mesmo com valores diferentes, a mesma chave considera o último valor inserido.

{1=Ana, 2=Marcos}

Para buscar uma posição específica, usamos get(). Se quisermos apenas o cliente com chave 1, usaremos clientes.get(1) no println(), que retornará "Ana", pois sobrescrevemos o valor.

System.out.println(clientes.get(1));

Ana

Vamos voltar a Ana para a chave 3, evitando confusões.

clientes.put(1, "Maria");
clientes.put(2, "Marcos");
clientes.put(3, "Ana");

Se quisermos a chave 2, retornará "Marcos".

System.out.println(clientes.get(2));

Marcos

Essa abordagem garante que buscamos pela chave, não pela ordem de inserção.

Um cenário para trabalhar com essa busca seria usar o CPF como chave e o nome como valor. Ao digitar o CPF, que é um identificador único, localizaríamos a pessoa correspondente, independentemente da ordem de armazenamento.

Conclusão

Trabalhar com coleções envolve chamar a coleção e, ao adicionar um ponto, visualizar todos os métodos disponíveis. É essencial consultar a documentação e exemplos, pois nenhum curso abrange todos os métodos. A curiosidade e a experimentação são fundamentais para explorar o universo de possibilidades com coleções em Java.

Esperamos que tenham gostado deste vídeo. No próximo, falaremos mais sobre como manipular essas informações utilizando Streams.

Coleções e stream - Streams

Um recurso fundamental ao trabalhar com coleções são as Streams, pois nos permitem realizar operações funcionais e processar essas coleções de forma funcional.

Isso significa que, em vez de utilizar um laço e realizar ações em cada elemento da coleção, a API nos oferece o recurso de fazer isso por meio de operações intermediárias e terminais. Isso ficará mais claro ao longo da explicação.

Explorando métodos da API de Streams

Ao trabalhar com a API de Streams, geralmente desejamos utilizar métodos para realizar filtros, transformações e agregações sem modificar a coleção original. Sempre faremos isso em outra coleção, mantendo a coleção original inalterada.

Teremos operações intermediárias que processam dados, gerando novos Streams, e operações terminais que encerram o fluxo, como uma impressão ou uma soma, finalizando a operação.

As operações intermediárias mais comuns são:

O filter é utilizado para filtrar, enquanto o map permite realizar transformações.

As operações terminais mais comuns são:

O reduce() é usado para reduzir e finalizar algum cálculo, e o collect() pode transformar os dados em outra lista.

Abordaremos alguns exemplos para esclarecer, e perceberemos como a API de Streams do Java é incrível.

Filtrando iniciais de nomes com filter()

No IntelliJ, acessaremos a classe Principal só com a estrutura implementada:

Principal.java:

package br.com.alura;

public class Principal {
    public static void main(String[] args) {

    }
}

Entre as chaves do método main(), criaremos uma lista de String de pessoas funcionárias, como fizemos anteriormente. Em vez de adicionar item a item, utilizaremos List.of() para incluir alguns nomes padrão: "Ana", "Bruno", "Carlos" e "Amanda".

Também devemos importar a classe List do pacote java.util:

import java.util.List;
List<String> funcionarios = List.of("Ana", "Bruno", "Carlos", "Amanda");

Suponhamos que desejássemos filtrar todos os funcionários que começam com a letra A, pois vamos presenteá-los. A cada dia da semana, será a vez de quem começa com uma letra.

Para isso, criaremos uma nova lista de String chamada funcionariosLetraA. Para pegar os elementos da lista de funcionários e os transformar em uma nova lista por meio de programação funcional, chamaremos a coleção funcionários e utilizaremos .stream() para indicar o início das operações.

List<String> funcionariosLetraA = funcionarios.stream()

Vamos pular uma linha para maior clareza e utilizaremos .filter(), pois queremos filtrar. No momento do filtro, precisaremos realizar uma operação com lambda (λ): para cada pessoa funcionária f (sendo f o predicado), verificaremos se o nome dela começa com A. Diremos: para cada funcionário f, verifique se F.startsWith("A").

List<String> funcionariosLetraA = funcionarios.stream()
            .filter(f -> f.startsWith("A"))

Nessa linha, filtramos cada pessoa funcionária da lista, verificando se seu nome começa com a letra A. Isso gerará outro Stream apenas com pessoas funcionárias cujos nomes começam com a letra A.

Em seguida, utilizaremos .collect() para gerar essa nova lista.

List<String> funcionariosLetraA = funcionarios.stream()
            .filter(f -> f.startsWith("A"))
            .collect(Collectors.toList());

Portanto, estamos dizendo: pegue a lista de pessoas funcionárias, filtre todas com a letra A e crie uma nova lista chamada funcionariosLetraA.

Agora, imprimiremos todos os nomes. Primeiro, imprimiremos a lista de pessoas funcionárias e, logo abaixo, a lista destas que possuem a inicial A, para verificar se o filtro foi realizado corretamente.

System.out.println(funcionarios);
System.out.println(funcionariosLetraA);

Ao executar a aplicação, veremos Ana, Bruno, Carlos e Amanda, e, na sequência, apenas Ana e Amanda, as pessoas funcionárias com a letra A.

[Ana, Bruno, Carlos, Amanda]

[Ana, Amanda]

Trabalhar dessa maneira torna nosso código muito mais simples e limpo, em comparação a percorrer a coleção, verificar se o item começa com "A", adicionar em outra coleção e depois imprimir. Assim, o processo fica mais objetivo, claro e rápido de processar.

Transformando dados com map()

Fizemos um exemplo com filtro, agora vamos transformar um dado usando o map() ao invés do filtro.

Vamos criar uma nova lista, desta vez de comissão das pessoas funcionárias. Será uma lista de Double, chamada valorVendas, onde colocaremos todos os valores de vendas para calcular as comissões.

Utilizaremos List.of() para inserir os elementos: 500, 1.800 e 6.200, por exemplo. Adicionaremos .0 no final de cada valor para evitar erros, já que é uma lista de double.

List<Double> valorVendas = List.of(500.0, 1800.0, 6200.0);

Agora, queremos calcular 5% de comissão sobre cada valor. Criaremos uma nova lista de Double, chamada comissao. Para essa lista, pegaremos valorVendas, aplicaremos .stream() e, em seguida, .map().

No map(), definimos o predicado: para cada valor de venda v, multiplicaremos ele mesmo por 0.05 para calcular a comissão. Em seguida, usaremos collect(), inserindo Collectors.toList() para armazenar na lista comissao.

List<Double> comissao = valorVendas.stream()
            .map(v -> v * 0.05)
            .collect(Collectors.toList());

Ao imprimir a lista de valorVendas e logo abaixo a lista de comissao, veremos que cada valor teve 5% aplicado.

System.out.println(valorVendas);
System.out.println(comissao);

No exemplo, R$ 500,00, R$ 1.800,00 e R$ 6.200,00 resultaram em comissões de R$ 25,00, R$ 90,00 e R$ 310,00, respectivamente.

[500.0, 1800.0, 6200.0]

[25.0, 90.0, 310.0]

Somando valores com reduce()

Para finalizar, vamos somar todas as vendas. Criaremos um double chamado totalVendas, recebendo a lista valorVendas. Nesta, aplicaremos .stream() e .reduce(), especificando que queremos somar os valores.

Utilizamos 0.0 para representar o valor inicial da soma e Double::sum para realizar a soma.

double totalVendas = valorVendas.stream()
            .reduce(0.0, Double::sum);

Ao imprimir totalVendas, ele apresentará o resultado: o total de vendas será R$ 8.500,00.

System.out.println("Total Vendas: " + totalVendas);

Total Vendas: 8500.00

Podemos perceber como é fácil trabalhar com Streams: com praticamente uma linha, conseguimos somar todos os itens de uma coleção de forma rápida e simples, utilizando recursos que a API já oferece.

Conclusão

Vale a pena estudar e se aprofundar em coleções e Streams para trabalhar melhor com dados, de forma eficiente e organizada, aplicando filtros, mapas, transformações e outros recursos que tornam a aplicação mais performática.

Esperamos que tenham gostado deste conteúdo. Não deixe de avaliar o curso e enviar seu feedback! Mostre os códigos que você está desenvolvendo e nos marque nas redes sociais!

Se tiver dúvidas, não hesite em participar do fórum ou enviar mensagens no Discord. Queremos ouvir você para trazer conteúdos cada vez melhores.

Agradecemos por nos acompanhar até aqui. Até o próximo curso!

Sobre o curso Praticando Java: coleções e streams

O curso Praticando Java: coleções e streams possui 29 minutos de vídeos, em um total de 17 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Java acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas