Strumienie znajdują się one w pakiecie java.util.stream i jest to interfejs generyczny. Przetwarzają one dane w postaci strumieni oraz wykonują operacje na wielu elementach. Strumienie nie przechowują danych – nie są żadną strukturą danych, nie modyfikują i nie zmieniają źródła, na którym operują.
W strumień można opakować dowolny zestaw danych. Dodatkowo strumienie nie muszą zawierać danych, które zwracają. Np. strumień generujący kolejne liczby pseudolosowe nie zawiera tych liczb, jedynie je generuje. Dane na których strumienie pracują mogą być przechowywane w kolekcji, mogą być wynikiem pracy z wyrażeniami regularnymi, mogą pochodzić z innego strumienia . Następnie dane te są przetwarzane przez dowolną liczbę operacji. Każda z tych operacji tworzy nowy strumień danych wywodzący się z poprzedniego. Na samym końcu strumień może mieć dokładnie jedną metodę kończącą pracę ze strumieniem. Każdy strumień ma dokładnie jedną metodę, która go tworzy na podstawie danych źródłowych.
Do reprezentowania kolekcji w trakcie przetwarzania służy interfejs Stream<T>. Na strumieniach możemy wykonać wiele operacji, aby na końcu pobrać wynik (np. metoda agregująca) tych operacji bez zmiany stanu samego strumienia. Operacje na strumieniach można podzielić na dwa typy: termalne (ang. terminal), pośrednie (ang. intermediate)
Terminal– to takie które zamykają strumień. Kończą one pracę ze strumieniem. Może być tylko jedna operacja kończąca, w przeciwnym wypadku wystąpi wyjątek IllegalStateException.
Stream<String> names = Stream.of ("John", "Marry", "George", "Paul", "Alice", "Ann"); Predicate<String> hasName = name -> name.equals("Alice"); names.anyMatch(hasName); // ok names.noneMatch(hasName); // exception druga operacja kończaca // Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed // at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229) // at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459) // at bosch.com.article.StreamApiDemo.fourthDemo(StreamDemo.java:88) // at bosch.com.article.StreamApiDemo.main(StreamDemo.java:488)
W powyższym przykładzie użyto dwóch operacji typu terminal anyMatch() oraz noneMatch(). Wywołanie pierwszej z nich (anyMatch()) spowodowało „zamknięcie” strumienia, a wywołanie kolejnej na „zamkniętym” strumieniu skutkowało wygenerowaniem wyjątku. Operacje typu terminal są operacjami, które w wyniku działania nic nie zwracają (void), bądź też wynik działania takiej operacji nie jest już strumieniem np. ForEach.
Operacje pośrednie (intermediate)– metody, które operują na strumieniu, ale go nie zamykają. Dzięki temu, możliwy jest chaining czyli wykonywanie operacji w łańcuchu . Np. filter, map oraz sorted są operacjami typu intermediate. Operacje tego typu w wyniku działania zwracają strumień w postaci sekwencji elementów, stąd też takie operacje można ze sobą łączyć bez używania średnika w kodzie. Dodatkowo operacje pośrednie są typu laziness-seeking czyli leniwie inicjowane. W praktyce oznacza to że jeżeli operacje typu intermediet nie są zakończone operacją termalną to się nie wykonają. W przykładzie poniżej forEach jest operacją kończącą strumień.
List<String> namesList = Arrays.asList("John", "Marry", "George", "Paul", "Alice", "Ann"); namesList .stream().filter(e -> { System.out.println("filter: " + e); return true; }) .forEach(e -> System.out.println("forEach: " + e));