Java folyamok

Kategória: Java standard könyvtárak.

Áttekintés

A folyam (stream) a Java 8-ban jelent meg, a nyelv funkcionális kiegészítőivel együtt. Nem összetévesztendő a Java IO stream fogalmával, pl. a FileInputStream-mel, bár rokon fogalmak. Ez a stream leginkább a Java Collections API kiterjesztéseként is felfogható: az ott definiált adatszerkezetekben található adatok átalakíthatóak folyammá, melyen egyesével hajtunk végre műveleteket. E megközelítéssel fel tudunk dolgozni akkora adatmennyiséget is, ami egyszerre nem fér be a memóriába, akár párhuzamosan is, elméletben akár fizikailag több számítógépen. A folyamon végrehajtható műveleteket két fő csoportba osztjuk, melyek megértése nélkülözhetetlen a stream-ek megértéséhez:

  • Nem végműveletek (nonterminal operations): egy-egy ilyen művelettel azt mondjuk meg, hogy amikor a feldolgozás egy adott elemhez ér, akkor mi történjen vele. Tipikus ilyen művelet a map(), ami átalakítja egy másik értékké (pl. a már megismert szintaxissal: map(x -> 2*x) azt jelenti, hogy a szám kétszeresét veszi; persze ez csak akkor működik, ha a folyam elemei számok), vagy a filter(), ami kiszűr bizonyos elemeket (pl. a filter(x -> x % 2 != 1) kiszűri a páratlan számokat). Az egyes műveletekkel a későbbiekben ismerkedünk meg. Most viszont fontos megjegyeznünk, hogy a művelet (azaz az átalakítás vagy a szűrés) egyelőre ténylegesen nem hajtódik végre. Valamint azt is fontos tudatosítani, hogy egyszerre egy elemet látunk; nem hajthatunk végre pl. rendezést, mert nem tudunk olyat mondani, hogy vedd az ilyen és ilyen indexű elemet, hasonlítsd őket össze stb.
  • Végműveletek (terminal operations): ennek hatására hajtódnak végre a nem végműveletek. Végművelet pl. a forEach(), ami végiglépked mindegyik elemen, és végrehajt bizonyos műveletet (pl. a forEach(System.out::println) kiírja; a szintaxis megértéséhez ld. a magyarázatot fent a metódus referencia szakaszban), vagy pl. a count(), ami megszámolja, hogy hány elem található a folyamban. Egy stream-en egy végműveletet lehet csak végrehajtani. Ráadásul ha egy stram-et egyszer elindítottuk, és befejeződött, azt újraindítani nem lehet. Lehet viszont másolatot készíteni, és a másolatot elindítani, vagy tudunk készíteni saját ún. Collector-t, melyet át tudunk adni a collect() végműveletnek, és az tetszőleges műveletet végrehajthat.

A stream-ekre jellemző még az ún. folyékony API (fluent API), ami azt jelenti, hogy a műveletek kimeneti típusa is ugyanaz mint a bemeneti (jelen esetben folyam, azaz stream), és az egyes műveletek (függvényhívások) ponttal egymás után írhatóak, azaz nem kell az egyes részeredményeket ideiglenes változókba helyezni. Egy egyúttal a funkcionális programozással is egybecseng, ahol drasztikusan lecsökken a változók száma.

Példa

Lássunk egy példát!

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
 
public class StreamExample {
    public static void main(String[] args) {
        List<String> fruits = Arrays.asList("apple", "banana", "orange", "banana", "apple");
        Stream<String> fruitsStream = fruits.stream();
        fruitsStream
            .filter(s -> s.contains("n"))
            .distinct()
            .map(s -> s.toUpperCase())
            .forEach(s -> System.out.println(s));
    }
}

A példában létrehoztunk egy 5 elemű, szavakat tartalmazó listát, hagyományos Java Collections API módszerrel, majd a stream() eljárással folyamot készítettünk belőle. Első műveletünk egy szűrés: csak azok a szavak maradnak meg, melyek tartalmaznak n karaktert. Figyeljük meg, hogy ez úgy eldönthető, hogy csak egy elemet látunk.

