Kihagyás

Java Functional: Stream, Collectors, Gatherer

1. Pipeline parts

List<String> result = names.stream()        // 1. source
    .filter(n -> n.length() > 3)            // 2. intermediate operation
    .map(String::toUpperCase)
    .collect(Collectors.toList());          // 3. terminal operation
Part Szerep Examples
Source létrehozza a streamet, nem tárol adatot list.stream(), Stream.of, iterate, generate, Files.lines, IntStream.range
Intermediate operation új Stream-et ad, lazy, láncolható filter, map, flatMap, mapMulti, sorted, distinct, limit, takeWhile, gather
Terminal operation eredményt ad vagy side effect-et, elindítja a végrehajtást collect, toList, reduce, count, findFirst, anyMatch, forEach

Egy mondatban: a source szolgáltat, az intermediate operation-ök leírnak, a terminal operation végrehajt.


2. Execution model

var s = Stream.of("a", "bb", "ccc", "dd")
    .filter(x -> { System.out.println("filter " + x); return x.length() > 1; })
    .map(x -> { System.out.println("map " + x); return x.toUpperCase(); });
System.out.println("pipeline built");
System.out.println(s.findFirst());
pipeline built
filter a
filter bb
map bb
Optional[BB]
  • Lazy: terminal operation nélkül semmi nem fut (a peek sem). Ezért íródik ki először a pipeline built.
  • Vertical processing: egy elem végigmegy a teljes láncon, utána jön a következő. Nem lépésenként dolgozza fel az egész kollekciót.
  • Short-circuit: a findFirst az első találatnál leáll, a ccc és a dd el sem jut a filter-ig.
  • Barrier (sorted): ha filter és map közé teszel egy .sorted()-et, a filter minden elemre lefut, és csak utána indul a map, mert a sorted addig nem adhat tovább semmit, amíg mindet meg nem kapta.
  • Single use: bármilyen művelet meghívása után a stream consumed. Újabb hívásra IllegalStateException. Újrafelhasználáshoz Supplier<Stream<T>> vagy új stream kell.

Side effects

  • A peek bizonyos esetekben nem fut le:
    List.of(1, 2, 3).stream().peek(x -> System.out.println("peek " + x)).count();
    // a peek nem fut le semmire
    
    List.of(1, 2, 3).stream().filter(x -> true).peek(x -> System.out.println("peek " + x)).count();
    // itt már lefut
    
  • A count() kihagyhatja az intermediate operation-öket, ha a méret a source-ból közvetlenül kiolvasható (SIZED). List.of(1,2,3).stream().peek(...).count() semmit nem ír ki.
  • A lambdákban lévő side effect-re nem szabad építeni.

Operation properties

Ezek nem külön részek, hanem a műveletek jellemzői. Egy művelet több kategóriába is tartozhat.

Property Jelentés Examples
Stateless elemenként, emlékezet nélkül dolgozik filter, map, flatMap, mapMulti, peek
Stateful állapotot tart sorted, distinct, limit, skip, takeWhile, dropWhile
Short-circuit idő előtt leállíthatja a bejárást limit, takeWhile, findFirst, anyMatch, allMatch, noneMatch
Barrier mindent begyűjt, mielőtt továbbad sorted

Szabály: minden barrier stateful, de nem minden stateful barrier. A distinct szekvenciális streamen stateful (egy HashSet-ben tartja a látott elemeket), de az újakat azonnal továbbadja, tehát nem barrier.

Stream flag-ek (ORDERED, SIZED, SORTED, DISTINCT): ezek alapján optimalizál a pipeline, elég tudni, hogy léteznek. A parallel() / sequential() az egész pipeline-ra hat, az utolsó hívás nyer.

Infinite stream pitfalls

Stream.iterate(1, n -> n + 1).sorted().limit(10).toList();        // soha nem ér véget (OOM)
Stream.iterate(1, n -> n + 1).limit(10).sorted().toList();        // helyes
Stream.iterate(1, n -> n + 1).filter(n -> n < 0).findFirst();     // örökké fut, mert sosem talál
  • A sorted barrier, ezért végtelen source-on sosem ad tovább elemet, a puffere pedig addig nő, amíg OutOfMemoryError nem lesz.
  • A short-circuit azt jelenti, hogy véget érhet, nem azt, hogy mindig véget ér.

