1. Lista de métodos da Streamclasse

A Streamclasse foi criada para facilitar a construção de cadeias de fluxos de dados. Para conseguir isso, a Stream<T>classe possui métodos que retornam novos Streamobjetos.

Cada um desses fluxos de dados realiza uma ação simples, mas se você os combinar em cadeias e adicionar funções lambda interessantes , terá um mecanismo poderoso para gerar a saída desejada. Em breve você verá por si mesmo.

Aqui estão os métodos da Streamclasse (apenas os mais básicos):

Métodos Descrição
Stream<T> of()
Cria um fluxo a partir de um conjunto de objetos
Stream<T> generate()
Gera um fluxo de acordo com a regra especificada
Stream<T> concat()
Concatena dois streams
Stream<T> filter()
Filtra os dados, passando apenas os dados que correspondem à regra especificada
Stream<T> distinct()
Remove duplicatas. Não repassa dados que já foram encontrados
Stream<T> sorted()
Classifica os dados
Stream<T> peek()
Executa uma ação em cada elemento no fluxo
Stream<T> limit(n)
Retorna um fluxo que é truncado para que não ultrapasse o limite especificado
Stream<T> skip(n)
Ignora os primeiros n elementos
Stream<R> map()
Converte dados de um tipo para outro
Stream<R> flatMap()
Converte dados de um tipo para outro
boolean anyMatch()
Verifica se há pelo menos um elemento no fluxo que corresponda à regra especificada
boolean allMatch()
Verifica se todos os elementos no fluxo correspondem à regra especificada
boolean noneMatch()
Verifica se nenhum dos elementos no fluxo corresponde à regra especificada
Optional<T> findFirst()
Retorna o primeiro elemento encontrado que corresponda à regra
Optional<T> findAny()
Retorna qualquer elemento no fluxo que corresponda à regra
Optional<T> min()
Procura o elemento mínimo no fluxo de dados
Optional<T> max()
Retorna o elemento máximo no fluxo de dados
long count()
Retorna o número de elementos no fluxo de dados
R collect()
Lê todos os dados do stream e os retorna como uma coleção

2. Operações intermediárias e terminais pela Streamclasse

Como você pode ver, nem todos os métodos na tabela acima retornam um Stream. Isso está relacionado ao fato de que os métodos da Streamclasse podem ser divididos em métodos intermediários (também conhecidos como não terminais ) e métodos terminais .

Métodos intermediários

Os métodos intermediários retornam um objeto que implementa a Streaminterface e podem ser encadeados.

Métodos terminais

Os métodos de terminal retornam um valor diferente de a Stream.

Pipeline de chamada de método

Assim, você pode criar um pipeline de fluxo que consiste em qualquer número de métodos intermediários e uma única chamada de método de terminal no final. Essa abordagem permite implementar uma lógica bastante complexa, enquanto aumenta a legibilidade do código.

Pipeline de chamada de método

Os dados dentro de um fluxo de dados não mudam. Uma cadeia de métodos intermediários é uma maneira inteligente (declarativa) de especificar um pipeline de processamento de dados que será executado após a chamada do método de terminal.

Em outras palavras, se o método do terminal não for chamado, os dados no fluxo de dados não serão processados ​​de forma alguma. Somente depois que o método terminal é chamado, os dados começam a ser processados ​​de acordo com as regras especificadas no pipeline de fluxo.

stream()
  .intemediateOperation1()
  .intemediateOperation2()
  ...
  .intemediateOperationN()
  .terminalOperation();
Aparência geral de um pipeline

Comparação dos métodos intermediário e terminal:

intermediário terminal
Tipo de retorno Stream não umStream
Pode ser combinado com vários métodos do mesmo tipo para formar um pipeline sim não
Número de métodos em um único pipeline qualquer não mais do que um
Produz o resultado final não sim
Inicia o processamento dos dados no stream não sim

Vejamos um exemplo.

Suponha que tenhamos um clube para amantes de animais. Amanhã o clube comemora o Ginger Cat Day. O clube tem donos de animais de estimação, cada um com uma lista de animais de estimação. Eles não estão limitados aos gatos.

Tarefa: você precisa identificar todos os nomes de todos os gatos ruivos para criar cartões personalizados para eles nas "férias profissionais" de amanhã. Os cartões comemorativos devem ser classificados pela idade do gato, do mais velho para o mais novo.

