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.ofile tek tek verilir ve bir akış zincirinde kullanılır. Kap burada bir dizgi değil, tek gözlü birint[]— 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()vetoUnmodifiableList(). İ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.
- FN25 —
Collectors.toList()’in ürettiği kabın üstüneCollections::unmodifiableListbir ikinci bitirici olarak eklenir. AyrıcaCollectors.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 ihlaliUnsupportedOperationExceptionile düşer.groupingByyinelenen anahtarla karşılaştığında değerleri bir listede biriktirir, hiçbir öğe kaybolmaz; iki argümanlıtoMapaynı durumdaIllegalStateExceptionfı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::newgibi) 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.