İçeriğe geç
academia.sh

Ders 09 / 15

Toplayıcılar

Bir toplayıcı dört parçadan kurulur: üretici, birikeç, birleştirici, bitirici. Sonuç kabının tipi ve değiştirilebilirliği arayüzün sözü değildir — toList() değiştirilebilir bir liste döndürüyor ama bunu hiçbir yerde vaat etmiyor, toUnmodifiableList() ise değiştirilemezliği garanti eder. Yinelenen anahtarla karşılaşıldığında sözlük toplayıcısı istisna verir, gruplama birleştirir.

İçindekiler

Önceki ders bir akış zincirinin ne söylediğ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. Zincirin ucunda genellikle tek bir değer değil, bir kap birikir — bir liste, bir sözlük, bir sayı — ve bu kabı kuran parça collect çağrısına verilen bir toplayıcıdır (Collector). toList(), groupingBy(), joining() gibi adlar birer yöntemden çok birer tarif gibi okunur: her biri kütüphanenin önceden kurup teslim ettiği bir toplayıcıdır. Toplayıcının kendisi hangi sözü verir, sonuç kabının tipi bu sözün bir parçası mıdır, ve yinelenen bir anahtarla karşılaşıldığında hangi toplayıcı ne yapar?

Toplayıcının Dört Parçası

Bir toplayıcı tek bir yöntem değildir; dört ayrı parçadan kurulur. Üretici boş bir kap kurar, birikeç akıştaki her öğeyi bu kaba işler, birleştirici iki ayrı kabı tek kapta toplar (paralel çalışmada devreye girer), bitirici biriken kabı son sonuca çevirir. Collectors sınıfındaki hazır toplayıcılar bu dört parçayı zaten kurulu verir; parçaların kendisini görmek için burada elle kurulur.

  • FN23 — Dört parça Collector.of ile tek tek verilir ve bir akış zincirinde kullanılır. Kap burada bir dizgi değil, tek gözlü bir int[] — toplam harf sayısını tutan bir biriktirici.
// Parca.java — toplayicinin dort parcasi: uretici, birikec, birlestirici, bitirici
import java.util.function.*;
import java.util.stream.*;

public class Parca {
    public static void main(String[] args) {
        Supplier<int[]> uretici = () -> new int[1];
        BiConsumer<int[], String> birikec = (kutu, kelime) -> kutu[0] += kelime.length();
        BinaryOperator<int[]> birlestirici = (a, b) -> { a[0] += b[0]; return a; };
        Function<int[], Integer> bitirici = kutu -> kutu[0];

        Collector<String, int[], Integer> harfSayaci =
                Collector.of(uretici, birikec, birlestirici, bitirici);

        int toplam = Stream.of("armut", "kiraz", "elma").collect(harfSayaci);
        System.out.println("dort parcayla kurulan toplayici, toplam harf: " + toplam);
    }
}
dort parcayla kurulan toplayici, toplam harf: 14

Sonuç doğru: armut (5) + kiraz (5) + elma (4) = 14. Ama burada ölçülen sonucun kendisi değil, dört parçanın rolüdür. uretici akış başlamadan önce bir kez çağrılır ve boş kutuyu kurar. birikec her öğede bir kez çağrılır ve kutuyu günceller — bu, akış zincirinin gerçek işidir. birlestirici bu dizide hiç çağrılmaz, çünkü akış sıralı; iki ayrı parça akıştan gelmedi ki birleştirilsin. bitirici en son bir kez çağrılır ve int[] kutusunu düz bir Integer’a çevirir. Collectors.toList(), Collectors.groupingBy() gibi hazır toplayıcılar bu dört parçayı zaten doldurup teslim eder; aralarındaki fark yalnız hangi kabı kurdukları ve o kaba nasıl yazdıklarıdır.

