Strumienie (te związane z danymi a nie do obsługi plików) w Javie 8 to potężne narzędzie do rzucania danymi w lewo i prawo. Do ich przekształcania, filtracji, itd., dlatego warto je wziąć na tapetę i trochę się nimi pobawić.

Najprościej strumień można sobie wyobrazić jako potok danych na których wykonujemy operację w różnych krokach, aż do uzyskania rezultatu.

Dlatego w strumieniach rozróżniamy parę rodzajów metod, które posiadają różne charakterystyki:

  • pośrednie (ang. intermediate) – nie kończą strumienia, pozwalają na dalsze operowanie na danych (np. map)
  • kończące/terminalne (ang. terminal) – kończą strumień i zwracają wynik (np. forEach)
  • bezstanowe – są wykonane niezależnie od innych danych, co ma znaczenie przy użyciu strumieni równoległych (np. filter)
  • stanowe – ich stan zależy od pozostałych danych i dlatego nie są przetwarzane równolegle (np. sort)
  • redukcyjne – dokonują redukcji danych (np. max)

Warto wiedzieć że:

  • strumienie są wywołane w sposób leniwy, tzn. dane są przetwarzane w monecie wywołania metody kończącej
  • transformacje strumienia nie modyfikują wejściowych danych z których strumień został utworzony

Tworzenie strumieni:

  • (Collection). stream() – tworzenie strumieni z kolekcji List, Set, z Mapy trzeba pobrać seta
  • stream(T[] array), Stream.of(T… values)– tworzenie strumieni z tablicy
  • (String). chars() – strumień z ciągu znaków
  • lines(path) – strumień linii tekstu z pliku tekstowego
  • (Pattern). splitAsStream() – strumień danych na podstawie wyrażenia regularnego
  • generate(…), Stream.iterate(…) – generowanie strumienia z pomocą metod
  • (Random). ints(), (Random). longs() – generowanie strumienia z losowymi danymi
  • concat(stream1, stream2) – Łączenie dwóch strumieni

Metody do operacji na strumieniach:

Metoda Typ Przeznaczenie Opis
map(Function…) pośrednia transformacja Zwraca strumień z przekształconymi danymi przy promocji funkcji
flatMap(Function f) – pośrednia transformacja Zwraca strumień przekształconymi elementami strumienia. przekształconymi za pomocą podanej funkcji. Przy czym po przekształceniu będą połączone w jeden zestaw danych.
filter(Predicate p) pośrednia filtrowanie Zwraca strumień danych dla których warunek będzie spełniony
distinct() pośrednia filtrowanie Zwraca strumień danych które będą unikalne
sorted(…) pośrednia, stanowa transformacja Przekształca strumień do postaci posortowanej
unordered() pośrednia transformacja Przekształca strumień do postaci nieuporządkowanej
limit(int n) pośrednia transformacja Ogranicza strumień do podanego rozmiaru
generate(Supplier s) pośrednia generowanie Generuje strumień składający się z wygenerowanych elementów
iterate(T init, UnaryOperator op) pośrednia generowanie Generuje strumień tworzony przez iteracyjne wywołanie operatora
substream(…) pośrednia filtrowanie Zwraca część strumienia, n-elementów od…
parallel () pośrednia generowanie Generuje strumień równoległy
sequential() pośrednia generowanie Generuje strumień sekwencyjny
allMatch(Predicate p) kończąca redukcja Testuje czy w strumieniu jeden wszystkie obiekty spełniają podany warunek
anyMatch(Predicate p) kończąca redukcja Testuje czy w strumieniu jeden chociaż jeden obiekt spełnia podany warunek
noneMatch(Predicate p) kończąca redukcja Testuje czy w strumieniu jeden wszystkie nie ma obiektów spełniających podany warunek
findAny() kończąca filtrowanie Zwraca dowolny element strumienia
findFirst() kończąca filtrowanie Zwraca pierwszy element strumienia
count() kończąca redukcja
forEach(Consumer c) Kończąca, bezstanowa Wykonuje akcję z każdym elementem strumienia, zamykając go jednocześnie
forEachOrdered(Consumer) kończąca, stanowa Wykonuje akcję z każdym elementem strumienia, zamykając go jednocześnie
reduce(…) kończąca redukcja
collect(…) kończąca Grupuje wszystkie elementy pozostające w strumieniu i zwraca je w postaci definiowanej przez podany Collector
max(…) kończąca redukcja Zwraca największą wartość wynikająca z zastosowanego komparatora
min(…) kończąca redukcja Zwraca najmniejszą wartość wynikająca z zastosowanego komparatora
toArray(…) kończąca Przekształca strumień do tablicy danych
peek(Consumer<? super T> action) pośrednia Wykonuje operację na elemencie bez przekształcania go

 

Sposób pracy ze strumieniem, generalnie robimy w ten sposób:

  • Tworzenie strumienia
  • Pracujemy z metodami pośrednimi gdzie dokonujemy filtrowania, przetwarzania danych, itd.
  • Zwracamy rezultat lub dokonujemy innych operacji przy pomocy metod kończących

Na pewno warto zapoznać się z: https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html

Porównanie kodu z Javy 7 do Javy 8, można powiedzieć ze jest czytelniej:

//
// Dodanie mapy z danymi
//
Map&lt;String, Integer&gt; map = new HashMap&lt;&gt;();
map.put("jeden", 1);
map.put("dwa", 2);
map.put("trzy", 3);
map.put("cztery", 4);
map.put("pięć", 5);
map.put("szcześć", 6);
 