TLDR: "A pipeline lazy: az intermediate operation-ök csak leírják a lépéseket, a terminal operation indítja a végrehajtást. Az elemek egyesével, függőlegesen haladnak végig a láncon, a short-circuit műveletek idő előtt leállíthatják a bejárást. A sorted barrier, mert mindent be kell gyűjtenie, mielőtt továbbad. A stream egyszer használható, és nem tárol adatot."


3. Sources: of vs iterate vs generate

Stream.of("EUR", "USD", "HUF").toList();                  // [EUR, USD, HUF]
Stream.iterate(1, n -> n * 2).limit(5).toList();          // [1, 2, 4, 8, 16]
Stream.generate(UUID::randomUUID).limit(3).toList();      // 3 db véletlen UUID
of iterate generate
Elem forrása felsorolt értékek f(előző elem) supplier.get()
Véges? igen végtelen (3 argumentumos: véges) végtelen
Ordered? igen igen nem
Függ az előzőtől? nem releváns igen nem
Használat fix lista sorozat, léptetés véletlen, UUID, időbélyeg

iterate két változata: - iterate(seed, f): végtelen, mindig kell mellé limit vagy short-circuit. - iterate(seed, hasNext, next): a for (x = seed; hasNext(x); x = next(x)) ciklus megfelelője, magától véget ér.

Ha a lépéshez több előző érték kell, az elem hordozza az állapotot (tömb vagy record), nem külső változó:

Stream.iterate(new long[]{0, 1}, f -> new long[]{f[1], f[0] + f[1]})
      .limit(10).map(f -> f[0]).toList();     // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

A stream itt [a, b] párokat tartalmaz. Minden lépés [a, b] → [b, a+b], a map pedig az első számot veszi ki. Külső változó nem megy: a lambdában effectively final kell. AtomicLong-gal megkerülhető, de rejtett mutable state lesz belőle, ami párhuzamosan hibás.

generate stateful supplier-rel (például AtomicInteger) szekvenciálisan működik, párhuzamosan hibás, ezért sorozatra az iterate való.

Két apróság: - Stream.of(array): objektumtömbnél kibontja az elemeket, primitív tömbnél (int[]) egyetlen elemű Stream<int[]> lesz. Oda Arrays.stream(arr) vagy IntStream.of(arr) kell. - Stream.ofNullable(x): nulla vagy egy elemű stream.


4. takeWhile vs dropWhile vs filter

var data = List.of(1, 2, 3, 10, 1, 2);
data.stream().takeWhile(n -> n < 5).toList();   // [1, 2, 3]
data.stream().dropWhile(n -> n < 5).toList();   // [10, 1, 2]
data.stream().filter(n -> n < 5).toList();      // [1, 2, 3, 1, 2]
Mit csinál Type
takeWhile(p) az elejétől addig adja az elemeket, amíg p igaz, az első false-nál leáll stateful, short-circuit
dropWhile(p) az elejétől eldobja az elemeket, amíg p igaz, utána mindent átenged stateful
filter(p) minden elemre külön dönt stateless
  • A takeWhile és a dropWhile prefixet vágnak, a filter elemenként dönt.
  • Végtelen streamen a takeWhile véget ér, a filter nem: Stream.iterate(1, n -> n + 1).takeWhile(n -> n < 5) → [1, 2, 3, 4].
  • Csapda: rendezetlen adaton hibátlanul lefutnak, de csendben rossz eredményt adnak. Csak rendezett adaton vagy monoton predikátummal helyesek.
  • Unordered vagy párhuzamos streamen az eredmény nem determinisztikus, ezért szekvenciálison használd.

5. mapMulti vs flatMap

record Order(String id, List<String> items) {}
var orders = List.of(new Order("o1", List.of("pen", "ink")),
                     new Order("o2", List.of()),
                     new Order("o3", List.of("pad")));

orders.stream().flatMap(o -> o.items().stream().map(i -> o.id() + ":" + i)).toList();

