1. Lista metod typówStream

Klasa Streamzostała stworzona, aby móc w prosty sposób konstruować łańcuchy przepływów danych. Aby to zrobić, obiekt typu Stream<T>ma metody, które zwracają nowe obiekty typu Stream.

Każdy z tych strumieni danych może wykonać jedną prostą akcję, ale jeśli połączysz je w łańcuchy, a nawet dodasz do tego tak interesującą rzecz, jak funkcje lambda , możesz uzyskać bardzo potężną rzecz na wyjściu. Wkrótce sam się przekonasz.

Oto metody, które ma klasa Stream(tylko te najbardziej podstawowe):

Metody Opis
Stream<T> of()
Tworzy strumień z zestawu obiektów
Stream<T> generate()
Generuje strumień zgodnie z podaną regułą
Stream<T> concat()
Łączy ze sobą wiele strumieni
Stream<T> filter()
Filtruje dane: przekazuje tylko dane pasujące do podanej reguły
Stream<T> distinct()
Usuwa duplikaty: nie pomija danych, które już były
Stream<T> sorted()
Sortuje dane
Stream<T> peek()
Wykonuje akcję na każdych danych
Stream<T> limit(n)
Obcina dane po osiągnięciu limitu
Stream<T> skip(n)
Pomija pierwsze n danych
Stream<R> map()
Konwertuje dane z jednego typu na inny
Stream<R> flatMap()
Konwertuje dane z jednego typu na inny
boolean anyMatch()
Sprawdza, czy co najmniej jedno ze strumieni danych pasuje do podanej reguły
boolean allMatch()
Sprawdza, czy wszystkie dane w strumieniu są zgodne z podaną regułą
boolean noneMatch()
Sprawdza, czy żadne dane w strumieniu nie pasują do podanej reguły
Optional<T> findFirst()
Zwraca pierwszy znaleziony element pasujący do reguły
Optional<T> findAny()
Zwraca dowolny element ze strumienia, który pasuje do reguły
Optional<T> min()
Wyszukuje minimalny element w strumieniu danych
Optional<T> max()
Zwraca maksymalny element w strumieniu danych
long count()
Zwraca liczbę elementów w strumieniu danych
R collect()
Odczytuje wszystkie dane ze strumienia i zwraca je jako kolekcję

2. Metody pośrednie i końcoweStream

Jak widać, nie wszystkie metody z powyższej tabeli zwracają Stream. Wynika to z faktu, że metody klas Streammożna podzielić na pośrednie ( pośrednie , nieterminalne ) i końcowe ( terminalne ).

Metody pośrednie

Metody pośrednie zwracają obiekt, który implementuje interfejs Streami mogą być łączone w wywołania.

Ostateczne metody

Metody końcowe zwracają wartość, której typem nie jest Stream.

Łańcuch wywołań metody

W ten sposób możesz budować łańcuchy wywołań z dowolnej liczby metod pośrednich i na końcu wywołać jedną metodę końcową. Takie podejście pozwala zaimplementować dość złożoną logikę, jednocześnie zwiększając czytelność kodu.

Wewnątrz strumienia danych dane w ogóle się nie zmieniają. Łańcuch metod pośrednich to skomplikowany (deklaratywny) sposób określenia określonej sekwencji przetwarzania danych, która rozpocznie się po wywołaniu metody końcowej (końcowej).

Oznacza to, że bez wywołania metody final dane w strumieniu danych nie są w żaden sposób przetwarzane. I dopiero po wywołaniu metody terminalowej dane zaczynają być przetwarzane zgodnie z regułami określonymi przez łańcuch wywołań metod.

stream()
  .intemediateOperation1()
  .intemediateOperation2()
  ...
  .intemediateOperationN()
  .terminalOperation();
Ogólny widok łańcucha połączeń

Porównanie metod pośrednich i końcowych:

mediator finał
zwracany typ Stream NieStream
Możliwość łączenia wielu metod danego typu w łańcuch wywołań Tak NIE
Liczba metod w jednym łańcuchu wywołań każdy nie więcej niż jeden
Tworzy efekt końcowy NIE Tak
Rozpoczyna przetwarzanie danych w strumieniu NIE Tak

Spójrzmy na przykład.

Jest klub dla miłośników zwierząt. Jutro Dzień Czerwonego Kota. Klub ma właścicieli zwierząt domowych, z których każdy ma listę zwierząt domowych. Może to nie tylko koty.

Zadanie: musisz wybrać imiona wszystkich czerwonych kotów, aby wydrukować dla nich spersonalizowane kartki z życzeniami na „Święto Zawodowe” na jutro. Pocztówki należy posortować według wieku kota, od najstarszego do najmłodszego.

Najpierw przedstawimy klasy pomocnicze do rozwiązania tego problemu:

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;
   }
}