birlestirici’nin bu koşumda hiç çağrılmaması bir eksiklik değil, tam olarak beklenen sonuçtur. Standart kütüphanenin dört parçalı tasarımı akışın paralel çalıştırılma olasılığını baştan hesaba katar: akış birden çok parçaya bölünüp her parça kendi kutusunu biriktirdiğinde, kutuları teke indiren adım birleştiricidir. Bu kurs akışları paralelleştirmiyor, bu yüzden birleştirici burada hiç iş görmüyor — ama toplayıcının imzası onu her zaman ister, çünkü aynı toplayıcı nesnesi paralel bir akışta da kullanılabilmelidir. Dört parçanın üçü (üretici, birikeç, bitirici) sıralı bir akışta yeterken, dördüncüsü yalnız olasılık için orada durur.

Sonuç Kabının Tipi Arayüzün Sözü Değildir

Collectors.toList() bir List<T> döndürür, ama hangi sınıfın bir List’i olduğunu belirtmez. Bu, bir önceki konuda ölçülen “gerçekleştirime bırakılmış söz” örüntüsünün burada tekrar çıkışıdır: arayüz bir tip söyler, hangi somut sınıf olduğunu söylemez.

  • FN24 — Aynı üç sözcük iki ayrı toplayıcıyla toplanır: toList() ve toUnmodifiableList(). İkisine de sonradan bir öğe eklenmeye çalışılır.
// Kap.java — toplayicinin dondurdugu kabin tipi ve degistirilebilirligi belgede mi
import java.util.*;
import java.util.stream.*;

public class Kap {
    public static void main(String[] args) {
        List<String> degistirilebilir = Stream.of("armut", "kiraz").collect(Collectors.toList());
        try {
            degistirilebilir.add("elma");
            System.out.println("toList() sonucuna ekleme: basarili (belgede soz verilmemis)");
        } catch (UnsupportedOperationException e) {
            System.out.println("toList() sonucuna ekleme: basarisiz");
        }

        List<String> degistirilemez = Stream.of("armut", "kiraz").collect(Collectors.toUnmodifiableList());
        try {
            degistirilemez.add("elma");
            System.out.println("toUnmodifiableList() sonucuna ekleme: basarili");
        } catch (UnsupportedOperationException e) {
            System.out.println("toUnmodifiableList() sonucuna ekleme -> istisna: " + e.getClass().getSimpleName());
        }
    }
}
toList() sonucuna ekleme: basarili (belgede soz verilmemis)
toUnmodifiableList() sonucuna ekleme -> istisna: UnsupportedOperationException

toList()’in döndürdüğü listeye ekleme başarılı oluyor — ama bu bir garanti değil, bugünkü gerçekleştirimin bir sonucu. Collectors.toList()’in belgesi dönen listenin tipi, değiştirilebilirliği ve iş parçacığı güvenliği hakkında hiçbir söz vermez; bu kod yarın farklı bir sınıf döndürse de sözleşme bozulmaz, çünkü zaten bir sözleşme verilmemiştir. toUnmodifiableList() ise tam tersini yapıyor: eklemeye çalışmak UnsupportedOperationException ile düşüyor, ve bu düşüş belgede açıkça garanti edilen bir davranıştır. İki yöntem de bir List<String> döndürüyor; imzaları aynı, sözleri farklı. Tipi okuyarak bu farkı görmek mümkün değildir — yalnızca belgeyi okuyarak ya da tam da burada yapıldığı gibi koşturarak.

Bu belirsizliğin bir gerekçesi var: söz vermemek kütüphaneye esneklik bırakır. toList() belgesi hangi sınıfı döndüreceğini sabitlemiş olsaydı, o sınıf bir daha hiç değiştirilemezdi — her değişiklik geriye dönük uyumluluğu bozardı. Söz vermeyerek kütüphane kendi gerçekleştirimini istediği zaman değiştirme hakkını saklı tutuyor; bunun bedelini, çağıranın “bu liste değiştirilebilir mi” sorusuna kod okuyarak değil, belgeye bakarak yanıt vermek zorunda kalması ödüyor.

Bitirici Var Olan Bir Toplayıcıya Sonradan Eklenebilir