orders.stream().<String>mapMulti((o, sink) -> {
    for (var i : o.items()) sink.accept(o.id() + ":" + i);
}).toList();
// mindkettő: [o1:pen, o1:ink, o3:pad]
  • flatMap(Function<T, Stream<R>>): minden elemhez visszaadsz egy új Stream-et, a pipeline kilapítja. Elemenként stream-allokáció.
  • mapMulti(BiConsumer<T, Consumer<R>>): nem adsz vissza semmit, az eredményeket a sink-be tolod (sink.accept(...)). Nincs köztes stream.
  • Alapértelmezés a flatMap (olvashatóbb, deklaratív). A mapMulti akkor jobb, ha elemenként kevés (0–2) eredmény keletkezik, vagy ha az imperatív logika egyszerűbb.
  • Type witness (.<String>): a sink.accept(...) nem árulja el a fordítónak az R típust, ezért Object lesz. Láncolásnál (.toList()) a cél típusa nem terjed vissza, ezért a változó típusa sem pótolja a witness-t.
  • A sink-et csak a lambdán belül használd, ne tárold el.

Szűrés + típuskonverzió egy lépésben (pattern matchinggel):

List<Object> mixed = List.of(1, "a", 2, "b", 3);
mixed.stream().<Integer>mapMulti((o, sink) -> {
    if (o instanceof Integer i) sink.accept(i * 10);
}).toList();                                       // [10, 20, 30]

Régi minta, ma már nem: .flatMap(x -> cond(x) ? Stream.of(f(x)) : Stream.empty()). Ez elemenként streamet allokál, helyette filter + map vagy mapMulti.

Primitív változatok: mapMultiToInt, mapMultiToLong, mapMultiToDouble.


6. Collectors

A collect(Collector) a terminal operation, amely a stream elemeit egy mutable eredménybe gyűjti. A Collectors kész Collectorokat ad, sajátot is lehet írni (lásd 7. pont).

Közös adat a példákhoz:

record Emp(String dept, String name, int salary) {}
var emps = List.of(
    new Emp("DEV", "Anna", 900), new Emp("DEV", "Bela", 700),
    new Emp("OPS", "Cili", 600), new Emp("OPS", "Dani", 800),
    new Emp("HR",  "Eva",  500));

groupingBy(classifier, downstream)

Kulcs szerint csoportosít, és csoportonként lefuttat egy másik Collectort (downstream). Downstream nélkül toList() az alapértelmezés. A HashMap sorrendje nem garantált. Rendezett kulcshoz: groupingBy(Emp::dept, TreeMap::new, counting()).

groupingBy(Emp::dept)                                // Map<String, List<Emp>>
groupingBy(Emp::dept, counting())                    // {DEV=2, OPS=2, HR=1}   (Long!)
groupingBy(Emp::dept, summingInt(Emp::salary))       // {DEV=1600, OPS=1400, HR=500}
groupingBy(Emp::dept, averagingInt(Emp::salary))     // {DEV=800.0, OPS=700.0, HR=500.0}
groupingBy(Emp::dept, mapping(Emp::name, toList()))  // {DEV=[Anna, Bela], OPS=[Cili, Dani], HR=[Eva]}

Downstream-ek: counting, summingInt/Long/Double, averagingInt/..., summarizingInt/... (count, sum, min, max, average egyben), mapping, filtering, flatMapping, maxBy / minBy (Optional), collectingAndThen, reducing.

filtering vs stream filter (interjús fogós kérdés)

groupingBy(Emp::dept, filtering(e -> e.salary() > 650, counting()))   // {DEV=2, OPS=1, HR=0}
Ha a filter a csoportosítás előtt fut, a HR kulcs eltűnik. A filtering a csoportosítás után szűr, ezért az üres csoport 0 értékkel megmarad. Riportnál, ahol minden csoport kell, ez számít.

toMap: ütköző kulcsnál kivételt dob

toMap(Emp::dept, Emp::salary)                 // IllegalStateException: Duplicate key DEV
toMap(Emp::dept, Emp::salary, Integer::sum)   // {DEV=1600, OPS=1400, HR=500}
- Merge function nélkül éles hiba lehet belőle. - null érték NullPointerException-t okoz. - A negyedik argumentum a map factory (LinkedHashMap::new, TreeMap::new). - Ha egy kulcshoz több érték tartozhat, groupingBy kell.

teeing: két Collector egy menetben

double avg = emps.stream().collect(teeing(
    summingInt(Emp::salary), counting(), (sum, cnt) -> sum / (double) cnt));   // 3500 / 5 = 700.0
A stream egyszer használható, ezért két aggregátumhoz (összeg és darabszám, min és max) régen két bejárás kellett. A teeing egy menetben, side effect nélkül oldja meg.

Még létezik: partitioningBy (boolean kulcs, mindig mindkét kulcs benne van), joining.

TLDR: "A collect a terminal operation, a Collectors kész gyűjtőket ad. A groupingBy kulcs szerint csoportosít, és csoportonként downstream Collectort futtat. A filtering megtartja az üres csoportot, a filter nem. A toMap ütköző kulcsnál kivételt dob merge function nélkül. A teeing két Collectort futtat egy menetben, és kombinálja az eredményüket."


7. Custom Collector

Elmélet: a Collector<T, A, R> három típusa: T az input element, A a mutable accumulator, R a result. Négy függvényből áll:

Function Szerep
supplier létrehozza az üres accumulatort
accumulator egy elemet beletesz az accumulatorba
combiner két részeredményt összevon
finisher az accumulatort végeredménnyé alakítja

A combiner csak párhuzamos streamnél hívódik: ott minden szál a saját accumulatorába gyűjt, a végén ezeket vonja össze. Szekvenciális streamen sosem fut le, de akkor is helyesen kell megírni.

1. Saját toList(), a legegyszerűbb eset

Collector<String, List<String>, List<String>> myToList = Collector.of(
    ArrayList::new,                          // supplier: üres lista
    List::add,                               // accumulator: elem hozzáadása
    (a, b) -> { a.addAll(b); return a; });   // combiner: két lista összevonása

Stream.of("a", "b", "c").collect(myToList);  // [a, b, c]

A típusok: T = String (egy elem), A = List<String> (az accumulator), R = List<String> (a result). Itt nincs finisher, mert az accumulator maga az eredmény, ezért elég a háromargumentumos Collector.of.

2. Stringek összefűzése, finisherrel

Collector<String, StringBuilder, String> concat = Collector.of(
    StringBuilder::new,        // supplier: üres StringBuilder
    StringBuilder::append,     // accumulator: elem hozzáfűzése
    StringBuilder::append,     // combiner: két StringBuilder egyesítése
    StringBuilder::toString);  // finisher: StringBuilder → String

Stream.of("a", "b", "c").collect(concat);    // "abc"

Típusok: T = String, A = StringBuilder, R = String. Az A és az R itt különbözik, ezért kell a finisher.

Mi történik párhuzamosan: a stream két részre oszlik, és mindkét szál saját StringBuilder-t kap (két supplier hívás): - Szál 1: "a", "b" → "ab" - Szál 2: "c" → "c" - combiner: "ab" + "c" → "abc", majd a finisher fut egyszer.

A combiner ordered streamen az elemek eredeti sorrendjében vonja össze a részeket, ezért az eredmény ugyanaz marad.

Miért nem reduce:

Stream.of("a", "b", "c").reduce("", String::concat);
Ez ugyanazt az eredményt adja, de minden lépésben új String objektumot hoz létre (az immutable reduction ára). A collect egyetlen mutable konténerbe gyűjt, ezért hatékonyabb és párhuzamosan is helyes. Éles kódban egyébként erre Collectors.joining() való.

3. Példa: átlagfizetés

Collector<Emp, long[], Double> avgSalary = Collector.of(
    () -> new long[2],                                     // supplier: [sum, count]
    (acc, e) -> { acc[0] += e.salary(); acc[1]++; },       // accumulator
    (a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; },   // combiner
    acc -> acc[1] == 0 ? 0.0 : (double) acc[0] / acc[1]    // finisher
);

emps.stream().collect(avgSalary);    // 700.0  (3500 / 5)

Az Emp lista ugyanaz, mint a Collectors résznél. Az accumulator itt egy long[2], mert a lambdák nem módosíthatnak lokális változót, és az átlaghoz két szám kell. Éles kódban ez gyakran egy kis privát osztály. Maga a példa azért van, mert a négy függvényt jól mutatja. Átlagra egyébként a beépített averagingInt való.

Characteristics (opcionális)

A Collector.of(...) végén varargs-ként adhatók meg: - IDENTITY_FINISH: a finisher identitás, kihagyható. Ekkor a háromargumentumos Collector.of(supplier, accumulator, combiner) is jó. - UNORDERED: az eredmény nem függ az elemek sorrendjétől. - CONCURRENT: az accumulator thread-safe, ezért több szál ugyanabba a konténerbe gyűjthet, a combiner nem kell. Így működik a groupingByConcurrent is (ConcurrentHashMap). Csak akkor használható helyesen, ha az accumulator tényleg thread-safe, és az UNORDERED jelleg is fennáll.

reduce vs collect

Gyakori interjúkérdés: - reduce: immutable reduction, az értékeket kombinálja (összeg, maximum). Ha ezzel építesz listát vagy stringet, minden lépésben új objektum jön létre, és párhuzamosan hibás vagy lassú. - collect: mutable reduction, egy konténerbe gyűjt (List, StringBuilder, Map). Ezért kell neki supplier és combiner.

Mikor kell saját Collector

Ritkán. Előbb próbáld a beépítettekből összerakni: - collectingAndThen: egy Collector eredményének utófeldolgozása - teeing: két Collector kombinálása - groupingBy downstreammel

Saját akkor kell, ha a végeredmény egy saját típus, vagy az akkumulálás logikája nem fejezhető ki a beépítettekkel.

Döntési szabály: stateful intermediate operation → Gatherer, aggregálás a végén → Collector.

Még létezik: Collector.of mellett a Collector interfészt közvetlenül is lehet implementálni, de gyakorlatban nem szokás.

TLDR: "A Collector négy függvényből áll: supplier (üres accumulator), accumulator (elem hozzáadása), combiner (részeredmények összevonása párhuzamosnál) és finisher (végeredmény). A combiner csak párhuzamos streamen hívódik. Characteristics-szel jelzem, ha az accumulator thread-safe (CONCURRENT), a sorrend nem számít (UNORDERED), vagy nincs finisher (IDENTITY_FINISH). A reduce immutable reduction-re való, a collect mutable konténerbe gyűjt. Saját Collectort ritkán írok, előbb teeing-gel és collectingAndThen-nel próbálom összerakni a beépítettekből."


8. Gatherer

Elmélet: a Stream API-ban korábban csak a terminal operation-t lehetett saját kóddal bővíteni (Collector), intermediate operation-t nem. A Stream.gather(Gatherer) ezt nyitja meg: saját, stateful intermediate operation.

A Gatherer négy része:

Part Szerep
initializer létrehozza az állapotot
integrator feldolgoz egy elemet, az eredményt a downstream.push(...)-sal adja tovább, false visszatéréssel leállít (short-circuit)
combiner párhuzamos futáshoz, ha nincs, az a szakasz szekvenciálisan fut
finisher a maradék állapot kiürítése a végén

Ha az integrator nem short-circuitel, Integrator.ofGreedy(...) jelzi ezt, így a stream optimalizálhat.

Built-in Gatherers (Gatherers)

Stream.of(1, 2, 3, 4).gather(Gatherers.scan(() -> 0, Integer::sum)).toList();
// [1, 3, 6, 10]          futó összeg, minden lépés eredményét kiadja

Stream.of(1, 2, 3, 4, 5, 6, 7).gather(Gatherers.windowFixed(3)).toList();
// [[1, 2, 3], [4, 5, 6], [7]]       egymást nem átfedő ablakok, az utolsó lehet rövidebb

Stream.of(1, 2, 3, 4, 5).gather(Gatherers.windowSliding(3)).toList();
// [[1, 2, 3], [2, 3, 4], [3, 4, 5]]  csúszóablak, mozgóátlaghoz jó