//
// Kod w Javie 7
//
List&lt;String&gt; listJava7 = new ArrayList&lt;&gt;();
for (Map.Entry&lt;String, Integer&gt; entry : map.entrySet()){
   if (entry.getValue() &gt; 3){
      listJava7.add(entry.getKey());
   }
}
System.out.println("Java7: " + listJava7);
 
//
// Kod w Javie 8
//
List&lt;String&gt; listJava8 = map.entrySet().stream()
                     .filter(entry -&gt; entry.getValue() &gt; 3)
                     .map(Map.Entry::getKey)
                     .collect(Collectors.toList());
System.out.println("Java8: " + listJava8);

Przykłady strumieni:

Tworzenie strumienii

//
// Z tablicy
//
System.out.println("Dane tablicy:");
 
double[] as = {42.20, 12.12, 10.0, 20.0, 30.0, 40.0, 43.0, 23.23, 10.22};
Arrays.stream(as)
     .forEach(d -&gt; System.out.print(d + " "));
 
//
// Losowe
//
System.out.println("Losowe dane:");
Random rnd = new Random();
rnd.ints(10, 0, 10)
   .forEach(d -&gt; System.out.print(d + " "));
 
//
// Z kolekcji
//
System.out.println("Dane z kolekcji:");
List&lt;String&gt; list = new ArrayList&lt;&gt;();
list.add("Pierwszy");
list.add("Drugi");
list.add("Trzeci");
list.add("Czwarty");
list.add("Piąty");
list.add("Szósty");
 
list.parallelStream()
   .forEach(s -&gt; System.out.print(s + " "));
 
//
// Z kolekcji intów
//
System.out.println("Dane intów z kolekcji:");
final List&lt;Integer&gt; integerList = Arrays.asList(10, 20, 30, 40, 50, 60, 70, 80, 90, 100);
integerList.stream()
         .forEach(i -&gt; System.out.print(i + " "));

Operacje na strumieniach

//
// Fitrowanie przy pomnocy strumieni
//
System.out.println("Filtrowanie danych:");
list.stream()
   .filter(s -&gt; s.startsWith("P") || s.startsWith("C"))
   .forEach(s -&gt; System.out.print(s + " "));
 
//
// Sortowanie
//
System.out.println("Sortowanie danych:");
list.stream()
   .sorted(Comparator.reverseOrder())
   .forEach(s -&gt; System.out.print(s + " "));
 
//
// Mapowanie, za obiekt podstawiany jest wynikający z mapowania
//
System.out.println("Mapowanie danych:");
list.stream()
   .map(s -&gt; s + " czy jest równy 'Trzeci' ? " + (s.equals("Trzeci") ? "TAK" : "NIE"))
   .forEach(s -&gt; System.out.println(s));
 
//
// Sortowanie, filtrowanie, mapowanie
//
System.out.println("Obróbka danych:");
list.stream()
   .map(s -&gt; s.toUpperCase())
   .filter(s -&gt; s.endsWith("Y"))
   .sorted()
   .forEach(System.out::println);

Operacja na własnych klasach

Mamy klasę przechowywująca informację o krajach:

class Country{
   private String name;
   private String system;
   private int area;
   private int population;
 
   public Country(String name, String system, int area, int population){
      this.name = name;
      this.system = system;
      this.area = area;
      this.population = population;
   }
 
   public String getName(){
      return name;
   }
 
   public String getSystem(){
      return system;
   }
 
   public int getArea(){
      return area;
   }
 
   public int getPopulation(){
      return population;
   }
 
   @Override
   public String toString(){
      return "Państwo: " + name + " powierzchnia: " + area + " km2 populacja: " + population;
   }
}

Możemy przy pomocy strumieni gdzie używamy małej ilości kodu trochę popracować nad danymi:

//
// Dodanie przykładowych danych
//
List&lt;Country&gt; countryList = Arrays.asList(
new Country("Poland", "Republika", 312679, 38454576),
new Country("Germany", "system kanclerski", 357375, 81083600),
new Country("Česká republika", "Republika", 78866, 10541466),
new Country("Slovenská republika", "Republika", 49035, 5421349),
new Country("République française", "Republika semiprezydencka", 643801, 66318000)
);
 
//
// Fitrowanie i sortowanie po nazwie
//
final List&lt;Country&gt; countriesMoreThan10Million =
countryList.stream()
         .filter(country -&gt; country.getPopulation() &gt; 10000000)
         .sorted((o1, o2) -&gt; o1.getName().compareToIgnoreCase(o2.getName()))
         .collect(Collectors.toList());
 
System.out.println("Państwa z populacją ponad 10 mil:");
countriesMoreThan10Million.forEach(System.out::println);
 
//
// Grupowanie według systemu politycznego
//
Map&lt;String, List&lt;Country&gt;&gt; countriesGroupBySystem =
countryList.stream()
         .sorted((o1, o2) -&gt; o1.getName().compareToIgnoreCase(o2.getName()))
         .collect(Collectors.groupingBy(b -&gt; b.getSystem()));
 
countriesGroupBySystem.forEach((s, countries) -&gt; System.out.println("Ustrój: " + s + " " + countries));
 
//
// Wyliczenie średniej poulacji dla podanej listy krajów
//
Double avgPopulation = countryList.stream()
                          .collect(Collectors.averagingInt(Country::getPopulation));
System.out.println("Średnia populacji dla podanej listy krajów to " + Math.round(avgPopulation) + " osób");