Ebben a pillanatban mi is történik? Ha hagyományos gyűjteményről lenne szó, akkor azt mondanánk, hogy már csak 3 elemű a lista: banana, orange és banana, mert csak ezekben van n karakter, a kétszer szereplő apple szóban nincs. Viszont mivel itt folyamokról van szó, fontos megértenünk, hogy ez nem így van: valójában a folyam továbbra is 5 elemet tartalmaz, és annyit mondtunk neki, hogy majd ha valaki elindítja a folyamot, akkor szűrd ki azokat a szavakat, melyekben van n betű. Az eredmény ez a módosított folyam.

A következő művelet a distinct(). Láthatjuk, hogy magán az eredményen hajtjuk végre; megvalósíthatnánk úgy is, hogy változóba mentjük a filter() által visszaadott Stream-et, majd azon végrehajtjuk a következő műveletet, és így tovább, a stream programozásakor viszont általában a láncolt (chained) megoldást szoktuk alkalmazni. Visszatérve a distinct()-re: ez is egy nem végművelet, ami kiszűri a duplikátumokat. Itt jó kérdés, hogy ezt hogyan valósítja meg, mert továbbra is abból kell kiindulnunk, hogy egyszerre egy elemet látunk. Feltehetőleg létre hoz egy globális szótárat, melybe beleírja, hogy mely elemek vannak már benne, majd ehhez a szótárhoz hasonlít akkor, amikor egy adott elemről kell eldönteni, hogy benne maradjon-e. Így feltehetőleg mindig az elsőként megtalált elem marad benne a folyamban. De ismét igaz a fent megfogalmazott alapelv: mivel nem végműveletről van szó, a valóságban ezen a ponton még nem történik semmi.

A következő művelet a map(), ami nagybetűssé konvertálja a szót. Azt gondolom, hogy az s -> s.toUpperCase() tömör és jól érthető kódrészlet; itt látható a korábban bemutatott funkcionális szintaxis nagy előnye.

Végül végrehajtunk egy végműveletet, ami jelen példában a forEach(): ez végigiterál mindegyik elemen, és kiírja az eredményt. És ez fogja kiváltani a nem végműveletek végrehajtását is. Valahogy a következőképpen történhet mindez:

  • apple → nincs benne n, tehát töröld ki
  • banana → van benne n, tehát ne töröld ki → kerüljön be a szótárba, de maradjon meg → változtasd nagybetűssé: BANANA → írd ezt ki
  • orange → van benne n, tehát ne töröld ki → kerüljön be a szótárba, de maradjon meg → változtasd nagybetűssé: ORANGE → írd ezt ki
  • banana → van benne n, tehát ne töröld ki → már benne van a szótárban, tehát töröld ki
  • apple → nincs benne n, tehát töröld ki

Az eredmény tehát az ORANGE és a BANANA lesz, külön sorban. A fenti példában az is látható, hogy a sorrend nagyon nem mindegy. Pl. ha megcserélnénk a distinct() és a filter() függvényeket, akkor megspóroltuk volna a második banana filter() függvényét, de írhattuk volna rosszabbul is: pl. ha előbb alakítjuk nagybetűssé, akkor 3 felesleges, és valójában drága műveletet hajtottunk volna végre.

Most nézzük meg a lehetőségeket!

Folyamok létrehozásának a módjai

Folyamot többféleképpen létrehozhatunk, melyek közül néhányat megnézünk:

  • A Collection interfész tetszőleges megvalósításán a stream() eljárást meghívva, melyre fent láthattunk példát.
  • Felsorolással, pl. Stream.of("apple", "banana", "orange", "banana", "apple").
  • Tömbből, a következőképpen: Arrays.stream(array).
  • Iterálással: egy képlet alapján generálja a következőt az előzőből. Pl. a Stream.iterate(2, (Integer n) -> n * n).limit(5) a 2 négyzeteinek a négyzeteit hozza létre (2, 4, 16, 256, 65536).
  • Generálással: pl. a Stream.generate(() -> Math.random()).limit(10) létrehoz 10 véletlen lebegőpontos számot 0 és 1 között.

Érdemes megemlíteni, hogy léteznek primitív stream típusok:

  • IntStream
  • LongStream
  • DoubleStream

Az IntStream létrehozásának a módjai (a fentieken felül):

  • Szakasz létrehozásával: IntStream.range(lower, upper+1), ill. IntStream.rangeClosed(lower, upper)
  • Átalakítással: mapToInt(i -> i) ill. flatMapToInt(i -> i). Ennek egyébként az az értelme, hogy vannak műveletek, amelyek csak primitív típusokon értelmezettek, pl. sum().

A primitív típusok stream-jét a boxed() hívással tudjuk Stream-mé alakítani. Erre amiatt lehet szükség, mert egyébként nem tudnánk a megszokott collect() függvényt használni, ld. később.

Nem végműveletek

Számos nem végművelet létezik, melyek közül most bemutatok párat:

  • map(source -> destination): átalakítás, erről már volt szó.
  • flatMap(source -> stream of source): ha a folyam összetett, pl. listák listája, vagy tömbök tömbje, és mi az elemeken szeretnénk végigiterálni, akkor ezt a függvényt használhatjuk a struktúra "kivasalására". Pl. ha az arrayOfArrays egy string tömbökből álló tömb (pl. String[][] arrayOfArrays = new String[][]{{"one", "two"}, {"three", "four", "five"}, {"six"}, {"seven";}}), akkor az Arrays.stream(arrayOfArrays).flatMap(x -> Arrays.stream(x)) egy olyan folyamot hoz létre, amely az egyes elemeken halad végig (a példában "one", "two", "three", "four", "five", "six", "seven").
  • filter(predicate): szűrés; erről is volt szó.
  • distinct(): a többszörös elemeket szűri ki; erről is volt már szó.
  • limit(n): a folyam hosszát lehet vele behatárolni. Példát lejjebb látunk majd.
  • peek(consumer): egy műveletet lehet segítségével végrehajtani, pl. a peek(s -> System.out.println(s)) kiírja az elemeket. Mivel ez nem végművelet, ténylegesen csak akkor fogja kiírni, ha egy végművelet aktiválódik.
  • takeWhile(predicate) (Java 9+): adott feltétel teljesítéséig hajtja végre a műveleteket.
  • parallel(): a stream párhuzamosítása.

Végműveletek

A végműveleteket is a teljesség igénye nélkül mutatom be:

  • forEach(consumer): mindegyik elemen végrehajt egy műveletet. Erre láttunk már fent példát. Vegyük észre a különbséget a peek() és a forEach() között: az előbbi csak akkor hajtódik végre, ha egy végművelet azt kiváltja, míg ez utóbbi önmagában kiváltja azt. A peek() tehát leginkább hibakereséshez használható, pl. egy filter() művelet előtt.
  • count(): megszámolja a folyamban levő elemeket.
  • collect(): paraméterként ún. Collector-t kap, amit végrehajt. Ennek segítségével tudjuk hagyományos adatszerkezetekké alakítani a folyamot, pl. a collect(Collectors.toList()) listává alakítja.
  • toArray(): tömbbé alakítja a folyam elemeit.
  • anyMatch(predicate), allMatch(predicate) és noneMatch(predicate): igaz értékkel tér vissza, ha a paraméterül átadott feltételnek legalább egy elem, az összes elem megfelel, ill. egyik elem sem felel meg.
  • min(comparator) és max(comparator): visszaadja a folyam legkisebb ill. legnagyobb elemét.
  • findFirst() és findAny(): az első ill. egy tetszőleges elemmel tér vissza.
  • reduce((a, b) -> c): összekombinálja a folyam elemeit. Bináris operátort vár paraméterül, melynek a paraméterei és a visszatérési értéke is megfelel a folyam elemeinek típusával. Működése a követező: veszi a folyam első két elemét, végrehajtja rajtuk a műveletet, majd az eredményen és a következő elemen szintén, egészen addig, amíg el nem fogynak az elemek. Opcionálisan átadhatunk egység elemet is; ez esetben azzal és az első elemmel kezdi a műveletet. Lássunk egy példát, mely összeszorozza a folyamban található számokat!
Stream<Integer> ints = Stream.of(5, 3, 7, 8, 2);
Optional<Integer> result = ints.reduce((a, b) -> a * b);
System.out.println(result.get());

Az eredmény típusa Optional<Integer>, ami szintén Java 8 újítás. Segítségével - ahogy a nevéből következtethetünk - opcionális értékeket tudunk létrehozni. Sokkal elegánsabb, mint a null érték ellenőrzése. Néhány fontosabb eljárás:

  • get(): visszaadja a tárolt értéket; ezt már láttuk,
  • boolean isPresent(): lekérdezi, hogy van-e benne tárolt érték.
  • orElse(other): ha jelen van az érték, akkor azt adja vissza, egyébként a paraméterül átadottat.

A primitív típusok további végműveletei:

  • sum(): összeg.
  • min(), max(): minimum ill. maximum. Ennek az eredménye Optional; int-té alakítási lehetőségek: max().getAsInt(), max().orElse(default).
  • average(): átlag. Ez is opcionális, Mivel az eredmény lebegőpontos, double-lé tudjuk alakítani: average().getAsDouble().

Érdemes azt is megvizsgálni, hogy párhuzamosítás esetén ezek hogyan működnek! Néhány példa:

  • count(): mindegyik szálon külön megszámolja, majd az összegeket összegzi.
  • max() (min()): szálanként megkeresi a maximális (ill. minimális) elemet, majd összefűzéskor veszi a maximumok maximumát (minimumok minimumát).
  • average(): itt az átlagok átlaga nem jó; ehelyett mindegyik szálon számolja az összeget és a darabszámot, összefűzéskor ezeket összegzi, a végén pedig elosztja.
  • reduce(): szálanként végrehajtja a műveleteket, melyeknek lesz egy eredményük, majd ezeken ismét végrehajtja a műveletet, egészen addig, amíg egyetlen értéket nem kapunk. Gondoljuk végig: a műveletnek asszociatívnak és kommutatívnak kell lennie. Például ha van 3 értékünk: a, b és c, akkor szinte tetszőleges feldolgozási sorrendet el tudunk érni. Például 2 szál esetén az egyik szálra kerülhet az a és a c, a másodikra a b, viszont előfordulhat, hogy az első szál előbb végez, mint a második, és az első szál eredménye előbb jön, mint a második szálé. Ez esetben - összeadást feltételezve - az a+b+c műveletből ez lesz: (a+c)+b. Vagy ha a második szál végez előbb, akkor b+(a+c). Az összeadásnál persze nyilvánvaló, hogy mindegy a sorrend, de a legritkább esetben lesz a gyakorlatban a művelet ennyire nyilvánvalóan asszociatív és kommutatív. És sajnos nincs olyan mechanizmus, ami figyelmeztetne arr, hogy valamelyik szempont nem teljesül; erről a programozónak kell gondoskodnia.

Példák

A szükséges importok:

import java.util.*;
import java.util.stream.*;
import java.util.function.*;

Alap stream példák

Nagybetűsítés

List<String> capitalize(List<String> items) {
    return items.stream()
            .map(s -> s.toUpperCase())
            .collect(Collectors.toList());
}

Szűrés

Csak az a betűt tartalmazó stringeket adja vissza:

List<String> filterA(List<String> items) {
    return items.stream()
            .filter(s -> s.contains("a"))
            .collect(Collectors.toList());
}

Szűrés és nagybetűsítés egyszerre

Elébb érdemes szűrni, és utána átalakítani:

List<String> filterAndCapitalize(List<String> items) {
    return items.stream()
            .filter(s -> s.contains("a"))
            .map(s -> s.toUpperCase())
            .collect(Collectors.toList());
}

Részeredmény kiírása

Minden lépésben kiírjuk a részeredményt:

List<String> printFilterAndCapitalize(List<String> items) {
    return items.stream()
            .peek(s -> System.out.println(s))
            .filter(s -> s.contains("a"))
            .peek(s -> System.out.println(s))
            .map(s -> s.toUpperCase())
            .peek(s -> System.out.println(s))
            .collect(Collectors.toList());
}

Azonnali kiírás

A következő példa nem visszaadja az eredményt, hanem rögtön kiírja:

void filterAndCapitalizeAndPrint(List<String> items) {
    items.stream()
            .filter(s -> s.contains("a"))
            .map(s -> s.toUpperCase())
            .forEach(System.out::println);
}

Stringgé alakítás

String filterAndCapitalizeAndFormat(List<String> items) {
    return items.stream()
            .filter(s -> s.contains("a"))
            .map(s -> s.toUpperCase())
            .collect(Collectors.joining(", ", "[", "]"));
}

Szószámláló

A groupingBy() collectort ld. lejjebb.

Map<String, Long> countWords(List<String> items) {
    return items.stream()
            .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
}

IntStream

Számok összege 1-től 5-ig

IntStream.range(1, 6)
        .reduce((a, b) -> a + b)
        .getAsInt()

Az eredmény Optional, mert lehet, hogy üres a stream. Emiatt kell a getAsInt(). Kiküszöbölése egységelem megadásával:

IntStream.range(1, 6).reduce(0, (a, b) -> a + b)

Az összeg a sum() függvény használatával:

IntStream.range(1, 6).sum()

Zárt határok (itt nem 6-ot, hanem 5-öt kell írni):

IntStream.rangeClosed(1, 5).sum()

Átlagszámítás

IntStream.range(1, 6)
        .average()
        .getAsDouble()

Az első 8 kettő hatvány listává alakítva

IntStream
        .iterate(1, i -> 2*i)
        .limit(8)
        .boxed()
        .collect(Collectors.toList())

A legnagyobb olyan szám, melynek nlgyzete kisebb mint 100

IntStream.iterate(0, i -> i + 1)
        .takeWhile(i -> i * i < 100)
        .max()
        .getAsInt()

Rendezés

IntStream.of(5, 3, 8, 9, 2)
        .sorted()
        .toArray()

Megjegyzés: az eredményt az Arrays.toString(…) metódussal lehet egy sorban stringgé alakítani.

Stringként tárolt számok rendezése oda-vissza konverzióval

Stream.of("5", "3", "8", "9", "2")
        .mapToInt(s -> Integer.parseInt(s))
        .sorted()
        .mapToObj(i -> String.valueOf(i))
        .collect(Collectors.toList())

Grouping

Az egyik legnagyobb tudású collector típus. Részletesebb leírás: https://www.baeldung.com/java-groupingby-collector.

A példákhoz hozzuk létre az alábbi osztályt:

class Fruit {
    private String name;
    private int pieces;
 
    public Fruit(String name, int pieces) {
        this.name = name;
        this.pieces = pieces;
    }
 
    public String getName() {
        return name;
    }
 
    public int getPieces() {
        return pieces;
    }
 
    @Override
    public String toString() {
        return "Fruit{" + "name='" + name + '\'' + ", pieces=" + pieces + '}';
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Fruit fruit = (Fruit) o;
        if (pieces != fruit.pieces) return false;
        return name != null ? name.equals(fruit.name) : fruit.name == null;
    }
 
    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + pieces;
        return result;
    }
}

Ez mindössze két adatmezőt tartalmaz, mégis, 30 sor hosszú. Csak érdekességképpen: a fentivel nagyjából megegyező Scala kód az alábbi:

case class Fruit(name: String, pieces: int)

Hozzunk létre néhány példányt:

List<Fruit> fruits = Arrays.asList(
    new Fruit("apple", 2),
    new Fruit("banana", 4),
    new Fruit("apple", 6),
    new Fruit("banana", 1),
    new Fruit("banana", 4)
);

Csoportosítás név alapján

Map<String, List<Fruit>> groupByName = fruits.stream().collect(Collectors.groupingBy(Fruit::getName));

Eredmény:

{
    banana=[
        Fruit{name='banana', pieces=4},
        Fruit{name='banana', pieces=1},
        Fruit{name='banana', pieces=4}
    ],
    apple=[
        Fruit{name='apple', pieces=2},
        Fruit{name='apple', pieces=6}
    ]
}

Az eredmény átalakítása halmazzá

Ehhez a groupingBy() második paraméterét kell használnunk:

Map<String, Set<Fruit>> groupByNameSet = fruits.stream().collect(Collectors.groupingBy(Fruit::getName, Collectors.toSet()));

Eredmény:

{
    banana=[
        Fruit{name='banana', pieces=1},
        Fruit{name='banana', pieces=4}
    ],
    apple=[
        Fruit{name='apple', pieces=2},
        Fruit{name='apple', pieces=6}
    ]
}

Tehát kiszedte a duplikátumot.

Számolás

Az alábbi példa azt számolja meg, hogy milyen nevű elem hányszor fordul elő a listában:

Map<String, Long> countedFruits = fruits.stream().collect(Collectors.groupingBy(Fruit::getName, Collectors.counting()));

Eredmény:

{
    banana=3,
    apple=2
}

Összegzés

A következő összeadja a gyümölcsök melletti darabszámokat:

Map<String, Integer> summedFruits = fruits.stream().collect(Collectors.groupingBy(Fruit::getName, Collectors.summingInt(Fruit::getPieces)));

Eredmény:

{
    banana=9,
    apple=8
}

Átlagolás

A következő a számok átlagát számolja ki:

Map<String, Double> averagedFruits = fruits.stream().collect(Collectors.groupingBy(Fruit::getName, Collectors.averagingInt(Fruit::getPieces)));

Eredmény:

{
    banana=3.0,
    apple=4.0
}

Statisztika számítás

Számolás, összegzés, minimum elem, átlag, maximum elem számítás egyszerre:

Map<String, IntSummaryStatistics> fruitsIntStatistics = fruits.stream().collect(Collectors.groupingBy(Fruit::getName, Collectors.summarizingInt(Fruit::getPieces)));

Eredmény:

{
    banana=IntSummaryStatistics{count=3, sum=9, min=1, average=3,000000, max=4},
    apple=IntSummaryStatistics{count=2, sum=8, min=2, average=4,000000, max=6}
}

Három paraméteres groupingBy

Ez esetben:

  • az első paraméter megegyezik az egy paraméteres paraméterével (ami a két paraméteres változat első paramétere is egyben),
  • a második paraméter a Map példányosítása,
  • a harmadik paraméter a két paraméteres változat második paraméterével egyetik meg.
Map<String, Integer> summedFruits = fruits.stream().collect(Collectors.groupingBy(Fruit::getName, Collectors.summingInt(Fruit::getPieces)));
Map<String, Integer> summedFruitsMapType = fruits.stream().collect(Collectors.groupingBy(Fruit::getName, () -> new Hashtable<>(), Collectors.summingInt(Fruit::getPieces)));
Map<String, Integer> summedFruitsHashtable = fruits.stream().collect(Collectors.groupingBy(Fruit::getName, () -> new Hashtable<>(), Collectors.summingInt(Fruit::getPieces)));
System.out.println(summedFruits);
System.out.println(summedFruitsHashtable);
System.out.println(summedFruits.getClass().getSimpleName());
System.out.println(summedFruitsHashtable.getClass().getSimpleName());

Eredmény:

{banana=9, apple=8}
{banana=9, apple=8}
HashMap
Hashtable

Saját Collector

Amint arról már szó volt, egy folyamban összesen egy végművelet lehet. Most tegyük fel, hogy milliárdnyi elemet tartalmaz a folyam; a lényeg, hogy nem lehetséges (vagy legalábbis nem gazdaságos) memóriában tárolni, valamint hosszú ideig tart feldolgozni. Tegyük fel, hogy a minimum és maximum elemet szeretnénk meghatározni egyszerre. Külön-külön ezt lehet beépített eljárással, de ezt alapból csak úgy tudjuk megtenni, hogy készítünk másolatot a folyamról, és így kétszer végigfutunk rajta. (Ugyanazt a folyamot kétszer nem lehet elindítani.) Másik, sokkal hatékonyabb megoldás az, ha saját Collector megvalósítást hozunk létre, és azt átadjuk a collect() eljárásnak.

Saját Collector írása nem egyszerű; álljon itt példaként a fenti probléma általános megvalósítása:

import java.util.Comparator;
import java.util.stream.Collector;
import java.util.stream.Stream;
 
class Stats<T> {
    private int count;
    private Comparator<? super T> comparator;
    private T min;
    private T max;
 
