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());
- Lazy: terminal operation nélkül semmi nem fut (a
peeksem). Ezért íródik ki először apipeline 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
findFirstaz első találatnál leáll, acccés addel sem jut afilter-ig. - Barrier (
sorted): hafilterésmapközé teszel egy.sorted()-et, afilterminden elemre lefut, és csak utána indul amap, mert asortedaddig 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áshozSupplier<Stream<T>>vagy új stream kell.
Side effects
- A
peekbizonyos esetekben nem fut le: - 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
sortedbarrier, ezért végtelen source-on sosem ad tovább elemet, a puffere pedig addig nő, amígOutOfMemoryErrornem 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 adropWhileprefixet vágnak, afilterelemenként dönt. - Végtelen streamen a
takeWhilevéget ér, afilternem: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 újStream-et, a pipeline kilapítja. Elemenként stream-allokáció.mapMulti(BiConsumer<T, Consumer<R>>): nem adsz vissza semmit, az eredményeket asink-be tolod (sink.accept(...)). Nincs köztes stream.- Alapértelmezés a
flatMap(olvashatóbb, deklaratív). AmapMultiakkor jobb, ha elemenként kevés (0–2) eredmény keletkezik, vagy ha az imperatív logika egyszerűbb. - Type witness (
.<String>): asink.accept(...)nem árulja el a fordítónak azRtípust, ezértObjectlesz. 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)
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}
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
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:
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
scanaz, ami régen indexelős ciklus vagy trükkös Collector volt (futó összeg, futó egyenleg).mapConcurrenta mai válasz a blokkoló, I/O jellegű párhuzamosításra. Régen erreparallelStream()-et használtak, ami a közösForkJoinPool-t blokkolta.- Még létezik:
fold(sok elemből egy eredmény, intermediate operation-ként) ésandThen(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.
orElseThrow()argumentum nélkül: ez aget()helyes utódjaor(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űveletekUnsupportedOperationException-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."