toUnmodifiableList() değiştirilemezliği garanti ediyordu, ama bu garantiyi yalnız kendi adını taşıyan tek bir toplayıcı verebiliyor değildir. Dört parça birbirinden ayrı olduğu için, var olan herhangi bir toplayıcının bitiricisi üstüne yeni bir dönüşüm eklenebilir — toplayıcının geri kalanına dokunmadan.

  • FN25Collectors.toList()’in ürettiği kabın üstüne Collections::unmodifiableList bir ikinci bitirici olarak eklenir. Ayrıca Collectors.joining üç ayrı katkı ile – ayraç, önek, sonek — boş ve dolu bir akışta denenir.
// Bitirici.java — bitirici sonradan eklenebiliyor mu, boyle uc parcali birlestirme nasil calisiyor
import java.util.*;
import java.util.stream.*;

public class Bitirici {
    public static void main(String[] args) {
        List<String> boyle = Stream.of("armut", "kiraz", "elma")
                .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
        try {
            boyle.add("uzum");
            System.out.println("collectingAndThen sonucuna ekleme: basarili");
        } catch (UnsupportedOperationException e) {
            System.out.println("collectingAndThen sonucuna ekleme -> istisna: " + e.getClass().getSimpleName());
        }

        String bos = Stream.<String>of().collect(Collectors.joining(", ", "[", "]"));
        String dolu = Stream.of("armut", "kiraz", "elma").collect(Collectors.joining(", ", "[", "]"));
        System.out.println("bos akista birlestirme  : " + bos);
        System.out.println("dolu akista birlestirme : " + dolu);
    }
}
collectingAndThen sonucuna ekleme -> istisna: UnsupportedOperationException
bos akista birlestirme  : []
dolu akista birlestirme : [armut, kiraz, elma]

Collectors.collectingAndThen var olan bir toplayıcıyı (Collectors.toList()) ve ek bir dönüşüm işlevini (Collections::unmodifiableList) alıp bunları tek bir yeni bitiriciye kaynaştırıyor — üretici ve birikeç toList()’inkiyle birebir aynı kalıyor, değişen yalnız son adım. Sonuç toUnmodifiableList() ile aynı: ekleme UnsupportedOperationException ile düşüyor. Bu, önceki bölümün ölçtüğü “sonuç kabının tipi arayüzün sözü değildir” gözlemini tamamlıyor: söz eksikse, çağıran onu kendi bitiricisiyle sonradan ekleyebilir.

İkinci ölçüm bitiricinin ne zaman çalıştığını gösteriyor. joining(", ", "[", "]") üç ayrı parça alır: ayraç öğeler arasına, önek ve sonek ise yalnızca bir kez, en başa ve en sona konur. Boş akışta bile sonuç []’dir — önek ve sonek hâlâ ekleniyor, çünkü onları ekleyen bitirici, kaç öğe birikmiş olursa olsun en sonda tam olarak bir kez çalışır. Dolu akışta üç öğe iki ayraçla ayrılıyor, önek ve sonek yine yalnızca birer kez görünüyor. Bu, bitiricinin birikeçten ayrı bir adım olduğunun doğrudan kanıtıdır: birikeç öğe sayısınca çalışır, bitirici öğe sayısından bağımsız olarak bir kez çalışır.

Ayracın kendisi de aynı ayrımı gösteriyor: üç öğe arasında iki ayraç var, üç değil. Ayracı ekleyen mantık birikeçte durur ve “ilk öğe değilse önce ayracı ekle” kuralını izler – ilk öğede bu kural devreye girmez, bu yüzden öğe sayısı kadar değil, öğe sayısından bir eksik ayraç üretilir. Önek ve sonek ise birikeçte hiç yer almaz; onlar yalnızca bitiricinin işidir ve bu yüzden akış tümüyle boş olsa bile ikisi de görünür.

Yinelenen Anahtar: Sözlükte İstisna, Gruplamada Birleşme

Bir toplayıcının çağıranı ilgilendiren asıl kuralı, girdi beklenmedik bir biçimde geldiğinde ortaya çıkar. Aynı uzunlukta — dolayısıyla aynı anahtarda — iki sözcük geldiğinde iki farklı toplayıcı iki farklı tepki veriyor.

  • FN26 — Beş sözcükten ikisi dört harfli (elma, uzum), ikisi beş harfli (armut, kiraz). Aynı akış önce uzunluğa göre gruplanır, sonra uzunluğu anahtar yapan bir sözlüğe toplanmaya çalışılır.
