Stream API ve Tembellik
30 saniyede özet
Stream aşama aşama değil, eleman eleman çalışır: her eleman zincirin tamamından tek başına geçer. İşlemlerin sırasının önemi ve akışın erken durabilmesi buradan gelir.
Aynı işi iki farklı sırayla yapabilirsin: biri çabucak biter, öteki boşuna yorulur. Java’daki Stream’lerde de sıra, kimin ne kadar çalışacağını belirler.
-
Bayt: Milyonlarca elemanlı bir Stream'den ilk üçünü istedim ve cevap anında geldi.
-
Sen: Ama önce hepsinin filtrelenmesi gerekmiyor muydu?
-
Bayt: Stream kuyruğa değil yolcuya bakar: her eleman bütün yolu tek başına yürür.
-
Bayt: Üç yolcu kapıdan geçince iş biter. Kalanlar koltuğundan hiç kalkmaz!
Şu iki satır aynı sonucu verir, ama maliyetleri çok farklıdır:
list.stream().filter(n -> n > 4).map(this::expensive).toList();list.stream().map(this::expensive).filter(n -> n > 40).toList();İkincisinde expensive her elemana uygulanır. Neden olduğunu anlamak için pipeline’ın nasıl aktığını bilmek gerekir.
Her eleman yolu tek başına yürür
Çoğumuz şöyle hayal ederiz: önce filter bütün listeyi süzer, sonra map kalanların hepsini dönüştürür. Bu yanlış.
Gerçekte her eleman zincirin tamamından tek başına geçer, sonra sıradaki elemana geçilir. Buna lazy evaluationİşi gerçekten ihtiyaç duyulana kadar ertelemek. Stream'de ara işlemler terminal işlem gelene kadar hiç çalışmaz.Sözlükte gör → deniyor:
5 → filter ✓ → map → 50 → collect3 → filter ✕ (burada durdu, map hiç çağrılmadı)8 → filter ✓ → map → 80 → collect...Bu tek gerçek, konunun geri kalanının hepsini açıklar.
Kafam karıştı, daha basit anlat
Önce herkes birinci kontrolden, sonra herkes ikinciden geçmez. Her yolcu bütün kontrollerden tek başına geçer, sonra sıradaki yolcu gelir.
`list.stream().filter(p).map(f).toList()` çağrısında elemanlar hangi sırayla işlenir?
Satır satır: eleman eleman akış
Dört elemanlı bir listede `filter(n > 4).map(n * 10)` çalıştırırsan, `map` lambda'sı kaç kez çağrılır? Cevabı göster
İki kez — yalnızca filtreyi geçen elemanlar için. Listedeki eleman sayısı kadar değil.
Adım adım oku
- İlk değer 7, filter, map ve limit'ten tek başına geçip kapıya varır. Sıradaki değer bu sırada bekler.
- 2 filtreye takılır. map onu hiç görmez.
- 9 da bütün yolu yürür ve kapıya varır. limit(2) doldu, akış durur.
- 4 ve 8'e hiç dokunulmadı: ne filtrelendiler ne dönüştürüldüler.
Zincirden kim geçiyor?
List.of(5, 3, 8, 2).stream() .filter(n -> n > 4) .map(n -> n * 10) .toList();Debug
stream Pipeline kuruldu ama hiçbir şey çalışmadı. Akışı başlatan şey bu satırdaki terminal işlem.
- filter cagrisi
- = 0
- map cagrisi
- = 0
Sol/sağ ok tuşlarıyla da gezebilirsin.
Kafam karıştı, daha basit anlat
Filtreyi geçemeyen eleman orada durur, sonraki adımlara hiç uğramaz. Bu yüzden ucuz bir filtreyi pahalı bir işten önce koymak iş tasarrufudur.
`list.stream().filter(x -> { System.out.println("bakıldı"); return true; });` satırı tek başına ne yazdırır?
Bir Stream değişkenine atanıp iki kez terminal işlem uygulanırsa ne olur?
Kendin izle
Simülatör her adımda bir elemanı bir aşama ilerletiyor. Dört pipeline’ı karşılaştır.
Stream pipeline — tembellik ve kısa devre
Tohum 4numbers.stream()
.filter(n -> n > 4)
.map(n -> n * 10)
.collect(toList());Kaynak liste
Aşamalar · lambda çağrı sayısı
filter(n > 4)0 çağrımap(n * 10)0 çağrıcollect(toList())0 çağrı
Sonuç
[]
- Lambda çağrısı
- 0
- Hiç bakılmayan
- 8
- Kısa devre
- hayır
Şu an ne oldu?
Pipeline bekliyor
Ara işlemler tanımlandı ama hiçbiri çalışmadı. Terminal işlem çağrılmadan bir Stream hiçbir şey yapmaz.
Aklında kalsın: Terminal işlem olmadan filter/map yazmak sessizce hiçbir şey yapmaz — derleyici de uyarmaz.
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
Sırayla dene:
filter → map(varsayılan) —mapyalnızca filtreyi geçen elemanlar için çağrılıyor. Aşama başına çağrı sayılarına bak.map → filter— aynı sonuç, amamapçağrı sayısı kaynak listenin tamamı kadar. Sırayı değiştirmek sonucu değil maliyeti değiştiriyor.findFirst— ilk sonuç bulunduğu anda duruyor.Hiç bakılmayansayacı sıfırdan büyük: kaynaktaki kalan elemanlar okunmadı bile.sorted → limit—sortedtüm akışı tamponluyor. Tampon dolana kadar aşağıya tek bir eleman bile geçmiyor.
Her Stream işlemini doğru kategoriye yerleştir.
Tembelliğin sınırı
Ara işlem, terminal işlem.
| Örnekler | Ne zaman çalışır | |
|---|---|---|
| Ara (tembel) | filter, map, peek, flatMap | Terminal işlem gelene kadar hiç |
| Terminal | collect, forEach, reduce, findFirst, anyMatch | Pipeline’ı başlatan şey |
Bir terminal operationStream'i tüketip sonuç üreten işlem — `collect`, `forEach`, `count`. Pipeline ancak bu çağrıldığında çalışmaya başlar.Sözlükte gör → yoksa hiçbir şey çalışmaz — ve derleyici bunu söylemez:
// Bu satır tek bir lambda bile çalıştırmazusers.stream().filter(User::isActive).map(User::email);peek neden güvenilmez?· istersen atla
peek hata ayıklamada yanıltıcıdır: terminal işlem yoksa hiç çalışmaz, kısa devre varsa yalnızca bazı elemanlar için çalışır.
short-circuitCevap belli olur olmaz durmak. `findFirst` ve `anyMatch` tüm veriyi gezmeden çıkar — sonsuz stream'leri çalışabilir kılan şey budur.Sözlükte gör →. findFirst, anyMatch, allMatch, noneMatch ve limit sonuç kesinleştiği anda pipeline’ı durdurur.
Bunun en çarpıcı sonucu: sonsuz bir Stream’i sonlandırabilirsin.
Stream.iterate(1, n -> n + 1) // sonsuz .filter(n -> n % 7 == 0) .limit(3) // kısa devre — üç eleman bulunca durur .toList(); // [7, 14, 21]limit olmasaydı bu program asla bitmezdi.
Durumlu ara işlemler. sorted() ve distinct() durumludur: ilk sonucu üretebilmek için akışın tamamını görmek zorundadırlar.
Stream.iterate(1, n -> n + 1) .sorted() // sonsuz akışı sıralamaya çalışıyor — buraya kilitlenir .limit(3) .toList();Kafam karıştı, daha basit anlat
Sonunda bir sonuç isteyen adım yoksa Stream hiç çalışmaz. Sonuç kesinleştiği anda da durur, bu yüzden sonsuz bir akışı bile güvenle bitirebilirsin.
`Stream.iterate(1, n -> n + 1).sorted().limit(3).toList()` çalıştırılırsa ne olur?
Tuzaklar
İşlem sırası bir performans kararıdır. Kural basit: ucuz ve eleyici işlemleri öne al.
users.stream() .map(this::loadFullProfile) // her kullanıcı için DB sorgusu .filter(p -> p.isActive()) // sonra çoğunu at .toList();
users.stream() .filter(User::isActive) // önce ele — bedava .map(this::loadFullProfile) // yalnızca kalanlar için DB sorgusu .toList();Sonuç ikisinde de aynı. Fark, loadFullProfile’ın kaç kez çağrıldığı.
Stream tek kullanımlıktır.
var stream = list.stream();stream.filter(...).toList();stream.map(...).toList(); // IllegalStateException: stream has already been operated uponparallelStream sihirli değil· istersen atla
Küçük listelerde ya da ağ ve disk bekleyen lambda’larda parallelStream() genellikle yavaşlatır. Varsayılan olarak bütün uygulamanın paylaştığı ortak ForkJoinPool’u kullanır; orada bekleyen bir çağrı başka yerleri de etkiler. Ölçmeden kullanılmaz.
Aşağıdaki örnek bir bankanın kart harcamaları üzerinde: işlem sırasını log’layan bir pipeline, ilk şüpheli harcamada duran kısa devre, taksit tarihlerini üreten sonsuz akış ve ekstredeki kategori özeti.
Derinleş · Kart harcamalarında stream tembelliği: görünür ve işe yarar 5 dosya · ~78 satır · ilk okumada atlayabilirsin
Kendini sına
Önce hızlı bir ısınma: puan yok, kayıt yok.
filter ve map, sonda toList gibi bir toplayıcı işlem çağrılmadan hiçbir şey yapmaz.
Bu program ne yazdırır?
Aklında kalacak üç şey
- 1 Her eleman zincirin tamamından tek başına geçer. 'Önce hepsi filtrelenir, sonra hepsi dönüştürülür' yanlıştır.
- 2 Ara işlemler tembeldir: sonda bir toplayıcı işlem çağrılmazsa filter ve map hiçbir şey yapmaz, derleyici de uyarmaz.
- 3 sorted ve distinct her şeyi görmeden sonuç veremez. Bu yüzden tembelliği bozarlar ve sonsuz bir akışta hiç bitmezler.
5 kart sonraki derste seni bekliyor