İçeriğe geç
academia.sh

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 List ve sırasız bir HashSet. İkisi üzerinde aynı akış zinciri çalıştırılır ve sonuçlar equals ile karşılaştırılır; ham gezinme sırası hiçbir yerde basılmaz.
  • FN18sorted() doğal sırayla çalışır; harf büyütme Locale.ROOT ile yapılır.
  • FN19distinct() ve findFirst() 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.

  • FN20findFirst() ile findAny() aynı sıralı kaynak üzerinde, aynı akış zincirinden ayrı ayrı çağrılır ve dönen değerler equals ile 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 ve toList() 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 try bloğ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, findFirst yalnı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 IllegalStateException ile düşer; bu kaynağın tipinden bağımsız, akış nesnesinin kendi durumuna ait bir kuraldır.
  • Akışı gezerken kaynak koleksiyonu büyütmek ConcurrentModificationException ile 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat