Ders 08 / 15
Akış API'si
Java'nın veri akışı zinciri on işlemden üçünü kaynaktan bağımsız garanti eder, beşini kaynağın sırasına devreder, ikisini çağırana bırakır. Ara işlem sonlandırıcı olmadan hiç çalışmaz, ama sonlandırıcı çağrıldığında bile hangi öğenin ziyaret edileceği garanti değildir. Aynı veri akışını iki kez tüketmek ve gezerken kaynağı büyütmek ikisi de istisnayla düşer.
İçindekiler
Önceki ders tek bir lambda ifadesinin çevresinden ne yakaladığını ölçtü ve dört sözdizim biçiminin aynı hedefe bağlandığını gösterdi. Standart kütüphanenin asıl kullanım biçimi böyle tek bir lambda değil, birbirine art arda bağlanmış birkaç lambdadır — bir veri akışı zinciri. Her halka kendi başına doğru çalışsa da, zincire dizildiğinde ayrı bir soru doğar: zincirin hangi parçası kaynağın sırasına bağlıdır, hangisi her koşulda aynı sırayı verir, hangisi bir sonlandırıcı olmadan hiç çalışmaz, ve hangi kural bütünüyle çağırana kalır?
Bu ders on ayrı iddiayı koşturarak sınıflar: her biri için gözlemlenen davranış kaynaktan
bağımsız mı (arayüz garantisi), kaynağın kendi sırasına mı bağlı, yoksa çağıranın uyduğu
bir kural mı? Ölçüm biçimi öncekilerle aynıdır — kod yazılır, çalıştırılır, sonuç
karşılaştırılır — ama burada karşılaştırılan iki gerçekleştirim değil, aynı işlemin iki
ayrı kaynak üzerindeki davranışıdır. java.io akışlarıyla karıştırılmaması için burada
geçen her çıplak “akış” sözcüğü bu paragraftaki niteleyiciden sonra kullanılacak: veri
akışı zinciri, kısaca akış zinciri.
Akış Zincirinin Sırası Kaynaktan Gelir, Arayüzden Değil
- FN17 — Aynı beş sözcük iki kaynakta tutulur: sıralı bir
Listve sırasız birHashSet. İkisi üzerinde aynı akış zinciri çalıştırılır ve sonuçlarequalsile karşılaştırılır; ham gezinme sırası hiçbir yerde basılmaz. - FN18 —
sorted()doğal sırayla çalışır; harf büyütmeLocale.ROOTile yapılır. - FN19 —
distinct()vefindFirst()sıralı kaynak üzerinde denenir; sırasız kaynakla tekrarlanmaz, çünkü ölçülen şey ilk-görülen sıranın varlığıdır ve bu kavram sırasız kaynakta tanımsızdır.
// AkisSira.java — akis zincirinin sirasi kaynaktan mi geliyor, arayuzden mi
import java.util.*;
import java.util.stream.*;
public class AkisSira {
public static void main(String[] args) {
List<String> siraliKaynak = List.of("armut", "kiraz", "elma", "uzum", "nar");
Set<String> sirasizKaynak = new HashSet<>(siraliKaynak);
List<String> sirali1 = siraliKaynak.stream().sorted().toList();
List<String> sirali2 = sirasizKaynak.stream().sorted().toList();
System.out.println("sorted(), sirali kaynakta alfabetik sira mi : "
+ sirali1.equals(List.of("armut", "elma", "kiraz", "nar", "uzum")));
System.out.println("sorted(), sirasiz kaynakta da ayni sonuc mu : " + sirali2.equals(sirali1));
List<String> beklenenBuyuk = List.of("ARMUT", "KIRAZ", "ELMA", "UZUM", "NAR");
List<String> haritalananSirali = siraliKaynak.stream().map(x -> x.toUpperCase(Locale.ROOT)).toList();
List<String> haritalananSirasiz = sirasizKaynak.stream().map(x -> x.toUpperCase(Locale.ROOT)).toList();
System.out.println("map(), sirali kaynakta giris sirasini koruyor mu: " + haritalananSirali.equals(beklenenBuyuk));
System.out.println("map(), sirasiz kaynakta giris sirasini koruyor mu: " + haritalananSirasiz.equals(beklenenBuyuk));
List<String> beklenenUzun = List.of("armut", "kiraz", "elma", "uzum");
List<String> filtrelenmis = siraliKaynak.stream().filter(x -> x.length() > 3).toList();
System.out.println("filter(), sirali kaynakta sirayi koruyor mu : " + filtrelenmis.equals(beklenenUzun));
List<String> tekrarli = List.of("armut", "kiraz", "armut", "elma", "kiraz");
List<String> tekil = tekrarli.stream().distinct().toList();
System.out.println("distinct(), ilk gorulen sirayla korunuyor mu : "
+ tekil.equals(List.of("armut", "kiraz", "elma")));
Optional<String> ilk1 = siraliKaynak.stream().findFirst();
Optional<String> ilk2 = siraliKaynak.stream().findFirst();
System.out.println("findFirst(), iki ayri cagri ayni ogeyi mi veriyor: " + ilk1.equals(ilk2));
}
}
sorted(), sirali kaynakta alfabetik sira mi : true sorted(), sirasiz kaynakta da ayni sonuc mu : true map(), sirali kaynakta giris sirasini koruyor mu: true map(), sirasiz kaynakta giris sirasini koruyor mu: false filter(), sirali kaynakta sirayi koruyor mu : true distinct(), ilk gorulen sirayla korunuyor mu : true findFirst(), iki ayri cagri ayni ogeyi mi veriyor: true
İlk iki satır bir kaynaktan bağımsız garantiyi ölçüyor: sorted() sıralı kaynakta alfabetik
sırayı üretiyor, sırasız HashSet kaynağında da aynı sonucu veriyor. Girdi hangi
kapta tutulursa tutulsun sorted() kendi sözünü tek başına yerine getiriyor — bu bir
arayüz garantisidir, kaynağın kendi sırasına ihtiyaç duymaz.
Üçüncü ve dördüncü satır tam tersini gösteriyor. map() sıralı List kaynağında girişin
sırasını taşıyor (true), ama sırasız HashSet kaynağında aynı sözcüklerle çalıştığı
hâlde bu sırayı taşımıyor (false). map()’in kendisi hiç değişmedi; değişen kaynaktı.
Sıra sözü map()’e değil, altındaki kaba aittir — bu, dersin sınırlayıcı ölçümüdür ve
brifingin iddiasını doğruluyor: “akış sırayı korur” cümlesi kaynağın sözüdür, akışın değil.
Kalan üç satır aynı kaynağa bağlı kümeye giriyor. filter() sıralı kaynakta kalan
öğelerin sırasını koruyor; distinct() yinelenenler arasından ilk görüleni tutuyor ve
sırayı bozmuyor; findFirst() sıralı kaynakta iki ayrı çağrıda hep aynı öğeyi veriyor.
Üçünün de ortak noktası: sonucu belirleyen şey kaynağın karşılaşma sırasıdır, ve bu
sıra yalnızca kaynak sıralı olduğunda vardır. Sırasız bir kaynakla tekrarlansalar
“ilk görülen” ya da “ilk öğe” kavramının kendisi tanımsızlaşır.
Bu ders “kaynağa bağlı” adını ayrı bir kova olarak kullanıyor, çünkü koleksiyonlar
konusundaki üçlü ayrımın orta terimi — gerçekleştirim davranışı — burada başka bir
seçime karşılık gelmiyor. Orada seçilen şey HashMap ile TreeMap gibi ayrı bir sınıftı;
burada map()’in kendisi hiç değişmiyor, tek değişen kaynağın yapısıdır (sıralı mı,
sırasız mı). Sonuç aynı yöne çıkıyor: gözlem çağrı yazımında görünmeyen bir üçüncü tarafın
elinde, ve o taraf bu konuda arayüz değil kaynaktır.
Aynı satırın bir de gevşek ucu var. findFirst() “sıralı kaynakta hep aynı öğeyi ver” der;
komşu yöntem findAny() böyle bir söz vermez, yalnızca eşleşen bir öğe döndürmeyi
garanti eder.
- FN20 —
findFirst()ilefindAny()aynı sıralı kaynak üzerinde, aynı akış zincirinden ayrı ayrı çağrılır ve dönen değerlerequalsile karşılaştırılır.
// FindAny.java — findFirst ile findAny ayni sozu mu veriyor
import java.util.*;
public class FindAny {
public static void main(String[] args) {
List<String> kaynak = List.of("armut", "kiraz", "elma", "uzum", "nar");
Optional<String> ilk = kaynak.stream().findFirst();
Optional<String> herhangi = kaynak.stream().findAny();
System.out.println("findFirst() sonucu: " + ilk.get());
System.out.println("findAny() sonucu : " + herhangi.get());
System.out.println("ikisi ayni mi : " + ilk.equals(herhangi));
}
}
findFirst() sonucu: armut findAny() sonucu : armut ikisi ayni mi : true
Sıralı, tek iş parçacıklı bu akış zincirinde ikisi aynı öğeyi veriyor — API’nin
gevşekliği burada hiç görünmüyor, çünkü gevşekliğin fiilen kullanılması ancak akış
paralelleştirildiğinde söz konusu olur ve bu kurs akış paralelleştirmeyi ölçmüyor. Ölçülen
tek şey şu: aynı sonucu iki ayrı yöntemin iki ayrı sözle vermesi, sözlerin sıkılığının
sonuçtan bağımsız olabileceğini gösteriyor — findAny() daha az söz verir, ama bu daha az
sözün bedelini bu koşumda ödemiyoruz. findAny() tabloya girmiyor, çünkü verdiği tek söz
“eşleşen bir öğe” sözüdür ve bu söz zaten koşulsuzdur; kaynağa bağlı olan yalnız
findFirst()’ün “hangi öğe” sorusudur.
Ara İşlem Sonlandırıcı Olmadan Hiç Çalışmaz
Bir akış zincirindeki map, filter, sorted gibi ara işlemler kurulur kurulmaz
çalışmaz; yalnızca zincire eklenirler. Bu, önceki bir konuda kurulan tembel değerlendirme
(lazy evaluation) kavramının burada sorulan hâlidir — kavram tekrar açılmıyor, sorulan
soru şu: bu tembellik akış zincirinin sözü müdür, yoksa yalnızca bir gerçekleştirim
ayrıntısı mıdır?
- FN21 — Aynı ara işlem üç kez kurulur: hiç sonlandırıcı olmadan,
count()sonlandırıcısıyla vetoList()sonlandırıcısıyla. Her seferinde bir sayaç, ara işlemin kaç kez çalıştığını sayar.
// AkisTembellik.java — ara islem sonlandirici olmadan calisiyor mu
import java.util.*;
import java.util.stream.*;
public class AkisTembellik {
public static void main(String[] args) {
List<String> kaynak = List.of("armut", "kiraz", "elma", "uzum", "nar");
int[] sayac1 = {0};
Stream<String> boruHatti = kaynak.stream().peek(x -> sayac1[0]++);
System.out.println("sonlandirici hic cagrilmadan calisan adim : " + sayac1[0]);
int[] sayac2 = {0};
long uzunluk = kaynak.stream().peek(x -> sayac2[0]++).count();
System.out.println("count() sonlandiricisiyla calisan adim : " + sayac2[0] + " (uzunluk=" + uzunluk + ")");
int[] sayac3 = {0};
kaynak.stream().peek(x -> sayac3[0]++).toList();
System.out.println("toList() sonlandiricisiyla calisan adim : " + sayac3[0]);
}
}
sonlandirici hic cagrilmadan calisan adim : 0 count() sonlandiricisiyla calisan adim : 0 (uzunluk=5) toList() sonlandiricisiyla calisan adim : 5
İlk satır beklenen sonucu veriyor: sonlandırıcı hiç çağrılmadığında ara işlem hiç çalışmıyor, sayaç sıfırda kalıyor. Bu, koşuldan bağımsız bir garanti — akış zincirinin kendi sözü — ve dördüncü konudaki tembel değerlendirmenin buradaki karşılığı bu kadardır.
İkinci satır bu garantiyi bir yönde daraltıyor. count() çağrıldığı hâlde sayaç yine
sıfır: uzunluk doğru hesaplandı (5) ama ara işlem hiç çalışmadı. Nedeni kaynağın
kendisinde: List.of(...)’un ürettiği liste boyutunu zaten bilir, ve map/filter gibi
öğe sayısını değiştirmeyen bir ara işlem zincire eklendiğinde akış zinciri sonucu tek tek
öğeleri ziyaret etmeden, doğrudan kaynağın boyutundan hesaplayabilir. peek de bu zincirin
bir parçası olduğu için, kaynağın boyutu üzerinden kısayoldan geçen bu hesap onu hiç
çağırmıyor. Üçüncü satır bunu doğruluyor: toList() sonucu üretmek için her öğeye
gerçekten ihtiyaç duyar, bu yüzden kısayol yoktur ve ara işlem beş kez çalışıyor.
Buradan çıkan sonuç ilk bakışta göründüğünden dardır: garanti edilen şey “sonlandırıcı
olmadan hiç çalışmama”dır, “sonlandırıcı çağrıldığında her öğe için çalışma” değildir.
İkincisi bir gerçekleştirim kararıdır — akış zinciri sonucu başka bir yoldan üretebiliyorsa
ara adımı atlayabilir. peek bu yüzden yalnızca gözlem ve hata ayıklama içindir; iş
mantığının bir parçası olarak kurulursa, çalışıp çalışmayacağı sonlandırıcıya bağlı kalır.
Veri Akışı Tek Kullanımlıktır, Kaynağını Gezerken Büyütülemez
Son iki satır çağıranın kuralına giriyor: bu ikisi kaynağın türünden ya da akış zincirinin biçiminden bağımsız, çağıranın uyması gereken iki kısıttır.
- FN22 — Bir akış nesnesi iki kez tüketilir; ikinci tüketim ayrı bir
trybloğunda denenir. Ayrı bir akış zincirinde, gezinme sırasında kaynak listesine bir öğe eklenir.
// AkisKural.java — cagiranin iki kurali: tek kullanim ve gezerken buyutme
import java.util.*;
import java.util.stream.*;
public class AkisKural {
public static void main(String[] args) {
List<String> kaynak = List.of("armut", "kiraz", "elma", "uzum", "nar");
Stream<String> tekKullanim = kaynak.stream();
tekKullanim.count();
try {
tekKullanim.count();
System.out.println("ayni akisi ikinci kez tuketmek: dustu");
} catch (IllegalStateException e) {
System.out.println("ayni akisi ikinci kez tuketmek -> istisna: " + e.getClass().getSimpleName());
}
List<String> genisleyen = new ArrayList<>(kaynak);
try {
genisleyen.stream().forEach(x -> {
if (x.equals("elma")) genisleyen.add("mango");
});
System.out.println("gezerken kaynagi buyutmek: dustu, boyut=" + genisleyen.size());
} catch (ConcurrentModificationException e) {
System.out.println("gezerken kaynagi buyutmek -> istisna: " + e.getClass().getSimpleName());
}
}
}
ayni akisi ikinci kez tuketmek -> istisna: IllegalStateException gezerken kaynagi buyutmek -> istisna: ConcurrentModificationException
İki kural da düştüğünde istisna veriyor, sessiz kalmıyor. tekKullanim üzerinde
count() bir kez çağrıldıktan sonra akış tüketilmiş sayılıyor; ikinci çağrı akışın
kendisini yeniden kullanmaya çalıştığı için IllegalStateException düşüyor — kaynağın
tipinden bağımsız, akış nesnesinin kendi durumundan kaynaklanan bir kural. Bu davranış bir
akış nesnesinin dahili olarak “tüketildi” diye işaretli bir bayrak taşıdığını gösteriyor:
bayrak bir kez kalkınca, üzerinde yeni bir sonlandırıcı çağırmak — hangisi olursa olsun –
aynı istisnayla karşılanıyor.
İkinci kural farklı bir mekanizmaya dayanıyor: genisleyen listesi forEach ile gezilirken
aynı liste büyütülüyor ve bu, listenin kendi değişiklik sayacını bozarak
ConcurrentModificationException düşürüyor — akışın sonlandırıcısı forEach olsa da,
düşen istisna aslında kaynağın kendi korumasıdır; akış zinciri burada yalnızca kaynağın
yineleyicisini kullanan bir aracıdır. Bu ikisi arasındaki fark önemlidir: birinci istisna
akış API’sinin kendi kuralıdır ve her akış nesnesinde geçerlidir, ikincisi ise akışın
kaynağına bağlıdır — değiştirilemez bir kaynaktan kurulan bir akış zinciri gezinirken
zaten hiç büyütülemeyeceği için bu kural orada hiç devreye girmez.
Ders boyunca ölçülen on satır tek tabloda toplanabilir:
| # | Ölçülen | Sonuç | Kaynağı |
|---|---|---|---|
| 1 | sorted(), sıralı kaynak |
alfabetik sıra | arayüz garantisi |
| 2 | sorted(), sırasız kaynak |
aynı sonuç | arayüz garantisi |
| 3 | map(), sıralı kaynak |
sıra korunur | kaynağa bağlı |
| 4 | map(), sırasız kaynak |
sıra korunmaz | kaynağa bağlı |
| 5 | filter(), sıralı kaynak |
sıra korunur | kaynağa bağlı |
| 6 | distinct(), sıralı kaynak |
ilk görülen korunur | kaynağa bağlı |
| 7 | findFirst(), sıralı kaynak |
tutarlı sonuç | kaynağa bağlı |
| 8 | ara işlem, sonlandırıcısız | 0 çalışma | arayüz garantisi |
| 9 | aynı akışı ikinci tüketim | istisna | çağıranın kuralı |
| 10 | gezerken kaynağı büyütme | istisna | çağıranın kuralı |
Dağılım şöyle: üçü kaynaktan bağımsız bir garanti, beşi kaynağın ya da
sonlandırıcının seçimine bağlı, ikisi çağıranın uyması gereken ve kırıldığında
istisnayla düşen birer kuraldır. count()’un peek’i atlaması tabloya ayrı bir satır
olarak girmedi, çünkü ölçtüğü şey bir kaynağa bağlılık değil, sonlandırıcının kendi
seçimidir — sekizinci satırın bir alt sınırı olarak okunmalıdır. On işlemin çoğunluğu –
beşi — arayüzün değil kaynağın sözünü taşıyor; bu, önceki konunun kurduğu düzenle aynı
yöndedir: standart kütüphanenin görünen davranışının çoğu, konuştuğu arayüzün değil,
altındaki seçimin sözüdür.
Özet
- Bir akış zincirinin sırayla ilgili sözü genellikle kaynaktan gelir:
map,filter,distinct,findFirstyalnızca kaynak sıralıysa karşılaşma sırasını korur. sorted()kaynaktan bağımsız bir garantidir: sıralı ya da sırasız kaynakla aynı sonucu üretir. Sınırlayıcı ölçüm: “akış sırayı korur” cümlesi çoğunlukla kaynağın sözüdür.- Bir ara işlem sonlandırıcı olmadan hiç çalışmaz; bu koşulsuz bir garantidir. Ama
sonlandırıcı çağrıldığında her öğenin ziyaret edileceği garanti değildir —
count()ara işlemi tamamen atlayabilir,toList()atlayamaz. - Aynı akışı ikinci kez tüketmek
IllegalStateExceptionile düşer; bu kaynağın tipinden bağımsız, akış nesnesinin kendi durumuna ait bir kuraldır. - Akışı gezerken kaynak koleksiyonu büyütmek
ConcurrentModificationExceptionile düşer; düşen istisna akışın değil, altındaki koleksiyonun korumasıdır. - On ölçümün üçü kaynaktan bağımsız garanti, beşi kaynağa bağlı, ikisi çağıranın kuralıdır; çoğunluk kaynağa bağlı kalmıştır.
Sonraki Adım
Bu ders bir akış zincirinin ne söylediğini, ne söylemediğini ölçtü: sıra çoğunlukla
kaynağın sözüdür, tembellik koşulsuz bir garantidir ama her öğenin ziyaret edileceği
değildir, ve iki kural — tek kullanım, gezerken büyütmeme — doğrudan çağırana düşer.
Zincirin ucunda genellikle tek bir değer değil, bir kap birikir — bir liste, bir sözlük,
bir sayı — ve bu derste hep hazır toplama yöntemleri (toList(), count()) kullanıldı.
Sıradaki ders o kabı kuran parçaya bakar: toplayıcının hangi sözü verdiğini, sonuç kabının
tipinin arayüzün sözü olup olmadığını ve yinelenen bir anahtarla karşılaşıldığında hangi
toplayıcının istisna verip hangisinin birleştirdiğini ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.