---
title: Toplayıcılar
source: 'https://academia.sh/tr/kurslar/java-standart-kutuphane/toplayicilar'
course: 'Standart Kütüphane ve Akışlar'
language: tr
updated: '2026-08-17T18:09:42+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

Ö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.

```java
// 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.

```java
// 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.

- **FN25** — `Collectors.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.

```java
// 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.

```java
// 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?