// Yinelenen.java — yinelenen anahtar: gruplamada birlesme, sozlukte istisna
import java.util.*;
import java.util.stream.*;

public class Yinelenen {
    public static void main(String[] args) {
        List<String> kelimeler = List.of("armut", "elma", "kiraz", "uzum", "nar");

        Map<Integer, List<String>> gruplanmis = kelimeler.stream()
                .collect(Collectors.groupingBy(String::length));
        System.out.println("gruplamanin sozluk tipi: " + (gruplanmis instanceof HashMap ? "HashMap" : "baska"));
        System.out.println("uzunluk 4 grubu     : " + gruplanmis.get(4));
        System.out.println("uzunluk 5 grubu     : " + gruplanmis.get(5));

        try {
            Map<Integer, String> sozluk = kelimeler.stream()
                    .collect(Collectors.toMap(String::length, s -> s));
            System.out.println("toMap yinelenen anahtar: dustu, boyut=" + sozluk.size());
        } catch (IllegalStateException e) {
            System.out.println("toMap yinelenen anahtar -> istisna: " + e.getClass().getSimpleName());
        }

        Map<Integer, String> birlesenSozluk = kelimeler.stream()
                .collect(Collectors.toMap(String::length, s -> s, (a, b) -> a + "+" + b));
        System.out.println("birlestirici verilince: " + birlesenSozluk.get(4));

        Map<Integer, String> siraliSozluk = kelimeler.stream()
                .collect(Collectors.toMap(String::length, s -> s, (a, b) -> a + "+" + b, TreeMap::new));
        System.out.println("uretici verilince tip : " + (siraliSozluk instanceof TreeMap ? "TreeMap" : "baska"));
    }
}
gruplamanin sozluk tipi: HashMap
uzunluk 4 grubu     : [elma, uzum]
uzunluk 5 grubu     : [armut, kiraz]
toMap yinelenen anahtar -> istisna: IllegalStateException
birlestirici verilince: elma+uzum
uretici verilince tip : TreeMap

İlk satır bir önceki bölümün gözlemini groupingBy için tekrarlıyor: groupingBy’ın belgesi “bir Map döner” der, hangi sınıf olduğunu söylemez; bugünkü gerçekleştirim bir HashMap veriyor, ama bu da toList()’teki gibi bir gerçekleştirim ayrıntısıdır, sözün kendisi değil. groupingBy(String::length) beş sözcüğü iki gruba ayırıyor ve hiçbir öğe kaybolmuyor — uzunluk 4 grubunda iki sözcük, uzunluk 5 grubunda iki sözcük var. Yinelenen anahtar burada bir hata değil, beklenen durumdur: gruplama zaten her anahtarın birden çok değeri taşıyabileceğini varsayar ve değerleri bir listede biriktirir.

toMap(String::length, s -> s) aynı girdiyle çağrıldığında farklı davranıyor: her anahtarın tek bir değere karşılık geleceğini varsayar, ikinci bir değer geldiğinde ne yapacağını bilmez ve IllegalStateException fırlatır. Üçüncü satır bu belirsizliği kapatan yolu gösteriyor: dört bağımsız argümanlı toMap bir birleştirici (merge) fonksiyonu alır ve çakışan iki değeri "elma+uzum" gibi tek bir değerde birleştirir — artık istisna yok.

Son satır sınırlayıcı ölçümdür ve dersin ikinci bulgusuyla birleşiyor. Beş bağımsız argümanlı toMap bir de kap üreticisi alır (TreeMap::new); üretici verildiğinde sonuç kabının tipi artık belirsiz değildir, tam olarak istenen sınıftır ve bu, koşumda instanceof ile doğrulanan kesin bir sonuçtur. Yani sonuç kabının tipiyle ilgili belirsizlik kalıcı değildir — çağıran üreticiyi kendisi verdiğinde, gerçekleştirime bırakılmış bir söz yeniden çağıranın eline geçer ve gözlem arayüz garantisi kadar kesin hâle gelir.

