Java 8 Stream: savladajte sustinu tokovnog programiranja
Još pre dve nedelje neki citaoc je energicno trazio da napisem clanak o Java Stream-u. Rekao sam mu da ih na netu vec ima na hiljade, a znaš šta mi je odgovorio? „Hocu bas tvoj!” Eto vidiš, kako je to toplo prepoznavanje. Pa ajmo onda „na pritisak” da napisem jedan, hehe.

Gledano samo na rec „Stream”, cini se da ima veze sa InputStream-om i OutputStream-om iz paketa java.io. A zapravo — nema veze. Stream uveden u Javi 8 sluzi da oslobodi programere tereta pri radu sa kolekcijama (Collection); jedan veliki deo zasluga za to ide Lambda izrazima koji su se pojavili istovremeno — oni su drasticno poboljsali i efikasnost programiranja i citljivost koda.
Šta je zapravo Stream?
Stream je poput naprednog iteratora, ali moze se proci samo jednom — kao sto prolece prolazi i reka tece ka istoku; tokom protoka, nad elementima toka se izvrsavaju odredjene operacije, poput „izbaci stringove duze od 10”, „uzmi prvo slovo svakog stringa” i sl.
Da bismo radili sa tokom, prvo nam treba izvor podataka — to moze biti niz ili kolekcija. Svaka operacija vraca novi objekat toka, što olaksava ulancavanje operacija, pri cemu originalni objekat toka ostaje nepromenjen.
Operacije nad tokom mogu se podeliti u dve kategorije:
Medjuoperacije — moze ih biti vise; svaka vraca novi tok i omogucava ulancavanje.
Terminalne operacije — moze biti samo jedna; nakon njenog izvrsenja taj tok je potrosen i ne moze se izvrsiti sledeca operacija, pa se uvek nalazi na kraju.
Pogledajmo primer.
List<String> list = new ArrayList<>();
list.add("Srecno Vuhan");
list.add("Srecno Kina");
list.add("Srecno svet");
list.add("Srecno svet");
long count = list.stream().distinct().count();
System.out.println(count);Metoda distinct() je medjuoperacija (uklanja duplikate) i vraca novi tok (bez ponavljanja elemenata).
Stream<T> distinct();Metoda count() je terminalna operacija i vraca broj elemenata u toku.
long count();Medjuoperacije se ne izvrsavaju odmah; tek nakon terminalne operacije tok zaista pocinje da se obilazi, radi mapiranja, filtriranja i sl. Prosto receno — vise operacija se izvrsava u jednom prolazu, što drasticno poboljsava performanse.
Toliko o teoriji; predjimo na praksu.
01. Kreiranje toka
Ako je izvor niz, mozes koristiti Arrays.stream() ili Stream.of(); ako je izvor kolekcija, mozes direktno koristiti metodu stream(), jer je ona vec dodata u Collection interfejs.
public class CreateStreamDemo {
public static void main(String[] args) {
String[] arr = new String[]{"Srecno Vuhan", "Srecno Kina", "Srecno svet"};
Stream<String> stream = Arrays.stream(arr);
stream = Stream.of("Srecno Vuhan", "Srecno Kina", "Srecno svet");
List<String> list = new ArrayList<>();
list.add("Srecno Vuhan");
list.add("Srecno Kina");
list.add("Srecno svet");
stream = list.stream();
}
}Ako pogledas izvorni kod klase Stream, vidces da metoda of() interno poziva Arrays.stream().
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}Pored toga, kolekcija moze pozvati i metodu parallelStream() kojom se kreira paralelni tok — podrazumevano koristi bazen niti ForkJoinPool.commonPool().
List<Long> aList = new ArrayList<>();
Stream<Long> parallelStream = aList.parallelStream();02. Operacije nad tokom
Klasa Stream nudi mnoge korisne metode za rad sa tokom; izdvojicu neke od najcesce koriscenih.
1) Filtriranje
Metodom filter() iz toka mozemo izdvojiti elemente koje zelimo.
public class FilterStreamDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Zhou Jielun");
list.add("Wang Lihong");
list.add("Tao Zhe");
list.add("Lin Junjie");
Stream<String> stream = list.stream().filter(element -> element.contains("Wang"));
stream.forEach(System.out::println);
}
}Metoda filter() prima parametar tipa Predicate (funkcionalni interfejs uveden u Javi 8 koji prihvata jedan ulazni argument i vraca boolean), pa joj Lambda izraz mozemo proslediti direktno — na primer element -> element.contains("Wang") izdvaja one stringove koji sadrze „Wang”.
Metoda forEach() prima parametar tipa Consumer (funkcionalni interfejs uveden u Javi 8 koji prihvata jedan ulazni argument i ne vraca nista). ImeKlase :: imeMetode je nova sintaksa uvedena u Javi 8; System.out vraca klasu PrintStream, a za println verujem da znas da stampa.
stream.forEach(System.out::println); je ekvivalentno stampanju unutar for petlje i licni na sledeci kod:
for (String s : strs) {
System.out.println(s);
}Ocigledno, jedna linija koda izgleda sazetije. Rezultat programa:
Wang Lihong2) Mapiranje
Ako zelimo nekom operacijom pretvoriti elemente jednog toka u elemente novog toka, mozemo koristiti metodu map().
public class MapStreamDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Zhou Jielun");
list.add("Wang Lihong");
list.add("Tao Zhe");
list.add("Lin Junjie");
Stream<Integer> stream = list.stream().map(String::length);
stream.forEach(System.out::println);
}
}Metoda map() prima parametar tipa Function (funkcionalni interfejs uveden u Javi 8 koji prihvata ulazni argument T i vraca rezultat R). U ovom slucaju argument je metoda length klase String, što znaci da se tok Stream<String> pretvara u tok Stream<Integer>.
Izlaz programa je:
3
3
2
33) Poklapanje
Klasa Stream nudi tri metode za poklapanje (match) elemenata:
anyMatch()— vraca true ako bar jedan element poklapa zadati uslov.allMatch()— vraca false ako bar jedan element ne poklapa uslov; ako se svi poklapaju, vraca true.noneMatch()— vraca false ako bar jedan element poklapa uslov; ako nijedan ne poklapa, vraca true.
public class MatchStreamDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Zhou Jielun");
list.add("Wang Lihong");
list.add("Tao Zhe");
list.add("Lin Junjie");
boolean anyMatchFlag = list.stream().anyMatch(element -> element.contains("Wang"));
boolean allMatchFlag = list.stream().allMatch(element -> element.length() > 1);
boolean noneMatchFlag = list.stream().noneMatch(element -> element.endsWith("Chen"));
System.out.println(anyMatchFlag);
System.out.println(allMatchFlag);
System.out.println(noneMatchFlag);
}
}Buduci „Wang Lihong” sadrzi znak „Wang”, anyMatchFlag je true; buduci duzine stringova „Zhou Jielun”, „Wang Lihong”, „Tao Zhe” i „Lin Junjie” sve prekoracuju 1, allMatchFlag je true; buduci nijedan od cetiri stringa ne zavrsava se na „Chen”, noneMatchFlag je true.
Izlaz programa je:
true
true
true4) Kombinovanje
Glavna svrha metode reduce() je da kombinuje elemente Stream-a; ima dva oblika:
Optional<T> reduce(BinaryOperator<T> accumulator)
Bez pocetne vrednosti, sa samo jednim parametrom koji predstavlja pravilo izracunavanja; tada se vraca Optional.
T reduce(T identity, BinaryOperator<T> accumulator)
Sa pocetnom vrednosti i pravilom izracunavanja — dva parametra; tada je tip povratne vrednosti isti kao tip pocetne vrednosti.
Pogledajmo sledeci primer.
public class ReduceStreamDemo {
public static void main(String[] args) {
Integer[] ints = {0, 1, 2, 3};
List<Integer> list = Arrays.asList(ints);
Optional<Integer> optional = list.stream().reduce((a, b) -> a + b);
Optional<Integer> optional1 = list.stream().reduce(Integer::sum);
System.out.println(optional.orElse(0));
System.out.println(optional1.orElse(0));
int reduce = list.stream().reduce(6, (a, b) -> a + b);
System.out.println(reduce);
int reduce1 = list.stream().reduce(6, Integer::sum);
System.out.println(reduce1);
}
}Pravilo izracunavanja moze biti Lambda izraz (npr. (a, b) -> a + b) ili oblik ImeKlase::imeMetode (npr. Integer::sum).
Rezultat programa:
6
6
12
12Bez pocetne vrednosti, 0+1+2+3 jednako je 6; sa pocetnom vrednosti 6 rezultat je 12.
03. Konverzija toka
Ako kolekciju ili niz mozemo pretvoriti u tok, onda bi trebalo da postoji i odgovarajuci metod da tok vratimo nazad — metoda collect() ispunjava tu potrebu.
public class CollectStreamDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Zhou Jielun");
list.add("Wang Lihong");
list.add("Tao Zhe");
list.add("Lin Junjie");
String[] strArray = list.stream().toArray(String[]::new);
System.out.println(Arrays.toString(strArray));
List<Integer> list1 = list.stream().map(String::length).collect(Collectors.toList());
List<String> list2 = list.stream().collect(Collectors.toCollection(ArrayList::new));
System.out.println(list1);
System.out.println(list2);
String str = list.stream().collect(Collectors.joining(", ")).toString();
System.out.println(str);
}
}Metoda toArray() pretvara tok u niz. Mozda te znatizelja šta je String[]::new — pogledaj izvorni kod metode toArray().
<A> A[] toArray(IntFunction<A[]> generator);Dakle, String[]::new je IntFunction — funkcija koja moze kreirati novi niz potrebne duzine; mozes pogledati decompajlirani bajtkod da vidis šta zapravo radi:
String[] strArray = (String[])list.stream().toArray((x$0) -> {
return new String[x$0];
});
System.out.println(Arrays.toString(strArray));Dakle, ekvivalentno je vracanju niza stringova zadate duzine.
Kada zelimo kolekciju po nekom pravilu pretvoriti u drugu kolekciju, mozemo kombinovati map() i collect().
List<Integer> list1 = list.stream().map(String::length).collect(Collectors.toList());Nakon što smo metodom stream() kreirali tok kolekcije, metodom map(String::length) mapiramo ga u novi tok duzina stringova, a zatim metodom collect() taj tok pretvaramo u novu kolekciju.
Collectors je alatna klasa za kolektore; ugradjena je citava serija implementacija — na primer, metoda toList() skuplja elemente u novi java.util.List; metoda toCollection() ih skuplja u novi java.util.ArrayList; metoda joining() skuplja elemente u string koji se moze razdvojiti zadatim separatorom.
Pogledajmo rezultat programa:
[Zhou Jielun, Wang Lihong, Tao Zhe, Lin Junjie]
[3, 3, 2, 3]
[Zhou Jielun, Wang Lihong, Tao Zhe, Lin Junjie]
Zhou Jielun, Wang Lihong, Tao Zhe, Lin Junjie
