Programowanie w Javie wykład 15 Elementy programowania funkcyjnego cd.

Podobne dokumenty
Aplikacje w Javie wykład 13 Elementy programowania funkcyjnego

Aplikacje w Javie wykład 13 Elementy programowania funkcyjnego

Programowanie w Javie - wykład 14 Podstawy programowania funkcyjnego

Język JAVA podstawy. Wykład 3, część 3. Jacek Rumiński. Politechnika Gdańska, Inżynieria Biomedyczna

10.1. Wprowadzenie. List<jakiś_typ> target = creat e( src, tu podać warunek wyboru elementów z listy src, tu podać operację na wybranych elementach);

Platformy Programistyczne Podstawy języka Java

Dokumentacja do API Javy.

Kurs programowania. Wykład 9. Wojciech Macyna. 28 kwiecień 2016

Dawid Gierszewski Adam Hanasko

Programowanie w Javie wykład 9 Klasy wewnętrzne, klasy anonimowe Klasy opakowujące

Obszar statyczny dane dostępne w dowolnym momencie podczas pracy programu (wprowadzone słowem kluczowym static),

Programowanie w języku Java - Wyjątki, obsługa wyjątków, generowanie wyjątków

Język JAVA podstawy. wykład 2, część 1. Jacek Rumiński. Politechnika Gdańska, Inżynieria Biomedyczna

java.util.* :Kolekcje Tomasz Borzyszkowski

Aplikacje w Javie- wykład 11 Wątki-podstawy

Wykład 4: Klasy i Metody

Laboratorium 03: Podstawowe konstrukcje w języku Java [2h]

Programowanie obiektowe. Literatura: Autor: dr inŝ. Zofia Kruczkiewicz

Informatyka I. Klasy i obiekty. Podstawy programowania obiektowego. dr inż. Andrzej Czerepicki. Politechnika Warszawska Wydział Transportu 2018

Typy sparametryzowane

Wykład 8: klasy cz. 4

Programowanie obiektowe

Interfejsy. Programowanie obiektowe. Paweł Rogaliński Instytut Informatyki, Automatyki i Robotyki Politechniki Wrocławskiej

PROE wykład 3 klasa string, przeciążanie funkcji, operatory. dr inż. Jacek Naruniec

PROGRAMOWANIE FUNKCYJNE

Programowanie Obiektowe (Java)

Wykład 2: Podstawy Języka

JAVA W SUPER EXPRESOWEJ PIGUŁCE

Języki i techniki programowania Ćwiczenia 2

PHP: bloki kodu, tablice, obiekty i formularze

Aplikacje w środowisku Java

Lista, Stos, Kolejka, Tablica Asocjacyjna

Kurs programowania. Wykład 9. Wojciech Macyna

API STREAM WYRAŻENIA LAMBDA

Klasy. dr Anna Łazińska, WMiI UŁ Podstawy języka Java 1 / 13

Definiowanie własnych klas

Podstawy programowania. Podstawy C# Tablice

Rozdział 4 KLASY, OBIEKTY, METODY

Java: kilka brakujących szczegółów i uniwersalna nadklasa Object

Konstruktory. Streszczenie Celem wykładu jest zaprezentowanie konstruktorów w Javie, syntaktyki oraz zalet ich stosowania. Czas wykładu 45 minut.

Podstawy otwartych języków programowania Przechowywanie danych

Programowanie w Internecie. Java

Polimorfizm, metody wirtualne i klasy abstrakcyjne

Dziedziczenie. Streszczenie Celem wykładu jest omówienie tematyki dziedziczenia klas. Czas wykładu 45 minut.

Java. język programowania obiektowego. Programowanie w językach wysokiego poziomu. mgr inż. Anna Wawszczak

Klasy abstrakcyjne i interfejsy

Programowanie obiektowe i zdarzeniowe wykład 4 Kompozycja, kolekcje, wiązanie danych

Kurs programowania. Wykład 1. Wojciech Macyna. 3 marca 2016

Programowanie obiektowe

Języki C i C++ Wykład: 2. Wstęp Instrukcje sterujące. dr Artur Bartoszewski - Języki C i C++, sem. 1I- WYKŁAD

Kurs programowania. Wykład 3. Wojciech Macyna. 22 marca 2019

Kurs programowania. Wstęp - wykład 0. Wojciech Macyna. 22 lutego 2016

Programowanie w języku Java. Kolekcje

Język JAVA podstawy. Wykład 4, część 3. Jacek Rumiński. Politechnika Gdańska, Inżynieria Biomedyczna