    public Stats(Comparator<? super T> comparator) {
        this.comparator = comparator;
    }
 
    public int count() {return count;}
    public T min() {return min;}
    public T max() {return max;}
 
    public void accept(T val) {
        if (count==0) min = max = val;
        else if (comparator.compare(val, min) < 0) min = val;
        else if (comparator.compare(val, max) > 0) max = val;
        count++;
    }
 
    public Stats<T> combine(Stats<T> that) {
        if (this.count == 0) return that;
        if (that.count == 0) return this;
 
        this.count += that.count;
        if (comparator.compare(that.min, this.min) < 0) this.min = that.min;
        if (comparator.compare(that.max, this.max) > 0) this.max = that.max;
 
        return this;
    }
 
    public static <T> Collector<T, Stats<T>, Stats<T>> collector(Comparator<? super T> comparator) {
        return Collector.of(
            () -> new Stats<>(comparator),
            Stats::accept,
            Stats::combine,
            Collector.Characteristics.UNORDERED,
            Collector.Characteristics.IDENTITY_FINISH
        );
    }
 
    public static <T extends Comparable<? super T>> Collector<T, Stats<T>, Stats<T>> collector() {
        return collector(Comparator.naturalOrder());
    }
}
 
public class CustomCollectorExample {
    public static void main(String[] args) {
        Stream<Integer> numberStream = Stream.of(5, 3, 8, 4, 7, 5);
        Stats<Integer> result = numberStream.collect(Stats.collector());
        System.out.println(result.min() + ", " + result.max());
    }
}

A Java 8 hatása a Java Collection API-ra

A Java 8-ban a funkcionális programozási elemek bevezetése és a folyamok bevezetése mellett a Java Collection API is kapott egy komolyabb ráncfelvarrást, melyből megnézünk néhány példát.

  • forEach(): végigiterál az elemeket, és végrehajt egy műveletet.
  • removeIf(): ha adott feltétel teljesül, akkor (helyben) törli az elemet.
  • replacAll(): az összes elemet kicseréli
  • A map-ben getOrDefault(): ha a map tartalmazza a kulcsot, akkor a hozzá tartozó értéket adja vissza, egyébként egy alapértelmezett értéket. Ez az utasítás egy gyakran előforduló több soros kódrészletet vált ki.
  • A map-ben putIfAbsent(): csak akkor adja hozzá a kulcs-érték párt, ha még nem volt benne.

Lássunk ezekre is egy példát!

import java.util.*;
 
public class Java8CollectionAPI {
    public static void main(String[] args) {
        List<String> fruits = new ArrayList<String>(Arrays.asList("apple", "banana", "peach", "banana"));
        fruits.removeIf(f -> f.startsWith("ban"));
        fruits.replaceAll(f -> f.toUpperCase());
        fruits.forEach(f -> System.out.println(f));
    }
}

Megjegyzés: az inicializálásnál nem elég csak az Arrays.asList(), mert az egy olyan szerkezetet eredményez, mely nem támogatja a helyben törlést.

Összegzés

A Java 8-ban komoly előrelépés történt a folyamok világába. Azzal, hogy a Java Collection API-ba is bevezették az új elemeket, kifejezetten jól olvasható és tömör kódot tudunk készíteni. Ugyanakkor komoly sajnos hiányosságok is maradtak. A legfontosabb talán az, hogy a folyamok természetszerűleg végtelenek: folyamatosan jönnek az elemek, melyeket fel kell dolgozni, és például nem definiálható az a fogalom, hogy utolsó elem. Márpedig a Java stream-ek esetén ez a fogalom nélkülözhetetlen. Ami leginkább hiányzik, az talán a kötegelt feldolgozás: pl. százasával vehetné az elemeket, és azon belül értelmezné pl. az utolsó elemet, de amúgy futhatna a végtelenségig. Ezen kívül nincs természetes integráció a gyakori folyam megvalósításokkal, pl. Apache Kafka vagy Event Hubs.

A funkcionális programozáshoz hasonlóan ez esetben is elmondható tehát, hogy komoly előrelépéseket tettek, de még van hová fejlődni.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License