toMap’in dört ayrı biçimi bu üç ölçümü tek bir örüntüde topluyor — her ek argüman, toplayıcının dört parçasından birini çağırana geri veriyor:

Argüman sayısı Verilen parçalar Yinelenen anahtarda davranış
2 (anahtar, değer) birikeç kütüphanenin varsayılanı IllegalStateException
3 (+ birleştirici) birikeç çağıranın kendi kuralıyla çakışan değerler birleşir
4 (+ kap üreticisi) üretici de çağıranındır birleşir, kabın tipi de kesindir

İki argümanlı biçim üreticiyi ve birleştiriciyi kütüphanenin kendi varsayılanına bırakır; varsayılan birleştirici yoktur, bu yüzden çakışma anında toplayıcının yapacağı tek şey durmaktır. Üç ve dört argümanlı biçimler bu iki parçayı sırayla çağırana geri veriyor – toplayıcının “dört parça” yapısı burada soyut bir açıklama değil, doğrudan yöntem imzasında görünen bir gerçektir. Birikeç her zaman dolaylı olarak sabittir: anahtar ve değer işlevlerinden kurulur, ayrıca yazılmaz. Bitirici de dört biçimde de aynıdır — kabın kendisi zaten sonuç olduğu için ek bir dönüşüme ihtiyaç yoktur. Değişen yalnız üretici ve birleştiricidir, ve tam da bu ikisi çağırana bırakılabilir çünkü ikisi de “hangi kap” sorusuna karşılık gelir; “hangi anahtar, hangi değer” sorusu zaten ilk iki argümanla yanıtlanmıştır.

Özet

  • Bir toplayıcı dört parçadan kurulur: üretici (boş kabı kurar), birikeç (her öğeyi kaba işler), birleştirici (iki kısmi kabı birleştirir), bitirici (kabı son sonuca çevirir).
  • Collectors.toList()’in döndürdüğü listenin değiştirilebilir olması bir gerçekleştirim ayrıntısıdır, belgede garanti edilmez; toUnmodifiableList() değiştirilemezliği açıkça garanti eder ve ihlali UnsupportedOperationException ile düşer.
  • groupingBy yinelenen anahtarla karşılaştığında değerleri bir listede biriktirir, hiçbir öğe kaybolmaz; iki argümanlı toMap aynı durumda IllegalStateException fırlatır.
  • Bir birleştirici fonksiyon verildiğinde toMap çakışan değerleri birleştirir; istisna ortadan kalkar.
  • Sınırlayıcı ölçüm: çağıran açık bir kap üreticisi verdiğinde (TreeMap::new gibi) sonuç kabının tipiyle ilgili belirsizlik kapanır — gerçekleştirime bırakılmış bir söz, çağıran tarafından geri talep edilebilir.
  • toMap’in dört biçimi bu dört parçayı doğrudan yöntem imzasında gösterir: iki argümanlı biçim üretici ve birleştiriciyi kütüphaneye bırakır, dört argümanlı biçim ikisini de çağırana devreder.

Sonraki Adım

Bu ders bir akış zincirinin sonunda biriken kabın hangi sözlerle kurulduğunu ölçtü: dört parça, sonuç kabının belgede söylenmeyen tipi, yinelenen anahtarda istisna ile birleşme arasındaki ayrım ve bu ayrımın açık bir kap üreticisiyle nasıl kapatılabildiği. Toplama her zaman bir değer üretir — boş bir listeye toplamak boş bir liste verir, boş bir akışı saymak sıfır verir. Ama tek bir öğe bulmak istendiğinde, kaynakta o öğe hiç yoksa ne olur? Standart kütüphanenin bazı yöntemleri bu durumda boş değer döndürür, bazıları bir istisna fırlatır. Sıradaki ders yokluğun tiple ifade edildiği bir tipi ele alır: yokluk tipe taşındığında çağıranın yükümlülüğü gerçekten derleme zamanına mı geçiyor, yoksa denetim yine çalışma zamanında mı kalıyor?

İ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