Programowanie obiektowe

Programowanie obiektowe

IMIĘ i NAZWISKO: Pytania i (przykładowe) Odpowiedzi

Zofia Kruczkiewicz, Programowanie obiektowe - java, wykład 2 1

Programowanie obiektowe

Języki i metody programowania Java. Wykład 2 (część 2)

KOTLIN. Język programowania dla Androida

Wyjątki. Streszczenie Celem wykładu jest omówienie tematyki wyjątków w Javie. Czas wykładu 45 minut.

Programowanie obiektowe

Kurs programowania. Wykład 2. Wojciech Macyna. 17 marca 2016

Java pierwszy program w Eclipse «Grzegorz Góralski strona własna

dr inż. Piotr Czapiewski Tworzenie aplikacji w języku Java Laboratorium 1

Pierwsze kroki. Algorytmy, niektóre zasady programowania, kompilacja, pierwszy program i jego struktura

2. Klasy cz. 2 - Konstruktor kopiujący. Pola tworzone statycznie i dynamicznie - Funkcje zaprzyjaźnione - Składowe statyczne

Programowanie obiektowe zastosowanie języka Java SE

Lab 9 Podstawy Programowania

Polimorfizm. dr Jarosław Skaruz

Programowanie obiektowe

Java - tablice, konstruktory, dziedziczenie i hermetyzacja

Informacje ogólne. Karol Trybulec p-programowanie.pl 1. 2 // cialo klasy. class osoba { string imie; string nazwisko; int wiek; int wzrost;

Podstawy i języki programowania

Programowanie obiektowe

Kompilacja javac prog.java powoduje wyprodukowanie kilku plików o rozszerzeniu.class, m.in. Main.class wykonanie: java Main

PROE wykład 2 operacje na wskaźnikach. dr inż. Jacek Naruniec

Informatyka I. Typy danych. Operacje arytmetyczne. Konwersje typów. Zmienne. Wczytywanie danych z klawiatury. dr hab. inż. Andrzej Czerepicki

Języki i techniki programowania Ćwiczenia 4 Wzorce

Współbieżność i równoległość w środowiskach obiektowych. Krzysztof Banaś Obliczenia równoległe 1

Podstawy i języki programowania