Primeiro, fornecemos algumas classes para ajudar a resolver essa tarefa:

public enum Color {
   WHITE,
   BLACK,
   DARK_GREY,
   LIGHT_GREY,
   FOXY,
   GREEN,
   YELLOW,
   BLUE,
   MAGENTA
}
public abstract class Animal {
   private String name;
   private Color color;
   private int age;

   public Animal(String name, Color color, int age) {
      this.name = name;
      this.color = color;
      this.age = age;
   }

   public String getName() {
      return name;
   }

   public Color getColor() {
      return color;
   }

   public int getAge() {
      return age;
   }
}
public class Cat extends Animal {
   public Cat(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Dog extends Animal {
   public Dog(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Parrot extends Animal {
   public Parrot(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Pig extends Animal {
   public Pig(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Snake extends Animal {
   public Snake(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Owner {
   private String name;
   private List<Animal> pets = new ArrayList<>();

   public Owner(String name) {
      this.name = name;
   }

   public List<Animal> getPets() {
      return pets;
   }
}

Agora vamos ver a Selectorclasse, onde a seleção será feita de acordo com os critérios especificados:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Selector {
   private static List<Owner> owners;

   private static void initData() {
      final Owner owner1 = new Owner("Ronan Turner");
      owner1.getPets().addAll(List.of(
            new Cat("Baron", Color.BLACK, 3),
            new Cat("Sultan", Color.DARK_GREY, 4),
            new Dog("Elsa", Color.WHITE, 0)
      ));

      final Owner owner2 = new Owner("Scarlet Murray");
      owner2.getPets().addAll(List.of(
            new Cat("Ginger", Color.FOXY, 7),
            new Cat("Oscar", Color.FOXY, 5),
            new Parrot("Admiral", Color.BLUE, 3)
      ));

      final Owner owner3 = new Owner("Felicity Mason");
      owner3.getPets().addAll(List.of(
            new Dog("Arnold", Color.FOXY, 3),
            new Pig("Vacuum Cleaner", Color.LIGHT_GREY, 8)
      ));

      final Owner owner4 = new Owner("Mitchell Stone");
      owner4.getPets().addAll(List.of(
            new Snake("Mr. Boa", Color.DARK_GREY, 2)
      ));

      final Owner owner5 = new Owner("Jonathan Snyder");
      owner5.getPets().addAll(List.of(
            new Cat("Fisher", Color.BLACK, 16),
            new Cat("Zorro", Color.FOXY, 14),
            new Cat("Margo", Color.WHITE, 3),
            new Cat("Brawler", Color.DARK_GREY, 1)
      ));

      owners = List.of(owner1, owner2, owner3, owner4, owner5);
   }
}

O que resta é adicionar código ao mainmétodo. Atualmente, primeiro chamamos o initData()método, que preenche a lista de donos de animais de estimação no clube. Em seguida, selecionamos os nomes dos gatos ruivos classificados por idade em ordem decrescente.

Primeiro, vejamos o código que não usa streams para resolver esta tarefa:

public static void main(String[] args) {
   initData();

   List<String> findNames = new ArrayList<>();
   List<Cat> findCats = new ArrayList<>();
   for (Owner owner : owners) {
      for (Animal pet : owner.getPets()) {
         if (Cat.class.equals(pet.getClass()) && Color.FOXY == pet.getColor()) {
            findCats.add((Cat) pet);
         }
      }
   }

   Collections.sort(findCats, new Comparator<Cat>() {
      public int compare(Cat o1, Cat o2) {
         return o2.getAge() - o1.getAge();
      }
   });

   for (Cat cat : findCats) {
      findNames.add(cat.getName());
   }

   findNames.forEach(System.out::println);
}

Agora vamos ver uma alternativa:

public static void main(String[] args) {
   initData();

   final List<String> findNames = owners.stream()
           .flatMap(owner -> owner.getPets().stream())
           .filter(pet -> Cat.class.equals(pet.getClass()))
           .filter(cat -> Color.FOXY == cat.getColor())
           .sorted((o1, o2) -> o2.getAge() - o1.getAge())
           .map(Animal::getName)
           .collect(Collectors.toList());

   findNames.forEach(System.out::println);
}

Como você pode ver, o código é muito mais compacto. Além disso, cada linha do stream pipeline é uma única ação, então elas podem ser lidas como sentenças em inglês:

.flatMap(owner -> owner.getPets().stream())
Mover de um Stream<Owner>para umStream<Pet>
.filter(pet -> Cat.class.equals(pet.getClass()))
Reter apenas gatos no fluxo de dados
.filter(cat -> Color.FOXY == cat.getColor())
Reter apenas gatos ruivos no fluxo de dados
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
Classificar por idade em ordem decrescente
.map(Animal::getName)
Obtenha os nomes
.collect(Collectors.toList())
Coloque o resultado em uma lista

3. Criando fluxos

A Streamclasse tem três métodos que ainda não abordamos. O objetivo desses três métodos é criar novos threads.

Stream<T>.of(T obj)método

O of()método cria um fluxo que consiste em um único elemento. Isso geralmente é necessário quando, digamos, uma função recebe um Stream<T>objeto como argumento, mas você só tem um Tobjeto. Então você pode facilmente e simplesmente usar o of()método para obter um fluxo que consiste em um único elemento .

Exemplo:

Stream<Integer> stream = Stream.of(1);

Stream<T> Stream.of(T obj1, T obj2, T obj3, ...)método

O of()método cria um fluxo que consiste em elementos passados . Qualquer número de elementos é permitido. Exemplo:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

Stream<T> Stream.generate(Supplier<T> obj)método

O generate()método permite definir uma regra que será utilizada para gerar o próximo elemento do stream quando solicitado. Por exemplo, você pode fornecer um número aleatório a cada vez.

Exemplo:

Stream<Double> s = Stream.generate(Math::random);

Stream<T> Stream.concat(Stream<T> a, Stream<T> b)método

O concat()método concatena os dois fluxos passados ​​em um . Quando os dados são lidos, eles são lidos primeiro no primeiro fluxo e depois no segundo. Exemplo:

Stream<Integer> stream1 = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream2 = Stream.of(10, 11, 12, 13, 14);
Stream<Integer> result = Stream.concat(stream1, stream2);

4. Filtrando dados

Outros 6 métodos criam novos fluxos de dados, permitindo combinar fluxos em cadeias (ou pipelines) de complexidade variável.

Stream<T> filter(Predicate<T>)método

Este método retorna um novo fluxo de dados que filtra o fluxo de dados de origem de acordo com a regra passada . O método deve ser chamado em um objeto cujo tipo é Stream<T>.

Você pode especificar a regra de filtragem usando uma função lambda , que o compilador converterá em um Predicate<T>objeto.

Exemplos:

Fluxos encadeados Explicação
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream2 = stream.filter(x -> (x < 3));

Reter apenas números menores que três
Stream<Integer> stream = Stream.of(1, -2, 3, -4, 5);
Stream<Integer> stream2 = stream.filter(x -> (x > 0));

Reter apenas números maiores que zero

Stream<T> sorted(Comparator<T>)método

Este método retorna um novo fluxo de dados que classifica os dados no fluxo de origem . Você passa um comparador , que define as regras para comparar dois elementos do fluxo de dados.

Stream<T> distinct()método

Esse método retorna um novo fluxo de dados que contém apenas os elementos exclusivos no fluxo de dados de origem . Todos os dados duplicados são descartados. Exemplo:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.distinct(); // 1, 2, 3, 4, 5

Stream<T> peek(Consumer<T>)método

Este método retorna um novo fluxo de dados , embora os dados nele sejam os mesmos do fluxo de origem. Mas quando o próximo elemento é solicitado do fluxo, a função que você passou para o peek()método é chamada com ele.

Se você passar a função System.out::printlnpara o peek()método, todos os objetos serão exibidos quando passarem pelo fluxo.

Stream<T> limit(int n)método

Este método retorna um novo fluxo de dados que contém apenas os primeiros nelementos no fluxo de dados de origem . Todos os outros dados são descartados. Exemplo:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.limit(3); // 1, 2, 3

Stream<T> skip(int n)método

Esse método retorna um novo fluxo de dados que contém todos os mesmos elementos do fluxo de origem , mas ignora (ignora) os primeiros nelementos. Exemplo:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.skip(3); // 4, 5, 2, 2, 2, 3, 4