ids.stream().gather(Gatherers.mapConcurrent(10, id -> client.fetch(id))).toList();
// legfeljebb 10 párhuzamos hívás virtual threaden, a sorrend megmarad
  • scan az, ami régen indexelős ciklus vagy trükkös Collector volt (futó összeg, futó egyenleg).
  • mapConcurrent a mai válasz a blokkoló, I/O jellegű párhuzamosításra. Régen erre parallelStream()-et használtak, ami a közös ForkJoinPool-t blokkolta.
  • Még létezik: fold (sok elemből egy eredmény, intermediate operation-ként) és andThen (Gatherer-ek összefűzése).

andThen példa

A Gatherer.andThen(másik) két Gatherert egyetlen Gatherer-ré fűz össze: az első kimenete a második bemenete.

Gatherer<Integer, ?, Integer> runningCount =
    Gatherers.<Integer>windowFixed(3)
             .andThen(Gatherers.<List<Integer>, Integer>scan(() -> 0, (acc, w) -> acc + w.size()));

Stream.of(1, 2, 3, 4, 5, 6, 7).gather(runningCount).toList();   // [3, 6, 7]

Először hármas ablakokat képez ([1,2,3], [4,5,6], [7]), utána a scan futóan összeadja az ablakok méretét: 3, 6, 7. A példa csak az összefűzést mutatja. A típusparaméterek itt azért kellenek, mert a fordító a láncolt generikusokat gyakran nem találja ki.

A .gather(a).gather(b) ugyanazt az eredményt adja. Az andThen akkor kell, ha az összetett lépést egy névvel akarod újrahasznosítani (például egy metódus visszaadja a kész Gatherert), és nem akarod mindenhol kétszer leírni.

Saját Gatherer váza

Gatherer.ofSequential(
    () -> new int[]{0},                                            // initializer: állapot
    Gatherer.Integrator.ofGreedy((state, element, downstream) ->   // integrator
        downstream.push(new Indexed<>(state[0]++, element))))      // elem + sorszám tovább

A váza ennyi: állapot az initializer-ben, elemenként az integrator dönt, mit ad tovább. A finisher akkor kell, ha a végén maradt állapotot is ki kell adni (például egy félig megtelt ablakot). A típusinferencia néha explicit típusparamétert kér (Gatherer.<T, int[], Indexed<T>>ofSequential(...)). Ezt a feladatnál úgyis ki kell próbálnod.

Döntési szabály

Stateful intermediate operation → Gatherer, aggregálás a végén → Collector.

Ami 2–3 éve még szokásos volt, ma már nem

  • "Saját intermediate operation-t nem lehet írni" → ma Gatherer.
  • Csúszóablak vagy futó összeg indexeléssel → windowSliding / scan.
  • parallelStream() blokkoló hívásokra → mapConcurrent.

TLDR: "A Gatherer a Collector intermediate megfelelője: saját, stateful intermediate operation-t lehet vele írni. Négy része van: initializer, integrator, combiner és finisher, az integrator a downstream-nek tolja az eredményt, és leállíthatja a streamet. Beépítettek: scan, windowFixed, windowSliding, mapConcurrent. Az utolsó virtual threaden futtat korlátozott számú párhuzamos hívást, és megőrzi a sorrendet."


9. Parallel stream pitfalls

Elmélet: a parallel() a közös ForkJoinPool.commonPool-ra dolgoztat, amely az egész JVM-ben osztozik (alapból magok száma mínusz egy szál). Csak akkor éri meg, ha mind igaz: - CPU-bound a művelet (nem I/O), - sok az adat, - a source olcsón darabolható (ArrayList, tömb, IntStream.range; LinkedList és Stream.iterate nem), - a lépések stateless-ek és associative-ok.

A klasszikus hiba: shared mutable state.

var list = new ArrayList<Integer>();
IntStream.range(0, 10_000).parallel().forEach(list::add);   // hiányzó elemek vagy kivétel

A helyes változat: IntStream.range(0, 10_000).parallel().boxed().toList(), vagyis a stream gyűjt, nem te.

További csapdák: - Blokkoló I/O a közös poolban az egész JVM többi parallel streamjét is lefékezi. - A reduce identity-jének tényleg identitásnak kell lennie (összegnél 0). Ha nem az, az érték szálanként bekerül az eredménybe. - forEach párhuzamosan nem tartja a sorrendet, ahhoz forEachOrdered kell. - Mindig mérj (JMH), a párhuzamos változat sokszor lassabb.

Ma a helyes válasz: blokkoló hívásokra Gatherers.mapConcurrent vagy virtual thread, a parallelStream() csak CPU-bound munkára.

TLDR: "A parallel stream a közös ForkJoinPool-t használja, ezért csak CPU-bound munkára, nagy adaton, jól darabolható source-szal és stateless műveletekkel éri meg. Shared mutable state és blokkoló I/O a tipikus hiba. Blokkoló hívásra mapConcurrent vagy virtual thread való."


10. Optional

Elmélet: return type, nem mező, nem paraméter, nem Serializable. Kollekció helyett sosem Optional<List<T>>, hanem üres lista.

String city = user.map(User::address).map(Address::city).orElse("n/a");
  • orElseThrow() argumentum nélkül: ez a get() helyes utódja
  • or(supplier), ifPresentOrElse, stream(), isEmpty()

A klasszikus fogós kérdés: orElse vs orElseGet.

opt.orElse(expensive());                 // az expensive() MINDIG lefut, akkor is, ha van érték
opt.orElseGet(() -> expensive());        // csak üres Optionalnál fut le

Anti-patternek: isPresent() + get() (helyette map/orElse...), Optional.of(null) (NPE, null-gyanús értékre ofNullable), Optional mezőként vagy paraméterként.

TLDR: "Az Optional return type-ra való, mezőnek és paraméternek nem. map, flatMap, orElseThrow a helyes használat, az isPresent + get anti-pattern. Az orElse mindig kiértékeli az argumentumát, az orElseGet csak üres esetben."


11. Sequenced Collections

Elmélet: három interfész (SequencedCollection, SequencedSet, SequencedMap) egységes API-t ad a definiált sorrendű kollekcióknak: getFirst, getLast, addFirst, addLast, removeFirst, removeLast, reversed. A map-nél firstEntry, lastEntry, pollFirstEntry, putFirst és társai. Implementálja: List, Deque, LinkedHashSet, SortedSet/TreeSet, LinkedHashMap, TreeMap. A HashSet és a HashMap nem sequenced.

var list = new ArrayList<>(List.of(1, 2, 3));
list.getFirst();           // 1
list.getLast();            // 3 (régen: list.get(list.size() - 1))
var rev = list.reversed(); // [3, 2, 1], de view, nem másolat
list.add(4);               // rev most: [4, 3, 2, 1]
  • A reversed() view, ezért az eredeti változása látszik rajta.
  • Üres kollekción a getFirst() NoSuchElementException-t dob.
  • Immutable kollekción (List.of) a módosító műveletek UnsupportedOperationException-t dobnak.
  • Legnagyobb nyereség a LinkedHashSet: az utolsó elemhez régen végig kellett iterálni.

TLDR: "A sorrendezett kollekciók közös interfészt kapnak: getFirst, getLast, reversed és társai egységesen működnek List, Deque, LinkedHashSet és LinkedHashMap fölött. A reversed() view, nem másolat."


12. Immutable factories

Létrehozás Módosítható null
Collectors.toList() igen (de nem garantált, hogy ArrayList) enged
Stream.toList() nem enged
List.of(...) nem NPE
Collections.unmodifiableList(x) nem, de view enged

Három csapda, amit tudnod kell: - List.of null-ellenes: List.of("x", null) és List.of(...).contains(null) is NPE. - Duplikátum: Set.of(1, 1) és Map.of("k", 1, "k", 2) IllegalArgumentException. A Set.of és a Map.of iterációs sorrendje szándékosan változik JVM-futásonként, sorrendre sosem építhetsz. - unmodifiableList csak view, a List.copyOf másolat:

var src = new ArrayList<>(List.of(1, 2));
var view = Collections.unmodifiableList(src);
var copy = List.copyOf(src);
src.add(3);
// view: [1, 2, 3]    copy: [1, 2]

Még létezik: Map.ofEntries, Set.copyOf, Map.copyOf.

TLDR: "A List.of és társai immutable-ek és null-ellenesek, a Set.of és a Map.of sorrendje nem garantált, a duplikátum kivételt dob. A Stream.toList() immutable, de enged null-t. A Collections.unmodifiableList csak view, a valódi másolathoz List.copyOf kell."