import java.util.*; public class ListExample { public static void main(string args[]) { List<String> lista1= new ArrayList<String> ();

Obiekt klasy jest definiowany poprzez jej składniki. Składnikami są różne zmienne oraz funkcje. Składniki opisują rzeczywisty stan obiektu.

Programowanie obiektowe

Tablice i łańcuchy znakowe jako obiektowe typy danych. dr Jarosław Skaruz

Podejście obiektowe. Tablice obiektów Przykład 1 metody i atrybuty statyczne oraz niestatyczne

Java Collections Framework

Przypomnienie o klasach i obiektach

Wykład 3 Składnia języka C# (cz. 2)

Instrukcja 2 Laboratorium z Podstaw Inżynierii Oprogramowania

Kolekcje. Na podstawie:

Kiedy potrzebne. Struktura (rekord) Struktura w języku C# Tablice struktur. struktura, kolekcja

Podstawy programowania. Wykład: 5. Instrukcje sterujące c.d. Stałe, Typy zmiennych c.d. dr Artur Bartoszewski -Podstawy programowania, sem 1 - WYKŁAD

Techniki programowania INP001002Wl rok akademicki 2018/19 semestr letni. Wykład 3. Karol Tarnowski A-1 p.

Realizacja ekstensji klasy. Paulina Strzelecka, Tomasz Roszkowski

METODY I JĘZYKI PROGRAMOWANIA PROGRAMOWANIE STRUKTURALNE. Wykład 02

Wykład 5: Klasy cz. 3

Tablice (jedno i wielowymiarowe), łańcuchy znaków

JAVA. Platforma JSE: Środowiska programistyczne dla języka Java. Wstęp do programowania w języku obiektowym. Opracował: Andrzej Nowak

Transkrypt:

Programowanie w Javie wykład 15 1 Elementy programowania funkcyjnego cd. Treści prezentowane w wykładzie zostały oparte o: Barteczko, JAVA Programowanie praktyczne od podstaw, PWN, 2014 http://docs.oracle.com/javase/8/docs/ C. S. Horstmann, Java. Techniki zaawansowane, Helion, Gliwice 2017

Interfejsy funkcyjne i lambda-wyrażenia 2 Jakiego typu jest lambda-wyrażenie: n -> n + 1? Zależy to od kontekstu użycia. Gdy w metodzie main napiszemy np. int[] arr = {1, 2, 3; arrop(arr, n -> n+1); to na podstawie konkludowania typów lambda-wyrażenie będzie typu IntProcessor, a gdy napiszemy String[] sarr = {"Ala", "kot", "pies"; sarrop(sarr, n -> n+1); to, w wyniku konkluzji, typem tego lambda stanie się StringProcessor. Mamy tu do czynienia z wyrażeniami wielotypowymi (polytype expressions), czyli takimi, które mogą mieć różne typy w zależności od kontekstu użycia. Lambda-wyrażenia stanowią jeden z przypadków wyrażeń wielotypowych w Javie. Stanie się to jeszcze bardziej widoczne (i bardziej funkcjonalne), gdy nieco uogólnimy kody, stosując metody sparametryzowane.

Interfejsy funkcyjne i lambda-wyrażenia //Lambda-wyrażenia - konkludowanie typów interface Processor<T> { T process(t val); public class ArrProc { public static <T> void processarr(t[] arr, Processor<T> p ){ for (int i = 0; i < arr.length; i++) { arr[i] = p.process (arr[i]); public static void main(string[] args) { String[] sa = { "a", "b", "c" ; Integer[] ia = (1, 2, 3); processarr(sa, v -> v+l); processarr(ia, v -> v+l); // konkludowanie typów // konkludowanie typów processarr (sa, v -> v.touppercase()); // konkludowanie typów processarr(ia, v -> v*v); System.out.println(Arrays.toString(sa)) ; System.out.println(Arrays.toString(ia)); //wynik: [A1, B1, C1] [4, 9, 16] // konkludowanie typów 3

Interfejsy funkcyjne i lambda-wyrażenia Jak już widzieliśmy, lambda-wyrażenia można stosować jako uproszczenie w użyciu anonimowych klas wewnętrznych, implementujących interfejsy funkcyjne. Podobnie jak w lokalnych klasach wewnętrznych lambda-wyrażenie ma dostęp do wszystkich składowych klasy otaczającej, przy czym pola tej klasy mogą być przez lambda-wyrażenie modyfikowane; do zmiennych lokalnych, ale muszą one albo być deklarowane ze specyfikatorem final albo być efektywnie finalne (effective final). Inaczej niż w anonimowych klasach wewnętrznych lambda-wyrażenie nie wprowadza nowego zasięgu widoczności i dostępności, co oznacza m.in., że zmienna this jest referencją do obiektu klasy, w której tworzone jest lambdawyrażenie (w klasie anonimowej this oznacza obiekt tej klasy); słowo kluczowe super oznacza nadklasę obiektu (klasy), w której tworzone jest lambda-wyrażenie; jeśli lambda-wyrażenie występuje w bloku, niedopuszczalne jest przesłanianie zmiennych lokalnych z bloku otaczającego (w klasie lokalnej anonimowej mamy nowy zasięg widoczności, więc możemy używać tych samych identyfikatorów zmiennych, co w bloku otaczającym). 4

Referencje do metod i konstruktorów 5 Często definicja lambda-wyrażenia sprowadza się do wywołania metody istniejącej klasy lub utworzenia jej obiektu. Wtedy zamiast wywołania tej metody w lambdawyrażeniu możemy zastosować referencję do metody lub konstruktora. Są cztery rodzaje takich referencji (symbolicznie przez x oznaczamy jeden parametr lub listę parametrów w nawiasach okrągłych): NazwaKlasy::nazwa_metody_statycznej Klasa::metStat - odpowiada lambda-wyrażeniu x -> Klasa.metStat(x) NazwaKlasy::nazwa_metody_niestatycznej Klasa::met - odpowiada lambda-wyrażeniu x -> x.met() zmienna_niestatyczna::nazwa_metody_niestatycznej Klasa v; v::met - odpowiada lambda-wyrażeniu x -> v.met(x) NazwaKlasy::new Klasa::new - odpowiada lambda-wyrażeniu x -> new Klasa(x)

Referencje do metod i konstruktorów 6 Naturalnie, liczba i typy parametrów oraz typ wyniku metody (konstruktora), do których używamy referencji, muszą odpowiadać deskryptorowi funkcji jakiegoś interfejsu funkcyjnego (który zostanie dobrany do realizacji lambda-wyrażenia). Najlepiej zobaczyć to na przykładzie. Załóżmy, że w kontekście kompilacji widoczne są interfejsy funkcyjne: interface Transformer<T,S> { T transform(s v); oraz następująca klasa Animal: class Animal{ String type; public Animal(String type) { this.type = type; public String tostring() { return "Animal{type=" + type + "";

Referencje do metod i konstruktorów 7 Zastosowanie referencji do metod i konstruktorów ilustruje poniższy kod: public class MetRef { static <T,S> List<T> create(list<s> src, Transformer<T,S> t){ List<T> target = new ArrayList<>(); for (S s : src) target.add(t.transform(s)); return target;

Referencje do metod i konstruktorów public static void main(string[] args) { List<String> s = Arrays.asList( "pies", "kot", "ryba" ); // zamiast List out = create(s, e -> e.touppercase()); System.out.println(out); //[PIES, KOT, RYBA] // możemy napisać out = create(s, String::toUpperCase); System.out.println(out); //[PIES, KOT, RYBA] String text = "pies i kot są w domu, a ryba pływa"; // zamiast out = create(s, e -> text.indexof(e)); System.out.println(out);//[0, 7, 24] // możemy napisać out = create(s, text::indexof); System.out.println(out);//[0, 7, 24] //zamiast out = create(s, e -> new Animal(e)); System.out.println(out); //[Animal{type=pies, Animal{type=kot, Animal{type=ryba] // możemy napisać out = create(s, Animal::new); System.out.println(out); //[Animal{type=pies, Animal{type=kot, Animal{type=ryba] 8

Gotowe interfejsy funkcyjne 9 Jak już wspominaliśmy, najczęściej nie musimy tworzyć własnych interfejsów funkcyjnych. Po pierwsze, w Javie już od lat jest wiele interfejsów typu SAM (np. znany nam FileFilter ). Po drugie, w wersji 8 pojawił się pakiet java.util.function, w którym zdefiniowano wiele interfejsów funkcyjnych dla częstych przypadków użycia lambda-wyrażeń, np. Predicate<T>, Function<S, T>, UnaryOperator<T>, Supplier<T>, Consumer<T>(), BiPredicate<U,V>, BiFunction<U,V,R>, BinaryOperator<T>, BiConsumer<U,V>. Przedstawimy, teraz interfejsy z których już korzystaliśmy: A)FileFilter. Ma on jedną metodę boolean accept(file f), wykorzystywaną do selekcji obiektów plikowych np. w trakcie listowania zawartości katalogu metodą listfiles (tu jako argument podajemy właśnie implementację metody accept). Zatem dla uzyskania tablicy plików z danego katalogu dir, mających podane rozszerzenie ext i czas modyfikacji późniejszy od podanego time możemy napisać: File[] files = new File(dir).listFiles( f -> f.isfile() && f.getname().endswith("."+ext) && f.lastmodified() > time);

Gotowe interfejsy funkcyjne 10 C)Comparator. Jak pamiętamy, ma jedną metodę int compare(t obiekt1, T obiekt2). Przy porównywaniu i sortowaniu możemy więc używać wygodnych lambdawyrażeń, np. do uporządkowania listy napisów wg ich długości: List<String> lst = Arrays.asList("ala", "ma", "kota", "i", "pieska"); Collections.sort(lst, (s1, s2) -> s2.length() - s1.length()); System.out.println(lst); Powyższy fragment wyprowadzi: [pieska, kota, ala, ma, i]

Lambda-wyrażenia w gotowych metodach W Javie 8 pojawiły się nowe metody w standardowych klasach i interfejsach (metody statyczne i/lub domyślne), których argumentami mogą być lambda-wyrażenia. Jest ich całkiem sporo. Omówimy krótko wybrane z nich. W interfejsie Iterable dodano domyślną metodę foreach(consumer op) (interfejs Consumer<T> ma metodę void accept(t t) umożliwiającą wykonywanie operacji na przekazanym obiekcie bez zwracania wyniku) dzięki której na dowolnych elementach zestawu danych, określanego przez implementację Iterable (m.in. na dowolnej kolekcji), możemy wykonywać podaną operację op. Aby przyjrzeć się bliżej działaniu foreach, zdefiniujmy metodę zwiększającą wartości elementów przekazanej listy liczb całkowitych. Argumentem lambda-wyrażenia w foreach będzie kolejny element listy. Nie mamy dostępu do indeksu tego elementu, zatem nie mogąc przeprowadzić modyfikacji za pomocą metody set(i,val) interfejsu List, musimy stworzyć i zwrócić nową listę, która będzie zawierać zmienione wartości : static List<Integer> increase(list<integer> inlist, int n) { List<Integer> out = new ArrayList<>(); inlist.foreach(e -> out.add(e+n)); return out; public static void main(string[] args) { List<Integer> in = Arrays.asList(1, 7, 11, 19); System.out.println(in); //[1, 7, 11, 19] in = increase(in, 2); System.out.println(in); //[3, 9, 13, 21] 12

Lambda-wyrażenia w gotowych metodach foreach różni się zdecydowanie od zwykłych pętli (for i rozszerzonego for). Gdybyśmy np. chcieli przerwać zwiększanie o 1 wartości elementów, gdy natrafimy na wartość > 9: inlist.foreach(e -> { if (e > 9) break; out.add(e+n); ); to uzyskalibyśmy błąd w kompilacji. W foreach nie wolno stosować instrukcji break. Również instrukcja return ma inne znaczenie niż zwykle. Nie zwraca sterowania z metody, w której użyliśmy foreach, kończy jedynie wykonanie lambda-wyrażenia dla aktualnie przetwarzanego argumentu. W takim kontekście: void show(list<integer> inlist) { inlist.foreach(e -> { ); //... if (e == 11) return; System.out.println(e); show(arrays.aslist(1, 7, 11, 19)); na konsoli nie uzyskamy 1, 7 tylko 1, 7, 19. W metodzie foreach instrukcja return przechodzi do następnej iteracji (działa tak jak continue w normalnych" pętlach). 13

Lambda-wyrażenia w gotowych metodach Aby z lambda-wyrażenia uzyskać zewnętrzną instrukcję return, zwracającą sterowanie z metody, w której to lambda-wyrażenie jest wykonywane, korzystamy z wyjątków: void show(list<integer> inlist) { try { inlist.foreach(e -> { ); if (e == 11) throw new RuntimeException(); System.out.println(e); catch(runtimeexception exc) { return; Dla inlist = [1,7,11,19] na konsoli uzyskamy tylko dwa elementy: 1 i 7. Tutaj dodatkowo zamiast ogólnego RuntimeException należałoby zdefiniować własną klasę wyjątku np. ExternalReturnFromLambda. W przeciwnym razie każdy wyjątek typu RuntimeException, powstający przy wykonaniu operacji w ciele lambda-wyrażenia, powodowałby zakończenie działania otaczającej metody. 14

Lambda-wyrażenia w gotowych metodach Drobną innowacją, jest dodanie domyślnej metody foreachremaining do interfejsu Iterator, która wykonuje podane lambda-wyrażenie na wszystkich w danym momencie pozostałych elementach (być może po wcześniejszym użyciu metody next() Iteratora). Utwórzmy listę wynikową, której pierwszy element będzie sumą dwóch pierwszych elementów listy wejściowej, a pozostałe będą zwiększone o 1: List<Integer> specialop(list<integer> inlist) { List<Integer> out = new ArrayList<>(); Iterator<Integer> it = inlist.iterator(); out.add(it.next() + it.next()); it.foreachremaining(e -> out.add(e+1)); return out; // in jest listą [1, 7, 11, 19] in = specialop(in); System.out.println(in); Ten fragment kodu wyprowadzi na konsolę: [8, 12, 20]. Naturalnie, gdy foreach iteruje po obiektach klas modyfikowalnych, możemy zmieniać ich stany: employeelist.foreach(e -> e.setsalary(e.getsalary()*1.2)); Metoda setsalary() ma typ wyniku void (co jest zgodne z kontraktem dla interfejsu Consumer, używanym pod spodem" lambda-wyrażenia w metodzie foreach), wobec tego mogliśmy zastosować w ciele lambdy pojedyncze wyrażenie. Gdyby metoda zwracała wynik, w lambda-wyrażeniu należałoby użyć bloku instrukcji, z których ostatnią byłoby proste return. 15

Lambda-wyrażenia w gotowych metodach Wygodnie używa się foreach, gdy lambda-wyrażeniem jest referencja do metody niezwracającej wyniku, której argumentem jest obiekt iterowanego zestawu. Sztandarowym przykładem jest wypisywanie informacji na konsolę. Zamiast pisać: kolekcja.foreach(e -> System.out.println(e)); możemy użyć referencji do metody println: kolekcja.foreach (System.out::println); Bardzo użyteczne jest foreach zdefiniowane jako domyślna metoda interfejsu Map. Lambda-wyrażenie jest tu BiConsumerem, czyli ma dwa argumenty (klucz i wartość kolejnego wejścia w mapie) oraz typ wyniku void. Dzięki temu przeglądanie map jest znacznie łatwiejsze, np. zamiast for(map.entry me : map.entryset()) { ; // tu coś robimy z kluczem, uzyskanym przez me.getkey() // i z wartością spod klucza pobraną przez me.getvalue() piszemy: map.foreach ( (key, val) -> robimy coś z key i val ); 16

Lambda-wyrażenia w gotowych metodach 17 Istotne dodatki pojawiły się w interfejsie Comparator. Wiemy już, że tam, gdzie trzeba dostarczyć komparatora możemy podać lambda-wyrażenie. Załóżmy, że mamy klasę Employee z polami: String lname (nazwisko), String fname (imię), Integer age (wiek), Double salary (pensja) z odpowiednimi metodami get zwracającymi wartość danego pola. Mając listę pracowników lemps i używając domyślnej metody sort interfejsu List, możemy sortować listę wg różnych kryteriów, np. wg wieku: List<Employee> lemps =... System.out.println(lemps); // oryginalna lista lemps.sort((el, e2) -> el.getage().compareto(e2.getage())); System.out.println(lemps); po posortowaniu w wyniku dostaniemy: [Jeż Jan (34,4100.0), Es Joe (41,3600.0), As Ala (27,3700.0)] [As Ala (27,3700.0), Jeż Jan (34,4100.0), Es Joe (41,3600.0)] Zauważmy, że w lambda-wyrażeniu, reprezentującym komparator, wywołujemy na rzecz obu porównywanych obiektów metodę klasy Employee (getage), a ponieważ wynikiem jest Integer (implementujący interfejs Comparable), to do porównania wartości możemy zastosować metodę compareto.

Lambda-wyrażenia w gotowych metodach 18 Użyteczna domyślna metoda removeif(predicate) została zdefiniowana w interfejsie Collection (interfejs Predicate<T> ma metodę boolean test(t v)). Usuwa ona wszystkie elementy kolekcji, których wartości spełniają podany w postaci predykatu warunek, np. Set<String> slist = new HashSet(Arrays.asList( "Cypr wyspa", "Madagaskar - wyspa", "Krabi", "Londyn")); slist.removeif(s ->!s.endswith("wyspa")); slist.foreach(system.out::println); da w wyniku: Cypr - wyspa Madagaskar wyspa W standardowych klasach i interfejsach Javy znajdziemy wiele innych gotowych metod, które pozwalają podawać jako argumenty lambda-wyrażenia.

Przetwarzanie strumieniowe Strumień (nie należy mylić tego pojęcia ze strumieniami wejścia-wyjścia) stanowi sekwencję elementów, ale nie przechowuje tych elementów w strukturach danych, tylko konstytuuje potok (połączenie) różnych operacji na elementach. Potok operacji ma swoje źródło (można to nazwać też źródłem strumienia), który może być np. kolekcją, tablicą lub napisem (ciągiem znaków). Operacje, łączone w potoku, mogą być pośrednie - dające w wyniku inny strumień (np. map, filter), przy czym operacje te mogą być bezstanowe - niewymagające od strumienia znajomości wartości poprzednio przetwarzanych elementów (np. filter lub foreach); takie operacje są bardzo efektywne; stanowe - wymagające od strumienia pamiętania stanów innych, od akurat przetwarzanego, elementów (np. sortowanie elementów strumienia); terminalne - kończące przetwarzanie strumienia (np. collect i foreach); po wykonaniu operacji terminalnej nie można wykonać na strumieniu żadnej innej operacji; skracające (short-circuit) - takie, które powodują, że wcześniejsze w potoku operacje pośrednie kończą działanie, gdy rezultat operacji skracającej jest jasny; operacje te mogą być zarówno pośrednie, jak i terminalne. 19

Przetwarzanie strumieniowe 20 Strumienie charakteryzują się ważnymi cechami. Operacje pośrednie są leniwe (lazy operations), czyli nie są wykonywane ani nie generują żadnych wartości, dopóki nie zostanie wywołana operacja terminalna, a elementy strumienia są przetwarzane tylko w takim zakresie, jaki jest potrzebny do uzyskania wymaganego wyniku (np. gdy rezultat operacji skracającej jest w pełni określony). Elementy strumienia mogą być generowane ad hoc za pomocą generatorów i iteratorów (ogólnie takie strumienie są nieskończone; w praktyce zawsze jakoś kończymy ich przetwarzanie, np. generator liczb pseudolosowych może generować nieskończoną ich sekwencję, a my ograniczamy ją do pierwszych 100). Operacje na strumieniach mogą być wykonywane równolegle (w odrębnych wątkach), przy czym bardzo łatwo to możemy zadekretować (np. za pomocą metody parallel), a podziałem pracy między równolegle wykonujące się podzadania zajmie się JVM. We wszystkich operacjach strumieniowych podajemy lambda-wyrażenia, co sprzyja zwięzłości i czytelności kodu.

Przetwarzanie strumieniowe 21 Strumienie są obiektami klasy implementującej interfejs Stream. Strumienie możemy uzyskać m.in.: od kolekcji - za pomocą metody stream() z interfejsu Collection, np. list.stream(); od tablic-za pomocą statycznej metody stream() z klasy Arrays, np. Arrays. stream(array); od napisów (strumień znaków) - za pomocą metody chars(); od podanych wartości, w tym tablic obiektowych (varargs), za pomocą metody Stream.of(zestaw wartości); od plików (strumień wierszy pliku) - za pomocą statycznej metody lines(...) z klasy Files lub lines() z klasy BuferredReader; od katalogów (strumień reprezentujący drzewo katalogowe obiektów plikowych) - za pomocą statycznej metody walk (...) lub find(...) z klasy Files; od archiwów (np. ZipFile lub JarFile; tu strumień reprezentuje uporządkowane elementy archiwum entries) za pomocą metody stream(); od wyrażenia regularnego, reprezentowanego przez obiekt klasy Pattern - za pomocą metody splitasstream(napis), zwracającej strumień symboli napisu, wyłuskanych za pomocą tego wyrażenia; za pomocą generowania elementów metodami Stream.generate(...) lub Stream.iterate(... ); za pomocą statycznej metody ints() klasy Random (strumień pseudolosowych liczb całkowitych; przez leniwe połączenie strumieni statyczną metodą Stream.concat(strum1, strum2).

Przetwarzanie strumieniowe 22 Podstawowe operacje pośrednie: map(function f) - zwraca strumień z elementami tego strumienia przekształconymi za pomocą podanej funkcji (transformacja elementów). flatmap(function f) - zwraca strumień z elementami tego strumienia przekształconymi za pomocą podanej funkcji, przy czym zastosowanie tej funkcji musi dawać strumienie, a po odwzorowaniu ich elementy będą stanowić jeden strumień (nastąpi swoiste spłaszczenie, połączenie różnych zestawów danych w jeden). filter(predicate p) - zwraca strumień elementów, dla których predykat p daje true (selekcja elementów). distinct() - zwraca strumień elementów, różniących się w sensie equals() (wybór niepowtarzających się elementów) - operacja stanowa. sorted(...) - zwraca strumień posortowanych elementów (w porządku naturalnym lub wg podanego komparatora) - operacja stanowa. unordered() - zwraca nieuporządkowany strumień. limit(int n) - zwraca strumień zawierający n pierwszych elementów tego strumienia - operacja skracająca. generate(supplier s) - zwraca strumień elementów, z których każdy jest tworzony przez podanego dostawcę", np. strumień stałych lub losowych wartości.

Przetwarzanie strumieniowe 23 iterate(t init, UnaryOperator op) - zwraca strumień tworzony przez iteracyjne zastosowanie operatora op wobec inicjalnej wartości init. substream(...) - zwraca część strumienia (np. od 10 elementu do końca lub od 5 do 20) - operacja skracająca. parallel () - zwraca strumień, na którym operacje będą wykonywane równolegle. sequential() - zwraca strumień, na którym operacje będą wykonywane sekwencyjnie. Wybrane operacje terminalne: allmatch(predicate p) - zwraca true, jeśli wszystkie elementy strumienia spełniają warunek p, false w przeciwnym razie. anymatch(predicate p) - j.w., ale pytanie brzmi: czy jakikolwiek element strumienia spełnia warunek; jest to operacja skracająca. nonematch(predicate p) - j.w, czy żaden element strumienia nie spełnia podanego warunku - operacja skracająca. findany() - zwraca dowolny element strumienia jako obiekt typu Optional, zawierający wartość tego elementu lub wartość pustą, jeśli strumień jest pusty - operacja skracająca. findfirst() - zwraca pierwszy element strumienia (jako Optional z jego wartością lub wartością pustą, gdy strumień jest pusty) - operacja skracająca. long count() - zwraca liczbę elementów strumienia.

Przetwarzanie strumieniowe 24 void foreach(consumer c) - wykonuje podane działanie na każdym elemencie strumienia; ale kolejność przetwarzania nie musi być taka sama, jak kolejność w zestawie, na który nałożono strumień (np. elementy listy mają swoją kolejność, ale foreach na strumieniu związanym z listą może wykonywać działania w innej kolejności). foreachordered(consumer) - j.w., ale jeśli dane, od których uzyskano strumień, mają swoją kolejność, to przy przetwarzaniu zostanie ona zachowana. reduce(...) - wykonuje redukcję elementów strumienia, czyli ogólnie - na podstawie zestawu jego elementów, stosując wobec nich odpowiednie operacje akumulujące czy kombinujące, wytwarza pewną wynikową wartość (np. sumę). collect(...) - wykonuje tzw. redukcję modyfikowalną, akumulującą elementy strumienia do modyfikowalnego kontenera, takiego jak kolekcja, mapa, bufor znakowy, inny strumień. max(...) i min( ) - zwracają największy (najmniejszy) element strumienia w porządku określonym przez podany jako argument komparator. toarray( ) - zwraca tablicę elementów strumienia.

Przetwarzanie strumieniowe 25 Możemy teraz zająć się przykładowymi zastosowaniami. Będziemy przy tym używać klasy opisującej państwa. Załóżmy, że mamy plik z informacjami o krajach (zaczerpnięty z serwisu GeoNames, gdzie wymienione są nie tylko niepodległe państwa, ale również regiony, które mają odrębne kody ISO2; w informacjach tych nie ma gęstości zaludnienia, a kontynenty są dane tylko w postaci kodów). Na wiersze tego pliku możemy nałożyć strumień i łatwo je przetwarzać na różne sposoby. Utwórzmy najpierw listę obiektów klasy Country. Zastosujemy metody map i collect strumieni do tworzenia listy obiektów klasy na podstawie wierszy pliku: Przetwarzanie strumieniowe Path p = Paths.get("nazwa_pliku"); Stream<String> ls = Files.lines(p, Charset.defaultCharset()); List<Country> clist = ls.map(country::new).collect(collectors.tolist()); Tutaj map(country::new) tworzy strumień obiektów klasy Country (na wierszach pliku jest wywoływany jej konstruktor, który inicjuje pola na podstawie informacji ze stringa), a metoda collect posługuje się predefiniowanym obiektem-kolektorem (statyczne odwołanie do klasy Collectors), który dodaje elementy strumienia do nowo utworzonej listy. Klasa Collectors dostarcza wielu innych gotowych kolektorów do wykorzystania w metodzie collect (np. do tworzenia różnych rodzajów kolekcji, a także grupowania elementów strumienia pod kluczami w mapach).

Przetwarzanie strumieniowe Od utworzonej listy krajów możemy pobrać strumień i wykonywać na nim inne operacje, np. przez clist.stream().count() dowiemy się, że mamy informacje o 250 krajach. Spróbujmy teraz odnaleźć pierwszy kraj w strumieniu, którego nazwa zaczyna się na B" i pobrać jego nazwę. Optional<String> first = clist.stream().map(country::getname) // strumień wszystkich nazw.filter(s -> s.startswith("b")) //strumień nazw na B.findFirst(); // pierwsza nazwa na B String nazwa = first.get(); System.out.println(nazwa); Jako wynik findfirst() mamy Optional, bo w strumieniu może nie być żadnego elementu. Optional opakowuje uzyskaną wartość, musimy ją jeszcze pobrać za pomocą metody get(). Jednak jeśli jej nie ma (np. w filter wyszukujemy kraje o nazwie zaczynającej się na X), to wywołanie get() spowoduje wyjątek. 26

Przetwarzanie strumieniowe Aby tego uniknąć, powinniśmy zastosować następująca konstrukcję: Optional<String> first... String nazwa = first.orelse(...) której rezultatem będzie wynik get() (jeśli jest), albo podany przez nas argument metody orelse. Dobrze to widać w następującym fragmencie : static String firstwithprefix(stream<country> str, String p){ return str.map(country::getname).filter(s -> s.startswith(p)).findfirst().orelse("nie ma kraju na " + p); //... List<Country> clist =... String nazwa = firstwithprefix(clist.stream(), "B"); System.out.println(nazwa); nazwa = firstwithprefix(clist.stream(), "X"); System.out.println(nazwa); Fragment ten wyprowadzi: Bośnia and Herzegovina nie ma kraju na X 27