Rozważmy teraz klasę Selector, w której zostanie dokonany wybór według podanych kryteriów:

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("Олег Малашков");
      owner1.getPets().addAll(List.of(
            new Cat("Барон", Color.BLACK, 3),
            new Cat("Султан", Color.DARK_GREY, 4),
            new Dog("Эльза", Color.WHITE, 0)
      ));

      final Owner owner2 = new Owner("Дмитрий Васильков");
      owner2.getPets().addAll(List.of(
            new Cat("Рыжик", Color.FOXY, 7),
            new Cat("Barsik", Color.FOXY, 5),
            new Parrot("Адмирал", Color.BLUE, 3)
      ));

      final Owner owner3 = new Owner("Наталия Криж");
      owner3.getPets().addAll(List.of(
            new Dog("Арнольд", Color.FOXY, 3),
            new Pig("Пылесос", Color.LIGHT_GREY, 8)
      ));

      final Owner owner4 = new Owner("Павел Мурахов");
      owner4.getPets().addAll(List.of(
            new Snake("Удав", Color.DARK_GREY, 2)
      ));

      final Owner owner5 = new Owner("Антон Федоренко");
      owner5.getPets().addAll(List.of(
            new Cat("Фишер", Color.BLACK, 16),
            new Cat("Зорро", Color.FOXY, 14),
            new Cat("Марго", Color.WHITE, 3),
            new Cat("Забияка", Color.DARK_GREY, 1)
      ));

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

Pozostaje dodać kod metody main, w której najpierw wywołamy metodę initData(), która wypełni listę właścicieli pupili w klubie danymi, a następnie wybierzemy imiona rudych kotów posortowane malejąco według wieku.

Najpierw spójrzmy na kod, który nie używa strumieni do rozwiązania tego problemu:

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);
}

Teraz spójrzmy na alternatywę:

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);
}

Jak widać, kod jest znacznie bardziej zwarty. Dodatkowo każda linia strumienia to pojedyncza akcja, więc można je czytać jak zdania w języku angielskim:

.flatMap(owner -> owner.getPets().stream())
przejście z Stream<Owner>doStream<Pet>
.filter(pet -> Cat.class.equals(pet.getClass()))
pozostawiając tylko koty w strumieniu danych
.filter(cat -> Color.FOXY == cat.getColor())
zostawiamy tylko rude w strumieniu danych
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
sortuj według wieku w kolejności malejącej
.map(Animal::getName)
przyjmujemy nazwiska
.collect(Collectors.toList())
umieść wynik na liście

3. Utwórz wątki

Wśród metod klasowych Streamsą trzy metody, których jeszcze nie omówiliśmy. Celem tych trzech metod jest tworzenie nowych wątków.

metodaStream<T>.of(T obj)

Metoda of()tworzy strumień , który składa się z pojedynczego elementu. Jest to zwykle potrzebne, jeśli, powiedzmy, funkcja przyjmuje jako parametr obiekt typu Stream<T>, a ty masz tylko obiekt typu T. Następnie możesz łatwo i prosto użyć metody, of()aby uzyskać strumień składający się z jednego elementu .

Przykład:

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

metodaStream<T> Stream.of(T obj1, T obj2, T obj3, ...)

Metoda of()tworzy strumień składający się z przekazanych elementów . Liczba elementów może być dowolna. Przykład:

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

metodaStream<T> Stream.generate(Supplier<T> obj)

Metoda generate()pozwala ustawić regułę, według której następny element strumienia będzie generowany, gdy zostanie o to poproszony. Na przykład możesz za każdym razem podać losową liczbę .

Przykład:

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

metodaStream<T> Stream.concat(Stream<T> a, Stream<T> b)

Metoda concat()scala dwa przekazane strumienie w jeden . Podczas odczytu danych najpierw zostaną odczytane dane z pierwszego strumienia, a następnie z drugiego strumienia. Przykład:

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. Filtrowanie danych

Kolejne 6 metod tworzy nowe strumienie danych, które pozwalają łączyć strumienie w łańcuchy o różnej złożoności.

metodaStream<T> filter(Predicate<T>)

Ta metoda zwraca nowy strumień danych , który filtruje dane ze strumienia źródłowego zgodnie z przekazaną regułą . Metoda musi być wywołana na obiekcie typu Stream<T>.

Możesz użyć funkcji lambda do ustawienia reguły filtrowania , która następnie zostanie przekonwertowana przez kompilator na obiekt typu Predicate<T>.

Przykłady:

Łańcuchy nici Wyjaśnienie
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream2 = stream.filter(x -> (x < 3));

Zostaw tylko liczby mniejsze niż trzy
Stream<Integer> stream = Stream.of(1, -2, 3, -4, 5);
Stream<Integer> stream2 = stream.filter(x -> (x > 0));

Pozostaw tylko liczby większe od zera

metodaStream<T> sorted(Comparator<T>)

Ta metoda zwraca nowy strumień danych, który sortuje dane ze strumienia źródłowego . Jako parametr można podać komparator , który ustali zasady porównywania dwóch elementów strumienia danych.

metodaStream<T> distinct()

Ta metoda zwraca nowy strumień danych , który zawiera tylko unikatowe dane ze źródłowego strumienia danych . Wszystkie zduplikowane dane są odrzucane. Przykład:

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

metodaStream<T> peek(Consumer<T>)

Ta metoda zwraca nowy strumień danych , chociaż dane w nim są takie same jak w strumieniu źródłowym. Ale kiedy żądany jest następny element ze strumienia, wywoływana jest dla niego funkcja , którą przekazałeś do metody peek().

Jeśli do metody peek()przekażemy funkcję System.out::println, to wszystkie obiekty będą wyświetlane na ekranie w momencie, gdy przejdą przez strumień.

metodaStream<T> limit(int n)

Ta metoda zwraca nowy strumień danych , który zawiera tylko pierwsze n dane ze źródłowego strumienia danych . Wszystkie inne dane są odrzucane. Przykład:

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

metodaStream<T> skip(int n)

Ta metoda zwraca nowy strumień danych , który zawiera te same dane co strumień źródłowy , ale pomija (ignoruje) pierwsze ndane. Przykład:

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