---
title: 'Standart Kütüphane ve Akışlar'
source: 'https://academia.sh/tr/kurslar/java-standart-kutuphane'
language: tr
updated: '2026-08-17T04:41:23+00:00'
license: 'CC BY-SA 4.0'
lessons: 15
outcomes:
    - 'Koleksiyon çerçevesini uygun veri yapısı seçimiyle kullanır'
    - "Akış API'siyle bildirimsel veri dönüşümü yazar"
    - "Girdi/çıktı ve tarih–zaman API'lerini doğru kullanır"
---

# Standart Kütüphane ve Akışlar

Standart kütüphaneyi kullanmak, ne söz verildiğini bilmektir: kurs her davranışı tek bir soruyla ölçer — bu garantiyi arayüz mü veriyor, yalnız bu gerçekleştirim mi yapıyor, yoksa çağıranın uyduğu bir kurala mı bağlı? Koleksiyonlar, akış API'si, girdi/çıktı ve tarih–zaman aynı gözlem çerçevesinden geçirilir ve garantiye yaslanmayan her kullanımın nerede kırıldığı sayılır.

## Koleksiyonlar

Koleksiyon çerçevesinin arayüz hiyerarşisi ve gerçekleştirimleri, liste, küme ve kuyruk seçiminin dayandığı garantiler, karma ve sıralı sözlüklerin ayrıldığı yer, yineleyicinin gezinme sırasında değişikliğe verdiği yanıt, doğal sıra ile karşılaştırıcının kapsam farkı.

### Koleksiyon Çerçevesi

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/koleksiyon-cercevesi

Aynı üç gerçekleştirim dört ayrı aile için yan yana koşturuluyor ve on beş gözlemin yedisi arayüz garantisi, sekizi yalnız seçilen sınıfın gerçekleştirim davranışı çıkıyor. Sınırlayıcı ölçüm bu ayrımın kendisini sınırlıyor: dört ögeli bir kaynakta PriorityQueue'nun gezinme sırası çıkış sırasıyla rastlantı eseri eşleşiyor, beşinci öge eklenince ayrım yeniden görünür oluyor.

Nesneye Dayalı Java kursu boyunca bir kararı hep dil verdi. Erişim belirteci hangi üyenin
görüleceğini, `final` hangi değişkenin bir daha bağlanamayacağını, denetlenen istisna hangi
hatanın imzada duracağını derleyici belirledi. Kapanış dersi üç sözleşmenin — önceki durum,
sonraki durum, değişmez — hiçbirinin kusuru yok etmediğini, yalnız yerini değiştirdiğini
ölçtü; ama karar yine dilin içindeydi ve derleyici onu her seferinde kaynaktan okuyabiliyordu.

Bu ders yazılmış bir kütüphaneyi çağırmakla başlıyor ve karar artık dilin dışında. `List`,
`Set`, `Queue`, `Map` birer arayüzdür ve bir arayüz bir sözdür; ama o sözü kimin verdiği tek
bir yerden okunmaz. Gözlemlenen bir davranış üç kaynaktan birine ait olabilir — arayüzün
kendisine, seçilen sınıfa, ya da çağıranın uyduğu bir kurala — ve üçü çağrı yazımında hiçbir
şekilde ayırt edilemez. `ArrayList` ile `LinkedList` aynı `koy(0)` çağrısını kabul eder, aynı
`get(0)` çağrısını yanıtlar; hangi davranışın ikisinde de aynı kalacağı, hangisinin sınıf
değiştiğinde değişeceği kaynak metinden görünmez. Bu dersin ve kursun sorusu budur: elde
tutulan bir gözlemin garantisini kim veriyor.

#### Üç Gerçekleştirim, Tek Soru

Sorunun yanıtı tek bir gerçekleştirimle bulunamaz — tek sınıfı gözleyen biri onun
davranışını arayüzün sözüyle karıştırır. Ölçüm bu yüzden aynı arayüzü konuşan **üç**
gerçekleştirimi yan yana koyar ve üçüne aynı soruyu sorar: yanıt üçünde de aynıysa gözlem
arayüzden geliyordur, üçü ayrışıyorsa gözlem gerçekleştirime aittir.

- **KO1** — Aşağıdaki `Garanti` sınıfı hiçbir aileye özgü değildir. `sor` yalnızca topladığı
  yanıtları karşılaştırır; liste, küme, kuyruk ya da sözlük olması ölçütü değiştirmez.
- **KO2** — Bir gerçekleştirim çağrı sırasında istisna fırlatırsa yanıtı `"hata"` sayılır ve
  öteki iki yanıtla aynı sütunda karşılaştırılır; istisna ayrı bir dal açmaz.

```java
// Garanti.java — bir davranisin garantisini kim veriyor: arayuz mu, gerceklestirim mi
import java.util.*;
import java.util.function.*;

public class Garanti {
    static int arayuz = 0;
    static int gerceklestirim = 0;

    final List<String> adlar = new ArrayList<>();
    final List<Supplier<Object>> uretecler = new ArrayList<>();

    Garanti ekle(String ad, Supplier<Object> uretec) {
        adlar.add(ad);
        uretecler.add(uretec);
        return this;
    }

    void baslik(String aile) {
        StringBuilder sb = new StringBuilder(String.format("%-32s", aile));
        for (String ad : adlar) sb.append(String.format("%-15s", ad));
        sb.append("garantiyi veren");
        System.out.println(sb);
    }

    void sor(String soru, Function<Object, String> gozlem) {
        List<String> yanitlar = new ArrayList<>();
        for (Supplier<Object> u : uretecler) {
            String y;
            try {
                y = gozlem.apply(u.get());
            } catch (RuntimeException e) {
                y = "hata";
            }
            yanitlar.add(y);
        }
        boolean ortak = new HashSet<>(yanitlar).size() == 1;
        if (ortak) arayuz++; else gerceklestirim++;
        StringBuilder sb = new StringBuilder(String.format("%-32s", soru));
        for (String y : yanitlar) sb.append(String.format("%-15s", y));
        sb.append(ortak ? "arayuz" : "gerceklestirim");
        System.out.println(sb);
    }

    static void ozet() {
        System.out.printf("gozlem: %d arayuz garantisi, %d gerceklestirim davranisi%n",
                arayuz, gerceklestirim);
    }
}
```

`ekle` bir aileye bir gerçekleştirim ekler; her aile üç kez çağırır. `sor` bir soruyu üç
gerçekleştirime birden sorar ve tek bir sayaç işletir: yanıtlar kümesi tek elemana
düşüyorsa (`HashSet<>(yanitlar).size() == 1`) soru arayüz sütununa, düşmüyorsa gerçekleştirim
sütununa yazılır. Kural mekanik ve öznesizdir — hangi ailenin sorulduğunu bilmez, yalnız
yanıtları sayar.

#### Dört Aile, On Beş Gözlem

- **KO3** — Kaynak liste `KAYNAK` beş sabit ögedir ve dört aile boyunca değişmeden kalır;
  sözlük doldurulurken de aynı sırayla yazılır.
- **KO4** — Sayılan on beş satırın hepsi aynı programın tek koşumundan gelir; `ozet` bu
  koşumun toplamını, başka hiçbir satırı saymadan, verir.

```java
// Cerceve.java — dort ailenin gozlemlenebilir sozlerini ayirir
import java.util.*;

public class Cerceve {
    static final List<String> KAYNAK = List.of("zulu", "delta", "alfa", "carli", "bravo");

    static Map<String, Integer> doldur(Map<String, Integer> m) {
        for (int i = 0; i < KAYNAK.size(); i++) m.put(KAYNAK.get(i), i);
        return m;
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Garanti liste = new Garanti()
                .ekle("ArrayList", () -> new ArrayList<>(KAYNAK))
                .ekle("LinkedList", () -> new LinkedList<>(KAYNAK))
                .ekle("List.of", () -> List.copyOf(KAYNAK));
        liste.baslik("liste");
        liste.sor("ekleme sirasi korunuyor mu",
                o -> String.valueOf(new ArrayList<>((List<String>) o).equals(KAYNAK)));
        liste.sor("indeksle erisim var mi", o -> ((List<String>) o).get(1));
        liste.sor("esitlik icerige mi bakiyor",
                o -> String.valueOf(o.equals(new ArrayList<>(KAYNAK))));
        liste.sor("oge eklenebiliyor mu", o -> {
            try { ((List<String>) o).add("ekstra"); return "evet"; }
            catch (RuntimeException e) { return "hayir"; }
        });
        liste.sor("bos deger tutulabiliyor mu", o -> {
            try { ((List<String>) o).add(null); return "evet"; }
            catch (RuntimeException e) { return "hayir"; }
        });
        System.out.println();

        Garanti kume = new Garanti()
                .ekle("HashSet", () -> new HashSet<>(KAYNAK))
                .ekle("LinkedHashSet", () -> new LinkedHashSet<>(KAYNAK))
                .ekle("TreeSet", () -> new TreeSet<>(KAYNAK));
        kume.baslik("kume");
        kume.sor("yinelenen oge kabul ediliyor mu", o -> {
            Set<String> s = (Set<String>) o;
            s.add("alfa");
            return String.valueOf(s.size());
        });
        kume.sor("gezinme sirasi ekleme sirasi mi",
                o -> String.valueOf(new ArrayList<>((Set<String>) o).equals(KAYNAK)));
        kume.sor("bos deger kabul ediliyor mu", o -> {
            try { ((Set<String>) o).add(null); return "evet"; }
            catch (RuntimeException e) { return "hayir"; }
        });
        System.out.println();

        Garanti kuyruk = new Garanti()
                .ekle("ArrayDeque", () -> new ArrayDeque<>(KAYNAK))
                .ekle("LinkedList", () -> new LinkedList<>(KAYNAK))
                .ekle("PriorityQueue", () -> new PriorityQueue<>(KAYNAK));
        kuyruk.baslik("kuyruk");
        kuyruk.sor("bastan cikan oge", o -> ((Queue<String>) o).poll());
        kuyruk.sor("bosalinca ne donuyor", o -> {
            Queue<String> q = (Queue<String>) o;
            while (q.poll() != null) { }
            return String.valueOf(q.poll());
        });
        kuyruk.sor("gezinme sirasi cikis sirasi mi", o -> {
            Queue<String> q = (Queue<String>) o;
            List<String> gezinme = new ArrayList<>(q);
            List<String> cikis = new ArrayList<>();
            String s;
            while ((s = q.poll()) != null) cikis.add(s);
            return String.valueOf(gezinme.equals(cikis));
        });
        System.out.println();

        Garanti sozluk = new Garanti()
                .ekle("HashMap", () -> doldur(new HashMap<>()))
                .ekle("LinkedHashMap", () -> doldur(new LinkedHashMap<>()))
                .ekle("TreeMap", () -> doldur(new TreeMap<>()));
        sozluk.baslik("sozluk");
        sozluk.sor("anahtar tekil mi", o -> {
            Map<String, Integer> m = (Map<String, Integer>) o;
            m.put("alfa", 99);
            return String.valueOf(m.size());
        });
        sozluk.sor("anahtar gorunumu canli mi", o -> {
            Map<String, Integer> m = (Map<String, Integer>) o;
            Set<String> g = m.keySet();
            m.put("ekstra", 5);
            return String.valueOf(g.contains("ekstra"));
        });
        sozluk.sor("gezinme sirasi ekleme sirasi mi",
                o -> String.valueOf(new ArrayList<>(((Map<String, Integer>) o).keySet())
                        .equals(KAYNAK)));
        sozluk.sor("bos anahtar kabul ediliyor mu", o -> {
            try { ((Map<String, Integer>) o).put(null, 0); return "evet"; }
            catch (RuntimeException e) { return "hayir"; }
        });
        System.out.println();

        Garanti.ozet();
    }
}
```

```
liste                           ArrayList      LinkedList     List.of        garantiyi veren
ekleme sirasi korunuyor mu      true           true           true           arayuz
indeksle erisim var mi          delta          delta          delta          arayuz
esitlik icerige mi bakiyor      true           true           true           arayuz
oge eklenebiliyor mu            evet           evet           hayir          gerceklestirim
bos deger tutulabiliyor mu      evet           evet           hayir          gerceklestirim

kume                            HashSet        LinkedHashSet  TreeSet        garantiyi veren
yinelenen oge kabul ediliyor mu 5              5              5              arayuz
gezinme sirasi ekleme sirasi mi false          true           false          gerceklestirim
bos deger kabul ediliyor mu     evet           evet           hayir          gerceklestirim

kuyruk                          ArrayDeque     LinkedList     PriorityQueue  garantiyi veren
bastan cikan oge                zulu           zulu           alfa           gerceklestirim
bosalinca ne donuyor            null           null           null           arayuz
gezinme sirasi cikis sirasi mi  true           true           false          gerceklestirim

sozluk                          HashMap        LinkedHashMap  TreeMap        garantiyi veren
anahtar tekil mi                5              5              5              arayuz
anahtar gorunumu canli mi       true           true           true           arayuz
gezinme sirasi ekleme sirasi mi false          true           false          gerceklestirim
bos anahtar kabul ediliyor mu   evet           evet           hayir          gerceklestirim

gozlem: 7 arayuz garantisi, 8 gerceklestirim davranisi
```

On beş satırın **yedisi** arayüz sütununda, **sekizi** gerçekleştirim sütununda topladı.
Ayrım rastgele değildir ve tabloyu ikiye bölen bir çizgi çizilebilir: **ne tutulduğu**
arayüzde durur, **nasıl ve hangi sırayla tutulduğu** gerçekleştirime bırakılmıştır. Bir
listenin ekleme sırasını koruyacağı, içindekini indeksle vereceği, eşitliğinin içeriğe
bakacağı — üçü de üç sınıfta aynı. Aynı ögenin ikinci kez eklenmesinin kümeyi büyütmeyeceği,
boş bir kuyruktan çekmenin `null` vereceği — bunlar da öyle. Buna karşılık bir kümenin veya
sözlüğün hangi sırayla gezileceği, bir kuyruğun başından hangi ögenin çıkacağı, bir kabın
boş değeri kabul edip etmeyeceği satır satır ayrışıyor. Tek bir sınıfı — yalnızca
`HashMap`'i, yalnızca `PriorityQueue`'yu — gözleyerek çıkarılan bir davranış listesinin
**çoğunluğu**, arayüzün verdiği söz değildir.

#### İsteğe Bağlı İşlem: Arayüzün Kendi Çekincesi

Tablodaki iki satır ayrı bir okumayı hak ediyor: "öge eklenebiliyor mu" ve "bos deger
tutulabiliyor mu" ikisi de `gerceklestirim` diye işaretlendi, çünkü `List.of` ile üretilen
kap ikisini de reddediyor, `ArrayList` ile `LinkedList` ikisini de kabul ediyor. Ama
`Collection` arayüzünün kendi tanımı `add` yöntemini **isteğe bağlı işlem** (optional
operation) diye adlandırır: arayüz yöntemi tanımlar, aynı zamanda bir gerçekleştirimin onu
reddedebileceğini de söyler. Bu, garanti sorusunu ikiye ayırıyor — çağrının **başarılı
olup olmayacağı** ayrı bir sorudur, **başarısız olduğunda ne olacağı** ayrı bir sorudur.
Birincisinin yanıtı yukarıdaki tabloda gerçekleştirime bağlıydı; ikincisi aşağıda ölçülüyor.

- **KO5** — Dört kap türü aynı denemeyi paylaşır: tek bir öge eklemeyi dene, reddedilirse
  istisnanın sınıf adını yaz, kabul edilirse `"basarili"` yaz.

```java
// IsteyeBagli.java — reddeden gerceklestirimlerin hepsi ayni istisnayi mi atiyor
import java.util.*;

public class IsteyeBagli {
    static String dene(Runnable islem) {
        try { islem.run(); return "basarili"; }
        catch (RuntimeException e) { return e.getClass().getSimpleName(); }
    }

    public static void main(String[] args) {
        List<String> degisebilirListe = new ArrayList<>(List.of("a"));
        List<String> sabitListe = List.of("a");
        Set<String> sabitKume = Set.of("a");
        Map<String, Integer> sabitSozluk = Map.of("a", 1);

        System.out.println("ArrayList.add     : " + dene(() -> degisebilirListe.add("b")));
        System.out.println("List.of(...).add  : " + dene(() -> sabitListe.add("b")));
        System.out.println("Set.of(...).add   : " + dene(() -> sabitKume.add("b")));
        System.out.println("Map.of(...).put   : " + dene(() -> sabitSozluk.put("b", 2)));
    }
}
```

```
ArrayList.add     : basarili
List.of(...).add  : UnsupportedOperationException
Set.of(...).add   : UnsupportedOperationException
Map.of(...).put   : UnsupportedOperationException
```

Üç ayrı kap türü, üç ayrı arayüz, aynı reddi aynı sınıfla veriyor. Bu satır yukarıdaki
tabloya girseydi ortak yanıt yüzünden `arayuz` diye işaretlenirdi — oysa reddin **kendisi**
`ArrayList` için hiç gerçekleşmiyor, yalnız üç sabit kap için gerçekleşiyor. Çelişki
görünüşte kalıyor, çünkü iki ayrı soru karışıyor. "Ekleme kabul edilir mi" sorusunun
yanıtı gerçekleştirime bağlıdır ve önceki tabloda öyle sayıldı. "Kabul edilmezse hangi
istisna düşer" sorusunun yanıtı ise `Collection` arayüzünün javadoc'unda adı geçen tek bir
sınıfa bağlanmıştır ve üç arayüzün üçünde de değişmiyor. Arayüz burada bir davranışı garanti
etmiyor, bir **reddin biçimini** garanti ediyor: reddeden her gerçekleştirim aynı adla,
denetlenmeyen aynı istisna sınıfıyla reddediyor. Bu, "isteğe bağlı işlem" ifadesinin neden
"işlem yoktur" değil "işlem olabilir ya da adıyla düşer" anlamına geldiğini gösteriyor.

#### Sınırlayıcı Ölçüm: Birlik Zayıf Kanıttır

Çerçevenin kuralı basit bir çıkarımdır: üç yanıt aynıysa arayüz. Bu bir **çıkarımdır**,
belgeye bakmaz — ve çıkarımın kendisi yanılabilir. Kuyruk tablosundaki üçüncü satır
(`gezinme sirasi cikis sirasi mi`) `PriorityQueue` için `false` verdi; `ArrayDeque` ile
`LinkedList` içinse `true`. Bu satırın beş ögeli kaynakla ayrıştığı zaten görüldü. Aynı
soru dört ögeli, daha küçük bir kaynağa sorulursa ne olur.

- **KO6** — Dört ögeli kaynak, beş ögeli `KAYNAK`'ın ilk ögesi (`zulu`) çıkarılmış hâlidir;
  beşinci öge geri eklendiğinde kaynak yeniden birebir `KAYNAK`'a döner.

```java
// Rastlanti.java — gozlem birligi garanti kaniti degil
import java.util.*;

public class Rastlanti {
    static final List<String> KAYNAK = List.of("zulu", "delta", "alfa", "carli", "bravo");

    static boolean gezinmeCikisaEsitMi(List<String> kaynak) {
        PriorityQueue<String> pq = new PriorityQueue<>(kaynak);
        List<String> gezinme = new ArrayList<>(pq);
        List<String> cikis = new ArrayList<>();
        String s;
        while ((s = pq.poll()) != null) cikis.add(s);
        return gezinme.equals(cikis);
    }

    public static void main(String[] args) {
        List<String> dortOgeli = KAYNAK.subList(1, 5);
        System.out.println("dort ogeli kaynak              : " + dortOgeli);
        System.out.println("gezinme sirasi cikis sirasi mi : " + gezinmeCikisaEsitMi(dortOgeli));

        List<String> besOgeli = new ArrayList<>(dortOgeli);
        besOgeli.add(0, "zulu");
        System.out.println("besinci oge eklenince kaynak   : " + besOgeli);
        System.out.println("gezinme sirasi cikis sirasi mi : " + gezinmeCikisaEsitMi(besOgeli));
    }
}
```

```
dort ogeli kaynak              : [delta, alfa, carli, bravo]
gezinme sirasi cikis sirasi mi : true
besinci oge eklenince kaynak   : [zulu, delta, alfa, carli, bravo]
gezinme sirasi cikis sirasi mi : false
```

Dört ögelik kaynakla `PriorityQueue`'nun gezinme sırası çıkış sırasıyla **birebir eşleşiyor**
— çıkarımın kuralına göre bu bir arayüz garantisi olurdu. Ama beşinci öge eklenip kaynak
yeniden `KAYNAK`'ın kendisi olduğunda aynı soru `false` veriyor: kuyruk tablosunda zaten
görülen sonuç. Aradaki tek fark ögelerin kendisidir; kuyruk arayüzü gezinme sırası için hiçbir
zaman söz vermedi, ne dört ögede ne beşte. Dört ögelik durumda ortaya çıkan eşleşme kütüphanenin
verdiği bir söz değil, o beş ismin heap dizilimiyle rastlantı eseri örtüşmesiydi.

Ders boyunca kurulan ölçütü tersinden okumanın yolu budur: **birlik gözlemi garantiyi zayıf
biçimde destekler; ayrılık gözlemi garantisizliği kesin gösterir.** Üç gerçekleştirimin aynı
yanıtı vermesi bir arayüz sözü olabileceği gibi, veri kümesinin küçüklüğünden doğan bir
rastlantı da olabilir; bunu ayırt etmenin tek yolu veri kümesini değiştirip aynı soruyu
yeniden sormaktır. Ayrılığın kanıt gücü bundan yüksektir, çünkü hiçbir rastlantı üç ayrı
sınıfı üç ayrı yanıta zorlayamaz — ayrışan bir satır her zaman gerçek bir gerçekleştirim
farkını gösterir.

#### Özet

- Bir davranışın garantisini kimin verdiği çağrı yazımından görünmez; üç gerçekleştirimi
  yan yana koşturup yanıtları karşılaştırmak gerekir. Yanıt üçünde aynıysa gözlem arayüzden,
  ayrışıyorsa seçilen sınıftan geliyordur.
- Dört ailede (liste, küme, kuyruk, sözlük) sorulan on beş sorunun **yedisi** arayüz
  garantisi, **sekizi** gerçekleştirim davranışı çıktı. Ne tutulduğu arayüzde durur, nasıl
  ve hangi sırayla tutulduğu gerçekleştirime bırakılır.
- `Collection` arayüzü `add` gibi yöntemleri isteğe bağlı işlem diye tanımlar: kabul edilip
  edilmeyeceği gerçekleştirime bağlıdır, ama reddedildiğinde hangi istisnanın düşeceği üç
  ayrı arayüz boyunca aynı kalır — reddin kendisi değil, reddin **biçimi** arayüzün sözüdür.
- Sınırlayıcı ölçüm: dört ögeli bir kaynakta `PriorityQueue`'nun gezinme sırası çıkış
  sırasıyla rastlantı eseri eşleşir; beşinci öge eklendiğinde ayrım yeniden görünür olur.
  Gözlem birliği garanti kanıtı değildir; ayrılık gözlemi garantisizliği kesin gösterir.
- `Olcek` çekirdeği (sınıf dosyasından bilgi okuma aracı) bu derste kullanılmadı; ölçüm
  yalnız çalışma zamanı gözlemine dayanıyor, çünkü sorulan soru derleyicinin değil
  kütüphanenin verdiği sözle ilgili.

#### Sonraki Adım

Bu derste dört aile bir bütün olarak ölçüldü ve her birinin kaç sözünün arayüzden geldiği
sayıldı. Ama "liste", "küme" ve "kuyruk" ayrımının kendisi hiç sorgulanmadı — üçü de birer
koleksiyondur, üçü de aynı iş yükünü kabul edebilir. Sıradaki ders aynı veriyi üç arayüze
birden verip her birinin **hangi sözü verdiğini ve hangi bilgiyi sessizce düşürdüğünü**
ölçer; `LinkedList`'in hem liste hem kuyruk olmasının aynı çağrıya iki ayrı anlam
kazandırdığı da orada görülür.

### Liste, Küme ve Kuyruk

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/liste-kume-ve-kuyruk

Aynı altı ögelik iş yükü liste, küme ve kuyruk gerçekleştirimine birden verilip her birinin girdiden ne düşürdüğü sayılıyor: liste hiçbir şey düşürmüyor, küme üç bilgiyi düşürüyor, kuyruk yalnızca konum erişimini. Sınırlayıcı ölçüm LinkedList'in aynı anda hem liste hem kuyruk olmasından doğuyor: iki arayüz de bir remove sözü veriyor, sözler tek bir çağrı satırında çakışıyor ve birebir aynı yazılan remove(1) iki ayrı iş yapıyor — garantilerin ikisi de yerinde, boşluk çağıranın yazdığı satırda ve kırılması sessiz.

Önceki ders dört aileyi tek bir bütün olarak ölçtü ve on beş gözlemin kaçının arayüzden,
kaçının seçilen sınıftan geldiğini saydı. Ama liste, küme ve kuyruk arasındaki seçimin
kendisi hiç sorgulanmadı — üçü de birer koleksiyondur, üçü de aynı `add` çağrısını kabul
eder. Bu ders o seçimi açar: aynı veri üçüne birden verildiğinde her biri neyi tutuyor,
neyi sessizce bırakıyor? Yanıt, hiçbirinin ötekinden "daha iyi" olmadığını, üçünün de
farklı bir soruya yanıt vermek üzere tasarlandığını gösterecek.

Veri Yapıları kursu liste, yığıt, kuyruk ve kümeyi soyut veri tipi olarak kurdu ve
gerçekleştirim seçeneklerini karmaşıklıkla karşılaştırdı; o karşılaştırma burada
tekrarlanmıyor. Buradaki soru büyüklük değil sözdür: bir liste seçmekle bir küme seçmek
arasındaki fark, işlemlerin hızından önce, hangi bilginin **saklanacağının** taahhüdüdür.

Üç arayüzün adı bile bir vaat taşır: liste sıralı bir dizin çağrıştırır, küme matematikteki
kümenin tekillik özelliğini, kuyruk fiziksel bir sıranın iki ucunu. Bu ders adların
çağrıştırdığı sezgiyi ölçülebilir bir sayıya indiriyor: aynı girdi karşısında her arayüz
kaç bilgiyi koruyor, kaçını feda ediyor — ve feda edilenin rastlantı mı, arayüzün kendi
sözü mü olduğu ayrı ayrı sınanıyor.

#### Aynı İş Yükü, Üç Söz

- **KO7** — İş yükü altı ögelik sabit bir listedir ve içinde iki yinelenen değer vardır
  (`elma` ve `armut` ikişer kez geçiyor); üç aile de aynı listeden, aynı sırayla doldurulur.
- **KO8** — Ölçülen üç bilgi birbirinden bağımsızdır: yinelenen ögenin tutulup
  tutulmadığı, ekleme sırasının korunup korunmadığı, konuma (indekse) doğrudan erişilip
  erişilmediği. Bir ailenin kaç bilgiyi düşürdüğü bu üçünün toplamıdır.

Her aileden yalnız bir gerçekleştirim seçildi (`ArrayList`, `HashSet`, `ArrayDeque`) —
önceki dersteki gibi üç gerçekleştirimi karşılaştırıp arayüz ile gerçekleştirimi ayırmak bu
derste tekrarlanmıyor, çünkü soru burada başka: aynı gerçekleştirim seçiliyken üç arayüz
birbirinden ne kadar ayrışıyor. `HashSet` bilinçli seçildi, çünkü onun gezinme sırası
girdiyle örtüşmez; sırayı koruyan bir küme gerçekleştirimi seçilseydi bu bölümdeki ölçüm
gizlenirdi.

```java
// UcSoz.java — ayni is yukunu uc arayuze verip hangi bilginin dustugunu sayar
import java.util.*;

public class UcSoz {
    static final List<String> ISYUKU = List.of("elma", "armut", "elma", "kiraz", "armut", "erik");

    static void olc(String aile, Collection<String> kap) {
        boolean yinelemeTutuluyor = kap.size() == ISYUKU.size();
        boolean siraTutuluyor = new ArrayList<>(kap).equals(ISYUKU);
        boolean indeksErisimVar = kap instanceof List;
        int dusen = (yinelemeTutuluyor ? 0 : 1) + (siraTutuluyor ? 0 : 1) + (indeksErisimVar ? 0 : 1);
        System.out.printf("%-8s boyut=%-3d yineleme=%-6s sira=%-6s indeks=%-6s dusen=%d%n",
                aile, kap.size(), yinelemeTutuluyor, siraTutuluyor, indeksErisimVar, dusen);
    }

    public static void main(String[] args) {
        olc("liste", new ArrayList<>(ISYUKU));
        olc("kume", new HashSet<>(ISYUKU));
        olc("kuyruk", new ArrayDeque<>(ISYUKU));
    }
}
```

```
liste    boyut=6   yineleme=true   sira=true   indeks=true   dusen=0
kume     boyut=4   yineleme=false  sira=false  indeks=false  dusen=3
kuyruk   boyut=6   yineleme=true   sira=true   indeks=false  dusen=1
```

Üç satır üç ayrı taahhüt anlatıyor. `ArrayList` girdiden **hiçbir şey düşürmüyor**: altı
öge de kalıyor, sırası korunuyor, herhangi bir konuma `get` ile doğrudan gidilebiliyor.
`HashSet` girdinin **üçünü birden** düşürüyor: altı ögelik girdi dört ögeye iniyor (iki
yinelenen atıldı), kalan dördün gezinme sırası girdiyle örtüşmüyor, konum erişimi hiç yok.
`ArrayDeque` ise yalnızca **birini** düşürüyor: yineleneni de tutuyor, sırayı da koruyor,
düşürdüğü tek şey konum erişimi.

#### Söz Karmaşıklıktan Değil Sözden Gelir

Kümenin düşürdüğü üç bilgi bir kusur değil, tekilliğin bedelidir: bir kap her ögeyi bir
kez tutacağını vaat ediyorsa, ikinci `elma` girdiği anda ya reddedilir ya da birinciyle
birleşir — ikisi de aynı sonucu, boyutun küçülmesini, doğurur. Kümenin sözü tektir:
**tekillik**. Sıranın ne olacağını söylemez, çünkü tekilliği sağlamanın hangi iç yapıyla
yapıldığı (karma tablosu, dengeli ağaç, ekleme sıralı bir bağlı liste) sıraya karışır ve
arayüz bu iç yapıyı seçmez — önceki derste `HashSet`, `LinkedHashSet` ve `TreeSet`'in aynı
soruya üç ayrı yanıt vermesi tam olarak bunun ölçümüydü.

Kümenin düşürdüğü sıra bilgisi, tekilliğin aksine, arayüzün **zorunlu kıldığı** bir kayıp
değildir — bunu doğrudan ölçmek mümkün. Aynı iş yükü iki küme gerçekleştirimine verilip
sonucun ilk-görülen sırayla (yinelenenler atıldıktan sonra kalan dört ögenin girdideki ilk
geçiş sırası) örtüşüp örtüşmediğine bakılır.

```java
// Tekil.java — kumenin dusurdugu sira arayuzun zorunlu kildigi bir sey mi
import java.util.*;

public class Tekil {
    static final List<String> ISYUKU = List.of("elma", "armut", "elma", "kiraz", "armut", "erik");
    static final List<String> ILKGORULEN = List.of("elma", "armut", "kiraz", "erik");

    static boolean siraIlkGorulenleAyniMi(Collection<String> kap) {
        return new ArrayList<>(kap).equals(ILKGORULEN);
    }

    public static void main(String[] args) {
        Set<String> hash = new HashSet<>(ISYUKU);
        Set<String> baglantili = new LinkedHashSet<>(ISYUKU);
        System.out.println("HashSet gezinme sirasi                  : " + hash);
        System.out.println("HashSet sira ilk-gorulenle ayni mi       : " + siraIlkGorulenleAyniMi(hash));
        System.out.println("LinkedHashSet gezinme sirasi             : " + baglantili);
        System.out.println("LinkedHashSet sira ilk-gorulenle ayni mi : " + siraIlkGorulenleAyniMi(baglantili));
    }
}
```

```
HashSet gezinme sirasi                  : [erik, elma, armut, kiraz]
HashSet sira ilk-gorulenle ayni mi       : false
LinkedHashSet gezinme sirasi             : [elma, armut, kiraz, erik]
LinkedHashSet sira ilk-gorulenle ayni mi : true
```

`LinkedHashSet` aynı tekillik sözünü tutarken sırayı da koruyor; `HashSet` korumuyor. Demek
ki kümenin sırayı düşürmesi arayüzün dayattığı bir şey değil, seçilen sınıfın bir tercihidir
— tıpkı önceki derste dört ailede ölçülen "gezinme sirasi ekleme sirasi mi" satırlarının
`gerceklestirim` diye işaretlenmesi gibi. Arayüzün zorunlu kıldığı tek şey tekilliktir; sıra,
konum erişimi gibi, sınıftan sınıfa değişebilen bir eklentidir.

Kuyruğun sözü de tektir ve kümeninkinden farklıdır: **uç**. `Queue` arayüzü `offer`,
`poll`, `peek` üçlüsüyle "bir uçtan eklenir, bir uçtan çıkarılır ve o uç sabittir" der;
hangi ögenin o uçta bekleyeceğini söylemez. `ArrayDeque` bu iş yükünde ekleme sırasını
tesadüfen değil, kendi gerçekleştirim tercihiyle (uçtan uca bir dizi) koruyor; önceki derste
aynı arayüzü konuşan `PriorityQueue`'nun aynı soruya `false` yanıtı vermesi, sıranın
kuyruğun sözü olmadığını zaten gösterdi. Kuyruğun burada kaybettiği tek bilgi konum
erişimidir, çünkü uç sözü veren bir kap ortadaki bir ögeye adres vermez — adres vermek
listenin işidir.

Liste ise iki sözü **birden** taşır: **sıra ve yineleme**. Bir listeye eklenen her öge,
eşiti daha önce eklenmiş olsa bile, kendi konumunda kalır; konumların hepsi `get` ile
adreslenebilir. Bu iki söz kümenin ve kuyruğun tek tek verdiği sözlerin toplamı değildir —
liste tekillik vaat etmez (iki `elma` da kalır), kuyruk gibi tek bir uçla sınırlı da
değildir (herhangi bir konumdan okunabilir). Üç arayüz üç ayrı ihtiyaca karşılık gelir ve
seçim, hangi bilginin **tutulmasının** gerektiğine bakılarak yapılır. Bu iki sözü aynı anda
taşımanın bir bedeli de vardır, ama o bedel bu derste ölçülmüyor: ortadaki bir konuma ekleme
ya da bir değerin var olup olmadığını arama, gerçekleştirime göre değişen bir maliyet taşır
ve bu maliyet Veri Yapıları kursunda zaten sayılmıştı. Burada ölçülen yalnızca hangi bilginin
**kalıp kalmadığı**; ne kadar sürede kaldığı ayrı bir eksendir.

Önceki dersteki isteğe bağlı işlem gözlemi burada bir kez daha karşımıza çıkıyor. `Set.of`
ile üretilen sabit bir küme de, `List.of` ile üretilen sabit bir liste de ekleme
girişimini aynı istisnayla reddediyordu; bu ders reddin **biçimini** değil, reddedilmeyen
durumda hangi bilginin kaldığını ölçüyor. İki ölçüm birbirini tamamlıyor: biri "ekleme
kabul edilirse ne olur" sorusuna, öteki "ekleme reddedilirse ne görürüz" sorusuna yanıt
veriyor. İkisi de aynı sonuca çıkıyor: bir kabın davranışını anlamak için tek bir çağrının
yazımı yetmiyor, hangi arayüzün hangi sözü verdiğini ayrı ayrı bilmek gerekiyor.

#### Sınırlayıcı Ölçüm: Aynı Nesne, İki Anlam

`LinkedList` çerçevenin tek çift kimlikli sınıfıdır: hem `List` hem `Queue` (`Deque`
üzerinden) arayüzünü aynı anda gerçekleştirir. Yukarıdaki ölçümde `kuyruk` satırı
`ArrayDeque` ile temsil edildi ve `liste` satırı `ArrayList` ile; ikisi de tek bir arayüzün
sözünü taşıyan, saf sınıflardı. `LinkedList` saf değildir — aynı nesne, hangi referanstan
çağrıldığına bağlı olarak, hem listenin hem kuyruğun sözünü taşıyabilir ve bu iki söz
aynı yöntem adını paylaştığında hangisinin geçerli olacağı çağrı yazımından görünmez.
Nesneye Dayalı Java kursunda ölçülen kural burada aynen geçerlidir: bir çağrının hangi
yönteme bağlanacağını çalışma zamanı tipi değil, bildirilen tip belirler.

- **KO9** — Tek bir `LinkedList` nesnesi kurulur ve iki değişkene atanır: biri `List`,
  öteki `Queue` tipiyle bildirilmiştir. İkisi de aynı nesneyi gösterir; `==` ile
  doğrulanabilir ama ölçüm buna gerek duymaz, çünkü ikinci çağrının etkisi birincinin
  bıraktığı durumu değiştiriyor.

```java
// Ikilem.java — ayni nesne, iki arayuz, iki ayri remove yontemi
import java.util.*;

public class Ikilem {
    public static void main(String[] args) {
        LinkedList<Integer> gercek = new LinkedList<>(List.of(10, 20, 30, 40));
        List<Integer> listeRef = gercek;
        Queue<Integer> kuyrukRef = gercek;

        System.out.println("baslangic                     : " + gercek);
        boolean silindiMi = kuyrukRef.remove(1);
        System.out.println("kuyruk referansindan remove(1) -> donen=" + silindiMi
                + "  sonuc=" + gercek);

        Integer silinenDeger = listeRef.remove(1);
        System.out.println("liste referansindan remove(1) -> donen=" + silinenDeger
                + "  sonuc=" + gercek);
    }
}
```

```
baslangic                     : [10, 20, 30, 40]
kuyruk referansindan remove(1) -> donen=false  sonuc=[10, 20, 30, 40]
liste referansindan remove(1) -> donen=20  sonuc=[10, 30, 40]
```

Yazılan satır ikisinde de birebir aynıdır: `remove(1)`. Sonuç hiç aynı değildir. `Queue`
arayüzünde `remove(int)` diye bir yöntem yoktur; derleyici `1` argümanını `Integer`'a
kutulayıp `Collection`'dan gelen `remove(Object)`'e bağlar — kap içinde değeri `1` olan bir
öge arar, bulamaz, `false` döndürür ve **hiçbir şeyi değiştirmez**. `List` arayüzünde ise
`remove(int)` doğrudan tanımlıdır ve derleyici `1`'i bir indeks olarak okur; ikinci
konumdaki `20` silinir ve silinen değer geri döner. Aynı iki karakter — `(1)` — bir yerde
"değeri bul ve sil", ötekinde "ikinci konumu sil" anlamına geliyor.

**Burada kırılan hiçbir garanti yoktur.** İki arayüz de sözünü tam tutuyor: `Queue` bir
ögeyi değerine göre silmeyi, `List` bir ögeyi konumuna göre silmeyi vaat etmişti ve ikisi
de vaat ettiğini yaptı. Gerçekleştirim de suçsuzdur; nesne baştan sona aynı `LinkedList`
nesnesidir ve içinde hiçbir şey değişmedi. Boşluk **çağıranın yazdığı satırdadır**: iki
sözün aynı ada ve aynı yazıma düştüğü yerde hangisini istediğini söylemek çağıranın
yükümlülüğüdür.

Bu yükümlülük kırıldığında sonuç, kursun ölçtüğü üç sınıftan **sessiz** olanıdır. "İkinci
ögeyi sil" niyetiyle yazılan çağrı hiçbir istisna fırlatmaz, **hiçbir şey yapmaz** ve
`false` döner; dönüş değeri okunmazsa kusur fark edilmeden geçer. `LinkedList`'in iki
arayüzü birden konuşması burada bir kolaylık değil, bir tuzaktır — çünkü tek bir sınıf, iki
sözü aynı çağrı yazımında çakıştırabilen ender yerlerden biridir.

- **KO10** — Çağıranın söylemesi gereken şey yalnızca hangi arayüzden konuştuğu değil,
  argümanı hangi biçimde yazdığıdır. Aynı `List` üzerinden argüman açıkça `Integer` olarak
  kutulanırsa "değerle sil" sözü seçilir — ölçüm bunu tek bir referansla, ikinci bir
  değişken açmadan gösteriyor.

```java
// Cozum.java — karari veren referansin tipi degil, argumanin tipi
import java.util.*;

public class Cozum {
    public static void main(String[] args) {
        LinkedList<Integer> gercek = new LinkedList<>(List.of(10, 20, 30, 40));
        List<Integer> listeRef = gercek;

        boolean silindiMi = listeRef.remove(Integer.valueOf(1));
        System.out.println("liste referansi, remove(Integer.valueOf(1)) -> donen="
                + silindiMi + "  sonuc=" + gercek);

        Integer silinenDeger = listeRef.remove(1);
        System.out.println("liste referansi, remove(1) (int)            -> donen="
                + silinenDeger + "  sonuc=" + gercek);
    }
}
```

```
liste referansi, remove(Integer.valueOf(1)) -> donen=false  sonuc=[10, 20, 30, 40]
liste referansi, remove(1) (int)            -> donen=20  sonuc=[10, 30, 40]
```

Değişken aynı `listeRef`, konuşulan arayüz aynı `List<Integer>`. Yalnızca argümanın yazımı
değişti: birinci çağrıda `Integer.valueOf(1)` bir nesnedir ve "değerle sil" sözüne düşer —
kap içinde `1` değerine eşit bir öge aranır, bulunamaz, `false` döner. İkinci çağrıda `1`
bir `int` sabitidir ve "konumla sil" sözüne düşer. Demek ki yükümlülük arayüz seçimiyle
bitmiyor: çağıran, argümanı yazarken de hangi sözü istediğini söylemiş oluyor ve bunu
farkında olmadan söyleyebiliyor. **Nesneye Dayalı Java** kursu bu seçimin hangi tarafta
yapıldığını ölçmüştü; burada ölçülen şey seçimin tarafı değil, seçimi **çağırana bırakan
kütüphanenin** bunu hiçbir yerde sormamasıdır.

Bu tuzağın yalnız `LinkedList` üzerinde gösterilebilir olmasının nedeni onun iki arayüzü
birden konuşmasıdır. `ArrayList` yalnızca `List`'i, `ArrayDeque` yalnızca `Queue`'yu konuşur;
o sınıflarda `remove(1)` yazımının karşılığı tektir ve çakışma hiç doğmaz. `LinkedList` ise
iki sözü aynı nesnede toplar; bu yüzden ölçüm iki ayrı sınıf değil, **tek bir nesne**
üzerinde, yalnız iki değişkenle yapılabiliyor. "Konumla sil" yalnızca `List` tarafında
vardır, `Queue` tarafında hiç yoktur — ayrım nesnenin davranışından değil, iki arayüzün
verdiği sözlerin kümesinden doğuyor. Bu, kütüphanede birden çok arayüzü aynı anda
gerçekleştiren her sınıf için geçerli genel bir kuraldır; `LinkedList` bunu tek bir çağrıyla
göstermeye en yakın örnektir.

#### Özet

- Aynı altı ögelik girdi listeye, kümeye ve kuyruğa verildiğinde liste hiçbir bilgi
  düşürmüyor, küme üç bilgiyi (yineleme, sıra, konum erişimi) birden düşürüyor, kuyruk
  yalnızca konum erişimini düşürüyor.
- Üç arayüzün seçim ölçütü işlem hızı değil sözdür: liste **sıra ve yineleme**, küme
  **tekillik**, kuyruk **uç** vaat eder. Karmaşıklık karşılaştırması Veri Yapıları
  kursunda yapılmıştı ve burada tekrarlanmadı; burada ölçülen yalnızca hangi bilginin
  girdiden çıktığında hâlâ okunabilir olduğu.
- Kümenin düşürdüğü bilgi bir kusur değildir; tekillik sözünü tutmanın zorunlu bedelidir.
  Kuyruğun sırayı koruması ise arayüzün sözü değil, seçilen gerçekleştirimin tercihidir.
- `LinkedList` hem `List` hem `Queue` arayüzünü aynı anda taşır ve iki arayüzün `remove`
  sözü tek bir çağrı yazımında çakışır: birebir aynı yazılan `remove(1)` bir yerde konumla,
  bir yerde değerle siler.
- Çakışmada hiçbir garanti kırılmaz — iki arayüz de sözünü tutar, nesne değişmez. Kırılan
  şey **çağıranın yükümlülüğüdür** ve kırılma **sessizdir**: istisna düşmez, `false` döner
  ve dönüş değeri okunmazsa kusur fark edilmez.

#### Sonraki Adım

Bu derste liste, küme ve kuyruğun her biri, tuttuğu ögeye tek bir yoldan erişiyordu: konum,
üyelik ya da uç. Bu üç erişim yolunun ortak noktası, kabın kendi kendine yetmesiydi — ögenin
nerede olduğunu bulmak için kabın dışından hiçbir bilgi gerekmiyordu. Anahtar-değer
eşleşmesi bu üçünden farklı bir söz veriyor: bir ögeye anahtarıyla erişilir ve arayüz o
anahtarın karşılığını tekil tutacağını vaat eder. Ama anahtarın kendisinin ne olabileceğine
dair sözün yarısı kütüphanede değil, çağıranın yazdığı sınıfta durur — kap yalnız anahtarı
sorar, anahtarın kendi kendiyle tutarlı davranıp davranmayacağını denetlemez. Sıradaki ders
bu yarıyı ölçer: bir anahtar sınıfı yanlış yazıldığında arama nereye düşer, hangi istisna
fırlar ve fırlamıyorsa yerine ne olur.

### Sözlükler

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/sozlukler

Anahtar-değer sözlüğünün üç sözü — anahtarın tekilliği, anahtar görünümünün canlılığı, yok anahtarın boş değer vermesi — üç gerçekleştirimde de aynı çıkıyor ve arayüze ait oluyor. Gezinme sırası, boş anahtar kabulü ve anahtarın karşılaştırılabilir olma zorunluluğu ise karma ile sıralı gerçekleştirim arasında ayrışıyor. Çağıranın kuralı ilk kez merkeze alınıyor: equals yazıp hashCode yazmayan bir anahtarla arama hiçbir istisna vermeden boş değer döndürüyor, aynı anahtar sıralı bir sözlükte ise sessizce bulunuyor çünkü orada kullanılan söz compareTo.

Önceki ders liste, küme ve kuyruğun ögeye üç ayrı yoldan eriştiğini ölçtü: konum, üyelik,
uç. Üçü de kabın kendi kendine yettiği erişimlerdi — ögeyi bulmak için kabın dışından
hiçbir bilgi gerekmiyordu. Sözlük bunu bozuyor. Bir sözlükte ögeye anahtarıyla erişilir ve
anahtar çağıranın yazdığı, kütüphanenin hiç görmediği bir sınıf olabilir. Kap yalnız
anahtarı sorar; anahtarın kendi kendiyle tutarlı davranıp davranmadığını denetlemez. Bu
ders o tutarsızlığın nereye düştüğünü ölçüyor: hangi söz kütüphaneden geliyor, hangi söz
çağıranın yazdığı sınıfın omuzlarında duruyor ve ikisi arasında bir çatlak açıldığında
program bunu nasıl haber veriyor — ya da vermiyor.

Önceki iki dersin ölçtüğü her kusur derlenen bir programda anında görünürdü ya da hiç
görünmezdi — istisna fırlıyorsa fırlıyordu; fırlamıyorsa davranış tutarlıydı. Sözlükteki
anahtar kusuru üçüncü bir yol açıyor: program derlenir, çalışır, hiçbir istisna
fırlatmaz ve yine de yanlış çalışır. Bu üçüncü yol kütüphanenin bir eksikliği değil,
kütüphanenin çağırana bıraktığı bir yükümlülüğün karşılıksız kalmasıdır.

#### Anahtar-Değer Tutmanın Arayüz Sözü

Sözlük de önceki iki dersteki gibi üç gerçekleştirimle (`HashMap`, `LinkedHashMap`,
`TreeMap`) sınanır; ölçüm motoru aynıdır, yalnız bu derste kullanılan sorular yenidir.

- **KO11** — Motor önceki iki dersle **anlamca birebir**; yalnız bu derste sorulan
  sorular eklenmiştir. `Garanti` sınıfına yeni bir soru eklemek var olan soruların
  davranışını değiştirmez.
- **KO12** — Sözlük beş anahtarla, aynı sırayla, aynı `doldur` yardımcısıyla doldurulur.

```java
// Garanti.java — bir davranisin garantisini kim veriyor: arayuz mu, gerceklestirim mi
import java.util.*;
import java.util.function.*;

public class Garanti {
    static int arayuz = 0;
    static int gerceklestirim = 0;

    final List<String> adlar = new ArrayList<>();
    final List<Supplier<Object>> uretecler = new ArrayList<>();

    Garanti ekle(String ad, Supplier<Object> uretec) {
        adlar.add(ad);
        uretecler.add(uretec);
        return this;
    }

    void baslik(String aile) {
        StringBuilder sb = new StringBuilder(String.format("%-32s", aile));
        for (String ad : adlar) sb.append(String.format("%-15s", ad));
        sb.append("garantiyi veren");
        System.out.println(sb);
    }

    void sor(String soru, Function<Object, String> gozlem) {
        List<String> yanitlar = new ArrayList<>();
        for (Supplier<Object> u : uretecler) {
            String y;
            try {
                y = gozlem.apply(u.get());
            } catch (RuntimeException e) {
                y = "hata";
            }
            yanitlar.add(y);
        }
        boolean ortak = new HashSet<>(yanitlar).size() == 1;
        if (ortak) arayuz++; else gerceklestirim++;
        StringBuilder sb = new StringBuilder(String.format("%-32s", soru));
        for (String y : yanitlar) sb.append(String.format("%-15s", y));
        sb.append(ortak ? "arayuz" : "gerceklestirim");
        System.out.println(sb);
    }

    static void ozet() {
        System.out.printf("gozlem: %d arayuz garantisi, %d gerceklestirim davranisi%n",
                arayuz, gerceklestirim);
    }
}
```

```java
// Sozluk.java — anahtar-deger tutmanin arayuz sozu ile karma/sirali ayrimi
import java.util.*;

public class Sozluk {
    static final List<String> KAYNAK = List.of("zulu", "delta", "alfa", "carli", "bravo");

    static Map<String, Integer> doldur(Map<String, Integer> m) {
        for (int i = 0; i < KAYNAK.size(); i++) m.put(KAYNAK.get(i), i);
        return m;
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Garanti sozluk = new Garanti()
                .ekle("HashMap", () -> doldur(new HashMap<>()))
                .ekle("LinkedHashMap", () -> doldur(new LinkedHashMap<>()))
                .ekle("TreeMap", () -> doldur(new TreeMap<>()));
        sozluk.baslik("sozluk");
        sozluk.sor("anahtar tekil mi", o -> {
            Map<String, Integer> m = (Map<String, Integer>) o;
            m.put("alfa", 99);
            return String.valueOf(m.size());
        });
        sozluk.sor("anahtar gorunumu canli mi", o -> {
            Map<String, Integer> m = (Map<String, Integer>) o;
            Set<String> g = m.keySet();
            m.put("ekstra", 5);
            return String.valueOf(g.contains("ekstra"));
        });
        sozluk.sor("yok anahtarin yaniti ne", o -> String.valueOf(((Map<String, Integer>) o).get("yok")));
        sozluk.sor("gezinme sirasi ekleme sirasi mi",
                o -> String.valueOf(new ArrayList<>(((Map<String, Integer>) o).keySet())
                        .equals(KAYNAK)));
        sozluk.sor("bos anahtar kabul ediliyor mu", o -> {
            try { ((Map<String, Integer>) o).put(null, 0); return "evet"; }
            catch (RuntimeException e) { return "hayir"; }
        });
        System.out.println();
        Garanti.ozet();
    }
}
```

```
sozluk                          HashMap        LinkedHashMap  TreeMap        garantiyi veren
anahtar tekil mi                5              5              5              arayuz
anahtar gorunumu canli mi       true           true           true           arayuz
yok anahtarin yaniti ne         null           null           null           arayuz
gezinme sirasi ekleme sirasi mi false          true           false          gerceklestirim
bos anahtar kabul ediliyor mu   evet           evet           hayir          gerceklestirim

gozlem: 3 arayuz garantisi, 2 gerceklestirim davranisi
```

Üç satır arayüz sütununda topluyor. Bir anahtara ikinci kez yazmak boyutu büyütmüyor —
üçü de beş anahtarlı kalıyor, çünkü `Map` arayüzü anahtarın tekil olacağını doğrudan
tanımlıyor. `keySet()`'in döndürdüğü küme kabın kendisine **bağlı bir görünümdür**: sözlüğe
sonradan eklenen `"ekstra"` anahtarı, daha önce alınmış görünümde de beliriyor; görünüm bir
anlık kopya değil, kabın kendisine açılan bir penceredir. Üçüncü satır ise sözlüğün en
sık unutulan sözüdür: olmayan bir anahtarı aramak istisna fırlatmıyor, boş değer
döndürüyor. Üç ayrı gerçekleştirim de bu üç sözü aynen tutuyor; bir sözlük seçerken bu
üçü tartışmasız kabul edilebilir.

Bu üçüncü sözün kendi sınırı var ve onu görmek için tek bir satır yeterli. Sözlük boş
değeri **değer** olarak da kabul ediyorsa, `get`'in boş dönmesi iki ayrı durumu
ayırt edemez: anahtar hiç yok, ya da anahtar var ve karşılığı zaten boş değer.

```java
// Belirsizlik.java — get(anahtar) yokluk ile bos degeri ayirt edemiyor
import java.util.*;

public class Belirsizlik {
    public static void main(String[] args) {
        Map<String, Integer> m = new HashMap<>();
        m.put("alfa", null);
        System.out.println("get(alfa)         : " + m.get("alfa"));
        System.out.println("get(yok)          : " + m.get("yok"));
        System.out.println("containsKey(alfa) : " + m.containsKey("alfa"));
        System.out.println("containsKey(yok)  : " + m.containsKey("yok"));
    }
}
```

```
get(alfa)         : null
get(yok)          : null
containsKey(alfa) : true
containsKey(yok)  : false
```

İki `get` çağrısı da aynı yanıtı, boş değeri, veriyor; ama biri gerçekten yok olan bir
anahtarı, öteki var olan ve kasıtlı olarak boş değere bağlanmış bir anahtarı anlatıyor.
`get`'in tek başına yeterli olmadığı yer burası: yokluk sözünü tam okumak için `containsKey`
ayrı bir çağrı olarak gerekiyor. Arayüzün sözü "yok anahtar boş değer döndürür"dür; "boş
değer dönerse anahtar yoktur" değildir — ikisi aynı önerme değil.

#### Karma ile Sıralının Ayrıldığı Yer

Tablonun alt iki satırı `HashMap`/`LinkedHashMap` ile `TreeMap`'i ayırıyor. Gezinme sırası
`HashMap`'te anahtarların karma değerine, `LinkedHashMap`'te ekleme sırasına, `TreeMap`'te
anahtarların doğal sırasına bakıyor — üçü de arayüzün değil, seçilen sınıfın kararı.
Boş anahtar kabulü de aynı çizgide ayrışıyor: `HashMap` ve `LinkedHashMap` boş anahtarı
tek bir özel yuvada tutuyor, `TreeMap` reddediyor, çünkü bir anahtarı yerleştirmek için
onu var olan anahtarlarla **karşılaştırması** gerekiyor ve boş değerin karşılaştırılacak
bir sırası yok.

Bu son gözlem üçüncü bir ayrımı işaret ediyor: `TreeMap`'in anahtardan istediği şey
`HashMap`'in istediğinden farklı. `HashMap` bir anahtarı yalnız `equals` ve `hashCode` ile
tanır; `TreeMap` bir sıra bekler. Bu, arayüzün kendisinde görünmeyen bir farktır — ikisi de
`Map<K, V>` gerçekleştirir, ikisinin de imzasında `K` üzerine hiçbir ek kısıt yazmaz.
Fark yalnız çalışma zamanında, anahtar ilk kez yerleştirilmeye çalışıldığında ortaya
çıkar. Anahtar sınıfı sıralanabilir değilse ne olur?

- **KO13** — Aynı anahtar sınıfı `equals` ve `hashCode`'u eksiksiz yazar ama hiçbir
  sıralama bildirmez (`Comparable` gerçekleştirmez). Üç sözlüğe de aynı tek anahtarla tek
  bir `put` çağrısı yapılır.

```java
// Zorunluluk.java — anahtarin siralanabilir olmasi hangi gerceklestirimde zorunlu
import java.util.*;

public class Zorunluluk {
    static class KarsilastirilamazAnahtar {
        final String ad;
        KarsilastirilamazAnahtar(String ad) { this.ad = ad; }
        @Override public boolean equals(Object o) {
            return o instanceof KarsilastirilamazAnahtar a && a.ad.equals(ad);
        }
        @Override public int hashCode() { return ad.hashCode(); }
    }

    static String dene(Runnable islem) {
        try { islem.run(); return "basarili"; }
        catch (RuntimeException e) { return e.getClass().getSimpleName(); }
    }

    public static void main(String[] args) {
        Map<KarsilastirilamazAnahtar, Integer> hash = new HashMap<>();
        Map<KarsilastirilamazAnahtar, Integer> baglantili = new LinkedHashMap<>();
        Map<KarsilastirilamazAnahtar, Integer> siralanan = new TreeMap<>();

        System.out.println("HashMap.put       : " + dene(() -> hash.put(new KarsilastirilamazAnahtar("a"), 1)));
        System.out.println("LinkedHashMap.put : " + dene(() -> baglantili.put(new KarsilastirilamazAnahtar("a"), 1)));
        System.out.println("TreeMap.put       : " + dene(() -> siralanan.put(new KarsilastirilamazAnahtar("a"), 1)));
    }
}
```

```
HashMap.put       : basarili
LinkedHashMap.put : basarili
TreeMap.put       : ClassCastException
```

`equals` ve `hashCode` tam yazılmış, hiçbir eksik yok — ve yine de üçüncü satır çöküyor.
`HashMap` ile `LinkedHashMap` anahtarı yuvasına yerleştirmek için yalnız karma değerini ve
eşitliğini bilmesi gerekiyor; `TreeMap` ise ilk `put` çağrısında bile anahtarı ağaçtaki
konumuna yerleştirmek için onu bir şeyle **karşılaştırmak** zorunda ve elinde ne bir
`Comparable` ne bir `Comparator` var. Kusur derleme zamanında görünmüyor — `Map<K, V>`
tanımı `K` üzerine hiçbir sıralama kısıtı koymuyor — ve çalışma zamanında, ilk yazmada,
adıyla düşüyor.

#### Çağıranın Kuralı: equals Yazıp hashCode Yazmamak

Yukarıdaki iki bölüm arayüzün ve gerçekleştirimin sözlerini ayırdı. Geriye üçüncü kaynak
kalıyor: çağıranın kendi yazdığı anahtar sınıfının uyması gereken kural. `Object` sınıfı
bir sözleşme tanımlar — iki nesne `equals` ile eşit sayılıyorsa `hashCode`'ları da eşit
olmalıdır — ama derleyici bu sözleşmeyi hiçbir yerde denetlemez. `equals` yazılıp
`hashCode` yazılmadığında ne olur?

- **KO14** — Anahtar sınıfı `equals`'ı doğru yazar (adı karşılaştırır) ve `hashCode`'u
  **hiç yeniden tanımlamaz**; miras aldığı `Object.hashCode()` çalışma zamanı kimliğine
  bakar. İki ayrı `new` çağrısı aynı adı taşısa da iki ayrı karma değeri üretir.

```java
// EslesmeyenAnahtar.java — equals yazilmis, hashCode yazilmamis bir anahtarla arama
import java.util.*;

public class EslesmeyenAnahtar {
    static class Anahtar {
        final String ad;
        Anahtar(String ad) { this.ad = ad; }
        @Override public boolean equals(Object o) {
            return o instanceof Anahtar a && a.ad.equals(ad);
        }
        // hashCode kasitli olarak yazilmadi
    }

    public static void main(String[] args) {
        Map<Anahtar, Integer> depo = new HashMap<>();
        depo.put(new Anahtar("alfa"), 9);
        Integer bulunan = depo.get(new Anahtar("alfa"));
        String sonuc = String.valueOf(bulunan);
        System.out.printf("%-30s %-9s bulunan=%-6s beklenen=9%n", "hashCode yazilmamis anahtar",
                sonuc.equals("9") ? "basarili" : "sessiz", sonuc);
    }
}
```

```
hashCode yazilmamis anahtar    sessiz    bulunan=null   beklenen=9
```

Yazan da arayan da aynı adı, `"alfa"`'yı, kullanıyor; `equals` ikisini eşit sayardı. Ama
`HashMap` anahtarı önce hangi yuvaya bakacağına karar vermek için `hashCode`'a başvuruyor
ve iki ayrı `new Anahtar("alfa")` nesnesi, `Object`'ten miras kalan varsayılan `hashCode`
yüzünden, neredeyse kesinlikle iki ayrı yuvaya düşüyor. Arama, değerin durduğu yuvaya hiç
uğramıyor ve `equals` çağrılma fırsatı bile bulamıyor. Sonuç **sessiz**: hiçbir istisna
fırlamıyor, `get` yalnızca boş değer döndürüyor — sanki anahtar hiç eklenmemiş gibi.
Kusurun görüneceği yer yazma satırı değil, aylar sonra yazılmış bir arama satırı olabilir.

Bu üç sınıfa ayrılan sonuç — `istisna`, `sessiz`, `etkisiz` — kursun kendi
sınıflamasıdır ve burada ilk kez somut bir örnekle karşılanıyor. `hashCode` eksikliği
`istisna` sınıfına girmiyor: hiçbir satır çökmüyor, program baştan sona çalışıyor. Aynı
sözlük binlerce anahtarla dolu olsaydı, kusur yine görünmezdi; yalnızca yanlış bir sayıda
öge "kayıp" görünürdü ve bu kaybın nedeni günler sonra, kaynağa hiç dokunmadan bulunması
gereken bir hata olurdu.

#### Sınırlayıcı Ölçüm: Sıralı Sözlükte Aynı Anahtar Bulunuyor

Bir önceki ölçüm "bu anahtar sınıfı bozuk" sonucuna götürüyor gibi görünüyor. Sınırlayıcı
ölçüm bunu düzeltiyor: aynı sınıf, aynı eksik `hashCode`, tek fark gerçekleştirim seçimi.

- **KO15** — Anahtar sınıfı yukarıdakiyle **birebir aynıdır**; `hashCode` yine yazılmamıştır.
  Tek değişen, sözlüğün `TreeMap` olması ve bir `Comparator` ile kurulmasıdır.

```java
// SiraliDeBulunuyor.java — ayni kusurlu anahtar siralamaya dayanan sozlukte bulunuyor
import java.util.*;

public class SiraliDeBulunuyor {
    static class Anahtar {
        final String ad;
        Anahtar(String ad) { this.ad = ad; }
        @Override public boolean equals(Object o) {
            return o instanceof Anahtar a && a.ad.equals(ad);
        }
        // hashCode kasitli olarak yazilmadi
    }

    public static void main(String[] args) {
        Map<Anahtar, Integer> siralanan = new TreeMap<>(Comparator.comparing(a -> a.ad));
        siralanan.put(new Anahtar("alfa"), 9);
        Integer bulunan = siralanan.get(new Anahtar("alfa"));
        System.out.println("TreeMap.get, ayni kusurlu anahtar : " + bulunan);
    }
}
```

```
TreeMap.get, ayni kusurlu anahtar : 9
```

Aynı sınıf, aynı eksik yöntem ve arama bu kez **buluyor**. Fark anahtarda değil, anahtarı
okuyan gerçekleştirimde: `TreeMap` bir konum bulmak için hiçbir zaman `hashCode`'a
bakmıyor, yalnız verilen `Comparator`'ı çağırıyor ve o da yalnız `ad` alanını okuyor. Bu,
ilk ölçümün yanlış okunmasını engelliyor. Kusurlu olan anahtar sınıfının kendisi değildi;
kusur, bir sınıfın verdiği sözle (yalnız `equals`) seçilen gerçekleştirimin beklediği
sözün (`hashCode` ve `equals` birlikte) **eşleşmemesiydi**. Aynı anahtar, beklentisi farklı
bir gerçekleştirimin elinde, kusursuz çalışıyor.

Bu iki ölçüm yan yana konduğunda kursun sorusu tam olarak buraya geliyor. Bir kusuru
tarif ederken "bu sınıf bozuk" demek çoğu zaman yanlış sorudur; doğru soru "bu sınıf hangi
sözü veriyor ve onu okuyan taraf hangi sözü bekliyor" sorusudur. `HashMap` `hashCode`
bekler ve anahtar onu vermez — kusur orada. `TreeMap` yalnız bir sıra bekler ve anahtar,
dolaylı yoldan bir `Comparator` üzerinden, onu verir — kusur orada yok. Anahtar sınıfı
hiç değişmedi; değişen, anahtarın hangi sözleşmeye karşı sınandığıydı.

#### Özet

- Sözlüğün üç sözü — anahtarın tekilliği, anahtar görünümünün canlılığı, yok anahtarın
  boş değer vermesi — üç gerçekleştirimde de aynı çıktı ve arayüze aitti.
- Gezinme sırası ve boş anahtar kabulü karma ile sıralı gerçekleştirim arasında ayrıştı;
  ayrımın kökeninde `TreeMap`'in anahtarı **karşılaştırma** zorunluluğu var.
- Sıralanamayan bir anahtar `HashMap`'te sorunsuz çalışırken `TreeMap`'te ilk yazmada
  `ClassCastException` ile düşüyor — kusur derlemede değil, çalışma zamanında görünüyor.
- Çağıranın kuralı: `equals` yazılıp `hashCode` yazılmadığında arama hiçbir istisna
  vermeden boş değer döndürüyor; kusur yazma satırında değil, arama satırında ortaya
  çıkıyor.
- Sınırlayıcı ölçüm: aynı eksik anahtar sınıfı sıralı bir sözlükte sorunsuz bulunuyor,
  çünkü orada aranan söz `compareTo`/`Comparator`'dır. Kusur anahtarda değil, anahtarla
  gerçekleştirimin beklentisinin eşleşmemesindeydi.

#### Sonraki Adım

Bu derste bir sözlüğün anahtarı bulup bulamayacağı ölçüldü; peki kap **gezinirken**
değişirse ne olur? Bir sözlüğün ya da kümenin üzerinde `for` ile ilerlerken aynı kaba yeni
bir öge eklemek ya da bir ögeyi silmek, üç ailenin de karşılaştığı ortak bir durumdur.
Sıradaki ders bu durumu ölçer: bazı gerçekleştirimler hemen ve adıyla bir istisna fırlatır,
bazıları hiçbir şey söylemeden yanlış bir gezinti tamamlar — ve hangisinin olacağına yine
arayüz değil, gerçekleştirim karar verir. Bu derste görülen "kusur derlenir, çalışır ve
sessizce yanlış sonuç verir" örüntüsü orada bir kez daha, farklı bir sebeple karşımıza
çıkacak.

### Yineleyiciler ve Eşzamanlı Değişiklik

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/yineleyiciler-ve-eszamanli-degisiklik

Yineleyicinin temel sözü — tükendiğinde NoSuchElementException — üç ayrı gerçekleştirimde birebir aynı çıkıyor. Ama gezinme sırasında kap değişirse üç ayrı yanıt görülüyor: erken hata veren, anlık kopya üzerinden gezen, hiçbir şey söylemeyen. Erken hatanın kendisi de bir garanti değil bir en iyi çaba çıkıyor: aynı silme işlemi bir konumda istisna verirken sondan bir önceki ögede sessizce ve bir eksik biten bir döngü bırakıyor. Sınırlayıcı ölçüm, yineleyicinin kendi remove'unun bu kuralın dışında kaldığını gösteriyor.

Önceki ders bir sözlüğün anahtarı bulup bulamayacağını ölçtü ve kusurun kaynağını üçe
ayırdı: arayüz, gerçekleştirim, çağıranın kuralı. Bu ders aynı üçlüyü, kabın **durağan**
haliyle değil, üzerinde **gezinirken** sınıyor. Bir listeyi, kümeyi ya da sözlüğü `for`
ile dolaşırken aynı kaba yeni bir öge eklemek ya da bir ögeyi silmek — üç ailenin de
paylaştığı bir durumdur. Kütüphane buna tek bir yanıt vermiyor.

Gezinme her zaman bir sıra varsayar: yineleyici bir sonraki ögenin nerede olduğunu, bir
önceki çağrının bıraktığı iç durumdan çıkarır. Kap gezinme ortasında değişirse bu iç durum
ile kabın gerçek hali arasında bir uyuşmazlık doğar. Sorunun kendisi bütün gerçekleştirimler
için aynı; ölçülecek olan, her birinin bu uyuşmazlığı nasıl ele aldığı — sessizce mi
görmezden geldiği, adıyla mı durdurduğu, yoksa hiç oluşmayacak biçimde mi tasarlandığı.

#### Yineleyicinin Arayüz Sözü

Değişiklik konusuna girmeden önce yineleyicinin bozulmayan tarafı ölçülür. `Iterator`
arayüzü `hasNext` ve `next` çiftini tanımlar ve `next`'in tükenmiş bir yineleyicide ne
yapacağını da tanımlar.

- **KO16** — Üç kap da aynı üç ögeyle doldurulur ve tükenene kadar gezilir; ardından bir
  `next` çağrısı daha yapılır. Ölçülen tek şey bu son çağrının sonucudur.

```java
// TemelSoz.java — next tukendiginde uc gerceklestirim de ayni mi davraniyor
import java.util.*;

public class TemelSoz {
    static String dene(Iterable<String> kap) {
        Iterator<String> it = kap.iterator();
        while (it.hasNext()) it.next();
        try {
            it.next();
            return "next basarili (beklenmiyordu)";
        } catch (NoSuchElementException e) {
            return e.getClass().getSimpleName();
        }
    }

    public static void main(String[] args) {
        List<String> kaynak = List.of("a", "b", "c");
        System.out.println("ArrayList  : " + dene(new ArrayList<>(kaynak)));
        System.out.println("HashSet    : " + dene(new HashSet<>(kaynak)));
        System.out.println("ArrayDeque : " + dene(new ArrayDeque<>(kaynak)));
    }
}
```

```
ArrayList  : NoSuchElementException
HashSet    : NoSuchElementException
ArrayDeque : NoSuchElementException
```

Üç aile de aynı istisnayı, aynı adla veriyor. Bu, `Iterator` arayüzünün doğrudan
tanımladığı bir sözdür: `next` sözleşmesi tükenmiş bir yineleyicide bu istisnayı fırlatmayı
şart koşar. Kabın kendisi değişmediği sürece — yalnız gezinildiği sürece — üç gerçekleştirim
de birbirinden ayırt edilemez. Ayrım, gezinme sırasında kabın **kendisi** değiştiğinde
başlıyor.

Bu ölçüm önceki derslerdeki "yok anahtarın yanıtı" ölçümüyle aynı kalıptadır: kabın
**hiç değişmediği**, yalnız sorgulandığı durumda üç gerçekleştirim tek bir davranış
etrafında toplanıyor. Ayrım her zaman kabın durumu **değiştiğinde** ortaya çıkıyor —
önceki derste anahtar yazılırken, bu derste ise kap gezinirken değiştiğinde.

#### Üç Yanıt: Erken Hata, Anlık Kopya, Sessizlik

- **KO17** — Dört kap da aynı dört ögeyle doldurulur ve `for`-each ile gezilirken, ikinci
  öge işlendiği anda kaba **beşinci bir öge** eklenir. Ölçülen, bu eklemenin gezintiyi
  nasıl etkilediğidir: istisna mı, sessiz devam mı ve devam ediyorsa kaç öge görülüyor.
  Değişiklik dört kapta da **aynı adımda** yapılır; değişen yalnız kaptır.

```java
// UcYanit.java — gezinme sirasinda kap degisirse uc ayri yanit
import java.util.*;
import java.util.concurrent.*;

public class UcYanit {
    static String dene(Iterable<String> kap, Runnable degisiklik) {
        int sayilan = 0;
        try {
            for (String s : kap) {
                sayilan++;
                if (sayilan == 2) degisiklik.run();
            }
            return "istisna yok, sayilan=" + sayilan;
        } catch (ConcurrentModificationException e) {
            return e.getClass().getSimpleName() + ", sayilan=" + sayilan;
        }
    }

    public static void main(String[] args) {
        List<String> hataVerenListe = new ArrayList<>(List.of("a", "b", "c", "d"));
        System.out.println("ArrayList (erken hata)                    : "
                + dene(hataVerenListe, () -> hataVerenListe.add("ekstra")));

        Set<String> hataVerenKume = new HashSet<>(List.of("a", "b", "c", "d"));
        System.out.println("HashSet (erken hata)                      : "
                + dene(hataVerenKume, () -> hataVerenKume.add("ekstra")));

        List<String> kopyaUzerinden = new CopyOnWriteArrayList<>(List.of("a", "b", "c", "d"));
        System.out.println("CopyOnWriteArrayList (anlik kopya)        : "
                + dene(kopyaUzerinden, () -> kopyaUzerinden.add("ekstra")));

        Map<String, Integer> sessizKalan = new ConcurrentHashMap<>();
        sessizKalan.put("a", 1); sessizKalan.put("b", 2);
        sessizKalan.put("c", 3); sessizKalan.put("d", 4);
        System.out.println("ConcurrentHashMap (hicbir sey soylemeyen) : "
                + dene(sessizKalan.keySet(), () -> sessizKalan.put("ekstra", 5)));
    }
}
```

```
ArrayList (erken hata)                    : ConcurrentModificationException, sayilan=2
HashSet (erken hata)                      : ConcurrentModificationException, sayilan=2
CopyOnWriteArrayList (anlik kopya)        : istisna yok, sayilan=4
ConcurrentHashMap (hicbir sey soylemeyen) : istisna yok, sayilan=5
```

Dört satır üç ayrı sözü gösteriyor ve üçü de `Iterator` arayüzünün kendisinde yazmıyor —
`Iterator` gezinme sırasında değişikliğe ne olacağını hiç tanımlamıyor, tanımı seçilen
sınıfa bırakıyor. `ArrayList`'in ve `HashSet`'in yineleyicileri her `next` çağrısında
kabın yapısal olarak değişip değişmediğini denetliyor ve değiştiyse hemen, adıyla,
duruyor: ikinci ögeden sonra ekleme yapıldığı için üçüncü `next` çağrısı ikisinde de
düşüyor. İki ayrı veri yapısının (dizi tabanlı liste, karma tabanlı küme) aynı sayacı
tutması rastlantı değil — `AbstractList` ve `HashMap` (ki `HashSet` içten içe bir
`HashMap` sarar) aynı `modCount` düzeneğini paylaşıyor; erken hata tek bir sınıfın değil,
çerçevenin tasarım deseninin bir parçası.

`CopyOnWriteArrayList` ise gezinmeye başladığı anda dizinin bir kopyasını almış durumda;
kaba ne eklenirse eklensin, o kopyada dört öge var ve yineleyici dördünü de görüyor,
hiçbir şeyi fark etmeden. Dördüncü satır öncekilerin ikisinden de farklı:
`ConcurrentHashMap` ne istisna fırlatıyor ne sabit bir kopyada donuyor — bu koşumda
eklenen beşinci öge de gezintiye giriyor ve toplam beş öge sayılıyor, ama bu API'nin
verdiği bir söz değil, yalnız bu koşumun sonucu; kütüphane yalnızca istisna
**fırlatmayacağını** garanti ediyor, göreceği ögelerin hangileri olacağını garanti etmiyor.

#### Erken Hata Bir En İyi Çaba, Bir Garanti Değil

`ArrayList`'in "erken hata veren" davranışı bir sonraki ölçümde kendi sınırını gösteriyor.
Yineleyici her değişikliği yakalamıyor; yalnızca bir sayaç tutuyor ve bu sayacı yalnızca
`next` çağrıldığında denetliyor. Silinen öge listenin **sondan bir öncekiyse**, döngü zaten
bir daha `next` çağırmadan bitiyor ve denetim hiç çalışmıyor.

- **KO18** — Beş ögelik aynı liste iki kez gezilir; birinde ortadaki (2. indeks), ötekinde
  sondan bir önceki (3. indeks) öge, gezinti o ögeye ulaştığında silinir. Değişen tek şey
  silinen konumdur.

```java
// SessizBitis.java — erken hata her zaman calismiyor
import java.util.*;

public class SessizBitis {
    static void dene(String etiket, int silinecekIndeks) {
        List<String> liste = new ArrayList<>(List.of("a", "b", "c", "d", "e"));
        List<String> gezilen = new ArrayList<>();
        try {
            for (String s : liste) {
                gezilen.add(s);
                if (gezilen.size() - 1 == silinecekIndeks) {
                    liste.remove(silinecekIndeks);
                }
            }
            System.out.println(etiket + " -> istisna yok, gezilen=" + gezilen);
        } catch (ConcurrentModificationException e) {
            System.out.println(etiket + " -> " + e.getClass().getSimpleName()
                    + ", gezilen=" + gezilen);
        }
    }

    public static void main(String[] args) {
        dene("indeks 2 (ortadaki) silinirse", 2);
        dene("indeks 3 (sondan bir onceki) silinirse", 3);
    }
}
```

```
indeks 2 (ortadaki) silinirse -> ConcurrentModificationException, gezilen=[a, b, c]
indeks 3 (sondan bir onceki) silinirse -> istisna yok, gezilen=[a, b, c, d]
```

Aynı sınıf, aynı `for`-each, aynı tek silme — sonuç iki ayrı şey söylüyor. Ortadaki öge
silindiğinde beklenen olur: bir sonraki `next` çağrısı sayaç uyuşmazlığını yakalar ve
istisna düşer. Sondan bir önceki öge silindiğinde ise listenin boyutu beşten dörde iner ve
döngünün iç sayacı (`cursor`), bir sonraki ögeye gitmek yerine, artık listenin yeni
boyutuna eşitlenmiş olur; `hasNext` bunu "gezinti bitti" diye okur ve döngü **hiç bir
`next` çağırmadan** sona erer — son öge (`"e"`) hiç görülmeden. Hiçbir istisna yok, ama
gezinti de eksik: dört öge görüldü, beşincisi sessizce atlandı.

Bu, "erken hata" adının yanıltıcı yanı. `ArrayList`'in yineleyicisi her değişikliği
yakalayan bir gözlemci değildir; yalnızca `next` çağrıldığında bir sayaç karşılaştırması
yapan, **en iyi çaba** (best-effort) bir denetimdir. Java'nın kendi belgeleri bu davranışı
garanti olarak değil, hataları erken yakalamaya yardımcı bir araç olarak tanımlar — ve bu
ölçüm, aracın kendisinin de atlayabileceği bir durumu gösteriyor.

Bunun pratik sonucu, bir programın doğruluğunu "istisna düşmedi" cümlesine dayandıramayacağı
olur. İki koşumun ikisi de aynı sınıfla, aynı döngüyle, tek bir silme işlemiyle çalıştı;
biri hatayı adıyla bildirdi, öteki hiçbir şey söylemeden bir ögeyi kaybetti. Denetimin
çalışıp çalışmayacağı silinen konumun listenin neresinde olduğuna bağlı — bu da çoğu zaman
çalışma zamanı verisine bağlı bir bilgi olduğundan, aynı kaynak kod bazı girdilerde
hatasız görünüp bazı girdilerde eksik gezinti üretebilir.

Denetimin nerede yapıldığı bu en iyi çabanın sınırını daha da netleştiriyor. `hasNext`
hiçbir zaman bir tutarlılık denetimi yapmaz; yalnız `next` ve ona benzer birkaç yöntem
denetler.

- **KO19** — Bir öge okunduktan sonra kaba bir öge eklenir; hemen ardından önce `hasNext`,
  sonra `next` çağrılır. İkisinin de aynı anda mı yoksa yalnız birinin mi istisna verdiği
  ölçülür.

```java
// HasNextCheck.java — denetim sadece next() icinde calisiyor
import java.util.*;

public class HasNextCheck {
    public static void main(String[] args) {
        List<String> liste = new ArrayList<>(List.of("a", "b", "c"));
        Iterator<String> it = liste.iterator();
        it.next();
        liste.add("ekstra");
        System.out.println("degisiklikten sonra hasNext() : " + it.hasNext());
        try {
            it.next();
            System.out.println("next() basarili (beklenmiyordu)");
        } catch (ConcurrentModificationException e) {
            System.out.println("next()    -> " + e.getClass().getSimpleName());
        }
    }
}
```

```
degisiklikten sonra hasNext() : true
next()    -> ConcurrentModificationException
```

`hasNext` değişiklikten sonra bile sessizce `true` döndürüyor; denetim yalnız `next`
çağrıldığında devreye giriyor. Bu, en iyi çabanın nerede durduğunu somutlaştırıyor: sayaç
karşılaştırması kabın her işleminde değil, yalnız yineleyicinin **ilerlediği** anlarda
çalışıyor. `hasNext`'in "evet, daha var" demesi bir garanti değil; bir sonraki adımın
başarılı olacağının garantisi hiç yok.

#### Sınırlayıcı Ölçüm: Kural Kabın Dışından Değiştirmeyi Yasaklar

Buraya kadarki ölçümler "gezinirken kabı değiştirme" kuralının kırılmasıydı. Ama bu kural
tam olarak ne demek? Yineleyicinin **kendi** `remove` yöntemi de kabı değiştiriyor —
peki o neden aynı istisnayı vermiyor?

- **KO20** — Aynı beş ögelik liste açık bir `Iterator` ile gezilir ve iki öge, doğrudan
  listenin `remove`'uyla değil, yineleyicinin kendi `remove`'uyla silinir.

```java
// YineleyiciRemove.java — kendi remove'u kurali kirmiyor
import java.util.*;

public class YineleyiciRemove {
    public static void main(String[] args) {
        List<String> liste = new ArrayList<>(List.of("a", "b", "c", "d", "e"));
        Iterator<String> it = liste.iterator();
        while (it.hasNext()) {
            String s = it.next();
            if (s.equals("b") || s.equals("d")) {
                it.remove();
            }
        }
        System.out.println("yineleyicinin kendi remove'uyla sonuc : " + liste);
    }
}
```

```
yineleyicinin kendi remove'uyla sonuc : [a, c, e]
```

Hiçbir istisna düşmüyor ve silme işlemi eksiksiz tamamlanıyor: iki öge de kalıcı olarak
çıkarılmış. Bu, önceki iki ölçümle çelişmiyor; kuralı daha doğru okumayı gerektiriyor.
Kural "koleksiyonu değiştirme" değildir — "koleksiyonu **yineleyicinin dışından**
değiştirme"dir. `Iterator.remove()` çağrıldığında, yineleyici kabın iç sayacını (`modCount`)
kendisi günceller ve kendi tuttuğu beklenen değeri de aynı anda ileri alır; bir sonraki
`next` çağrısında ikisi hâlâ eşittir ve denetim sessizce geçer. Listenin kendi `remove`'u
ise bu ikinci sayacı bilmiyor ve yalnız birinciyi değiştiriyor; aradaki fark bir sonraki
`next` çağrısında ortaya çıkıyor. Erken hata mekanizması "kap değişti mi" diye sormuyor,
"kap **yineleyicinin bilmediği bir yoldan** değişti mi" diye soruyor.

Bu ayrım, kuralın neden var olduğunu da açıklıyor. Bir yineleyici, kabın hangi ögesinde
durduğunu bir iç konumla (dizi tabanlı kaplarda bir indeks, bağlı yapılarda bir düğüm
referansı) izliyor. Kap dışarıdan değiştiğinde bu iç konum geçersizleşebilir — bir öge
silinmişse bir sonraki `next` çağrısı ya yanlış bir ögeye ya da hiç var olmayan bir
konuma gidebilir. `Iterator.remove()` kendi kaydını kendi güncellediği için bu tehlike
hiç doğmuyor; dışarıdan gelen bir `remove` ise yineleyicinin haberi olmadan konumu kaydırıp
onu tutarsız bırakıyor. Erken hata, bu tutarsızlığı sessizce yanlış bir ögeye gitmektense
adıyla durmayı tercih eden bir tasarım kararı.

Bu, üç dersin ortak sonucunu bir kez daha doğruluyor. Kural bir davranışı yasaklamıyor —
"koleksiyonu değiştirme" gibi kaba bir yasak koysaydı `Iterator.remove()`'un kendisi de
her zaman istisna verirdi, oysa vermiyor. Kural, **kimin bilgilendirildiğini** ayırıyor:
yineleyicinin kendi yolundan gelen bir değişiklik bilgilendirilmiş bir değişikliktir,
kabın herhangi bir başka yolundan gelen bir değişiklik değildir. Aynı sonucu (bir ögenin
kalıcı olarak çıkarılması) üreten iki çağrı, yineleyicinin bakış açısından birbirinden
tamamen farklı iki olaydır.

Üç yanıt da kendi içinde tutarlıdır, ama hiçbiri ötekinin verdiği garantiyi vermez. Erken
hata "yanlış çalışmaktansa dur" der; anlık kopya "gezinti ne görürse görsün, tutarlı bir
görüntü sunulur" der; sessiz devam "hiçbir zaman durmam, ama gördüklerimin listesi için söz
vermem" der. Üçü de meşru bir sözdür ve üçü de `Iterator` arayüzünün dışında, seçilen
sınıfın kendi kararıdır — arayüz yalnız `hasNext` ve `next` çiftini garanti eder, gezinme
sırasında kabın değişmesine ne olacağını hiç konuşmaz.

#### Özet

- Yineleyicinin temel sözü — `next` tükendiğinde `NoSuchElementException` — üç
  gerçekleştirimde de birebir aynı çıktı; bu, `Iterator` arayüzünün doğrudan sözü ve kap
  hiç değişmediği sürece bozulmuyor.
- Gezinme sırasında kap değiştiğinde dört gerçekleştirimde üç ayrı yanıt ölçüldü:
  `ArrayList` ve `HashSet` erken ve adıyla hata veriyor, `CopyOnWriteArrayList` gezinmeye
  başladığı anki kopyada kalıp değişikliği hiç görmüyor, `ConcurrentHashMap` istisna
  vermeden devam ediyor ama hangi ögeleri göreceğini garanti etmiyor.
- Erken hata bir garanti değil, en iyi çaba: aynı silme işlemi ortadaki bir ögede istisna
  verirken, sondan bir önceki ögede döngüyü hiçbir uyarı vermeden bir öge eksik bitiriyor —
  sonuç, silinen konumun listede nerede olduğuna bağlı.
- Sınırlayıcı ölçüm: yineleyicinin kendi `remove`'u aynı kuralı kırmıyor, çünkü kural
  "değiştirme" değil "yineleyicinin dışından değiştirme"; `Iterator.remove()` kabın iç
  sayacını ve yineleyicinin beklediği değeri aynı anda güncelliyor.
- Üç yanıtın hiçbiri `Iterator` arayüzünde yazmıyor; hangisinin seçileceğine yalnızca
  seçilen sınıf karar veriyor ve üçü de kendi içinde tutarlı, birbirinden bağımsız
  sözlerdir.

#### Sonraki Adım

Bu derste gezinme sırasında kabın **değişmesi** ölçüldü; peki kabın hiç değişmediği,
yalnızca gezinme **sırasının** sorgulandığı durumda ne oluyor? Şimdiye kadarki her
ölçümde bir kabın kendi ögelerini hangi sırayla vereceğine kendi gerçekleştirimi karar
verdi. Sıradaki ders bu kararı kabın dışına taşıyor: sıra kuralı artık çağıranın yazdığı,
kaptan bağımsız bir nesne oluyor ve o nesnenin tutarlı olup olmadığı, kabın kendisinden
önce sınanması gereken ayrı bir soru haline geliyor. Bu derste görülen kalıp orada da
sürüyor: bir kural yazılabilir, derlenebilir, çoğu girdide doğru sonuç verebilir ve yine
de belirli bir girdide sessizce yanlış bir davranışa düşebilir.

### Karşılaştırma ve Sıralama

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/karsilastirma-ve-siralama

Doğal sıra bir tipin kendi compareTo'sunda durur, karşılaştırıcı ise çağıranın dışarıdan verdiği ayrı bir nesnede; aynı veri iki ayrı sırayla dizilebiliyor. Kararlı sıralama List'in kendi sözü ve iki ayrı liste gerçekleştiriminde birebir aynı çıkıyor. Çağıranın kuralı: equals ile uyumsuz bir karşılaştırıcı sıralı bir kümede iki ayrı ögeyi bire indiriyor, geçişli olmayan bir karşılaştırıcı küçük girdide sessizce yanlış sıralıyor ve yeterince büyük girdide adıyla düşüyor. Sınırlayıcı ölçüm aynı karşılaştırıcının bir listede hiçbir öge kaybetmediğini gösteriyor — kusuru doğuran karşılaştırıcı değil, onu tekillik kararına çeviren kaptır.

Önceki dört ders bir kabın kendi ögesine verdiği sözleri ölçtü. Sıralama bunlardan farklı
bir soru sorar: bir kabın ögelerini **hangi sırayla** vereceğine kim karar veriyor? Bazen
karar tipin kendi içindedir, bazen çağıranın elinde ayrı bir nesnedir — ve bu ders o ayrımı
açıp, dışarıdan verilen kuralın kendi kusurlarını nereye düşürdüğünü ölçüyor.

Önceki dersin gösterdiği kalıp burada da sürüyor: bir kural derlenebilir, çoğu girdide
doğru çalışabilir ve yine de belirli bir girdide ya sessizce yanlış sonuç üretebilir ya da
adıyla düşebilir. Sıralamanın farkı, kuralın artık kabın **dışında**, çağıranın yazdığı bir
nesnede durmasıdır — ve bu nesnenin kendi iç tutarlılığı, kabın davranışından önce
sınanması gereken ayrı bir konudur.

#### Doğal Sıra Tipin Sözü, Karşılaştırıcı Çağıranın Sözü

Bir tip `Comparable`'ı gerçekleştirip kendi `compareTo`'sunu yazdığında, bu sıraya
**doğal sıra** denir ve tipin kendi tanımının bir parçasıdır — sınıfın içinde durur,
sınıfla birlikte derlenir. `Comparator` ise böyle değildir: çağıranın, sınıfa hiç
dokunmadan, çağrı yerinde yazdığı ayrı bir nesnedir.

- **KO21** — Aynı üç kayıt iki ayrı sırayla dizilir: biri kaydın kendi `compareTo`'suyla
  (ada göre), öteki çağıranın verdiği bir `Comparator`'la (uzunluğa göre). Kayıt sınıfı
  ikisinde de aynıdır.

```java
// DogalSira.java — dogal sira tipin icinde, karsilastirici disarida
import java.util.*;

public class DogalSira {
    record Kayit(String ad, int uzunluk) implements Comparable<Kayit> {
        @Override public int compareTo(Kayit o) { return ad.compareTo(o.ad); }
    }

    public static void main(String[] args) {
        List<Kayit> liste = new ArrayList<>(List.of(
                new Kayit("zulu", 4), new Kayit("delta", 5), new Kayit("alfa", 4)));

        List<Kayit> dogal = new ArrayList<>(liste);
        Collections.sort(dogal);
        System.out.println("dogal sira (Kayit.compareTo, ada gore)           : " + dogal);

        List<Kayit> disaridan = new ArrayList<>(liste);
        disaridan.sort(Comparator.comparingInt(Kayit::uzunluk));
        System.out.println("disaridan verilen karsilastirici (uzunluga gore) : " + disaridan);
    }
}
```

```
dogal sira (Kayit.compareTo, ada gore)           : [Kayit[ad=alfa, uzunluk=4], Kayit[ad=delta, uzunluk=5], Kayit[ad=zulu, uzunluk=4]]
disaridan verilen karsilastirici (uzunluga gore) : [Kayit[ad=zulu, uzunluk=4], Kayit[ad=alfa, uzunluk=4], Kayit[ad=delta, uzunluk=5]]
```

Aynı üç kayıt, aynı `List`, aynı `sort` çağrısı ailesi — ve iki ayrı sonuç. `Collections
.sort(dogal)` hiçbir sıralama bilgisi almıyor, çünkü `Kayit`'in kendisi zaten `compareTo`
yazarak bir söz vermiş durumda; bu sözün nerede yazıldığı bellidir ve tek bir yerdedir.
`disaridan.sort(...)` ise tam tersini yapıyor: `Kayit` sınıfı hiç değişmeden, çağıran kendi
kuralını (`uzunluk`) çağrı satırında tanımlıyor. İki sıralamanın da geçerli olması,
sıralamanın **tek bir doğrusu** olmadığını gösteriyor — geçerli olan, hangi sözün
kullanıldığının açık olmasıdır.

Bu ayrımın önemi, `compareTo`'nun sınıfla birlikte derlenmesinden geliyor: `Kayit` sınıfını
elinde bulunduran her kod aynı doğal sırayı görür, çünkü söz sınıfın kendi dosyasında bir
kez yazılmıştır. `Comparator` ise her çağrı yerinde yeniden yazılabilir; aynı `Kayit`
listesi bir yerde ada göre, başka bir yerde uzunluğa göre, üçüncü bir yerde başka bir alana
göre sıralanabilir ve üçü de aynı ölçüde geçerlidir. Doğal sıra "bu tipin tek bir kanonik
sırası vardır" iddiasını taşır; karşılaştırıcı böyle bir iddiada bulunmaz, yalnızca o
çağrının ihtiyacına göre bir sıra sağlar.

#### Kararlı Sıralama: Arayüzün Kendi Garantisi

`List.sort` yalnızca bir sıra üretmiyor; eşit sayılan ögelerin **göreli sırasını da**
koruyacağını belgesinde yazıyor. Bu sözün gerçekten iki ayrı gerçekleştirimde de aynı
çıkıp çıkmadığı ölçülebilir.

- **KO22** — Aynı yedi kayıt, her biri bir anahtar ve bir ilk sıra numarasıyla, `ArrayList`
  ve `LinkedList`'e aynı sırayla yazılır. Anahtar tekrar ediyor; ilk sıra numarası tekildir
  ve sıralama sonrası hangi kaydın önce geldiğini okumaya yarıyor.

```java
// Kararlilik.java — kararli siralama iki liste gerceklestiriminde de ayni mi
import java.util.*;

public class Kararlilik {
    record Oge(int anahtar, int ilkSira) {}

    public static void main(String[] args) {
        List<Oge> kaynak = new ArrayList<>();
        int[] anahtarlar = {3, 1, 3, 2, 1, 3, 2};
        for (int i = 0; i < anahtarlar.length; i++) kaynak.add(new Oge(anahtarlar[i], i));

        List<Oge> dizi = new ArrayList<>(kaynak);
        List<Oge> baglantili = new LinkedList<>(kaynak);
        dizi.sort(Comparator.comparingInt(Oge::anahtar));
        baglantili.sort(Comparator.comparingInt(Oge::anahtar));

        System.out.println("ArrayList sonrasi  : " + dizi);
        System.out.println("LinkedList sonrasi : " + baglantili);
        System.out.println("iki sonuc birebir ayni mi : " + dizi.equals(baglantili));
    }
}
```

```
ArrayList sonrasi  : [Oge[anahtar=1, ilkSira=1], Oge[anahtar=1, ilkSira=4], Oge[anahtar=2, ilkSira=3], Oge[anahtar=2, ilkSira=6], Oge[anahtar=3, ilkSira=0], Oge[anahtar=3, ilkSira=2], Oge[anahtar=3, ilkSira=5]]
LinkedList sonrasi : [Oge[anahtar=1, ilkSira=1], Oge[anahtar=1, ilkSira=4], Oge[anahtar=2, ilkSira=3], Oge[anahtar=2, ilkSira=6], Oge[anahtar=3, ilkSira=0], Oge[anahtar=3, ilkSira=2], Oge[anahtar=3, ilkSira=5]]
iki sonuc birebir ayni mi : true
```

İki dizi de anahtara göre sıralı ve her anahtar grubunun içinde `ilkSira` **artan**: `1`
anahtarlı iki kayıt `1, 4` sırasıyla, `3` anahtarlı üç kayıt `0, 2, 5` sırasıyla duruyor —
hiçbiri yer değiştirmedi. `ArrayList` dizi tabanlı, `LinkedList` düğüm tabanlı; iki ayrı iç
yapı olmasına karşın sonuç birebir aynı. Bu, önceki derslerde ölçülen "gezinme sırası"
gibi gerçekleştirime bırakılmış bir şey değil: `List.sort` kararlılığı doğrudan kendi
sözleşmesinde şart koşuyor ve iki gerçekleştirim de bu şartı aynen tutuyor. Kararlı
sıralama burada arayüzün sözü, seçilen sınıfın tercihi değil.

Bu, önceki derste ölçülenle keskin bir tezat oluşturuyor: gezinme sırası, boş anahtar
kabulü gibi sorular gerçekleştirime bırakılmıştı ve üç aile arasında ayrışıyordu.
Kararlılık öyle değil — `List` arayüzünün `sort` yöntemi tanımı, bu yöntemi
gerçekleştiren her sınıfı bağlıyor. Bir yöntemin sözleşmesi bazen "her gerçekleştirim aynı
yanıtı verir" der, bazen "gerçekleştirim seçer" der; ikisi de aynı arayüzün içinde yan
yana durabilir ve hangisinin hangi olduğu yalnızca belgeye bakılarak değil, birden çok
gerçekleştirim koşturularak da doğrulanabilir.

#### Çağıranın Kuralı: Karşılaştırıcı equals İle Uyumsuz Olduğunda

Bir `TreeSet` ya da `TreeMap` tekilliği `equals` ile değil, verilen karşılaştırıcının
`0` dönüp dönmediğiyle belirliyor. Karşılaştırıcı iki farklı ögeyi eşit sayarsa ne olur?

- **KO23** — İki kayıt `equals` ile **farklıdır** (adları farklı) ama uzunlukları
  aynıdır. Uzunluğa göre kuran bir `TreeSet`'e ve adlara bakan bir `HashSet`'e aynı iki
  kayıt eklenir.

```java
// Uyumsuz.java — karsilastirici equals ile uyumsuz olunca sirali kume ne yapiyor
import java.util.*;

public class Uyumsuz {
    record Kayit(String ad, int uzunluk) {}

    public static void main(String[] args) {
        Kayit a = new Kayit("elma", 4);
        Kayit c = new Kayit("kivi", 4);
        System.out.println("a.equals(c) : " + a.equals(c));

        Set<Kayit> siraliKume = new TreeSet<>(Comparator.comparingInt(Kayit::uzunluk));
        siraliKume.add(a);
        siraliKume.add(c);
        System.out.println("uzunluga gore siralanan kume boyutu : " + siraliKume.size());
        System.out.println("kume icerigi                        : " + siraliKume);

        Set<Kayit> hashKume = new HashSet<>();
        hashKume.add(a);
        hashKume.add(c);
        System.out.println("HashSet boyutu (ayni iki kayit)      : " + hashKume.size());
    }
}
```

```
a.equals(c) : false
uzunluga gore siralanan kume boyutu : 1
kume icerigi                        : [Kayit[ad=elma, uzunluk=4]]
HashSet boyutu (ayni iki kayit)      : 2
```

`equals` iki kaydı **farklı** buluyor; `HashSet` bunu doğru yansıtıyor, boyut `2`.
`TreeSet` ise anahtarını hiç `equals`'a sormuyor — yalnız karşılaştırıcıyı çağırıyor ve
uzunluklar eşit olduğu için `0` dönüyor. `TreeSet`'in tekillik kuralı "`compareTo`/
`Comparator` `0` dönerse aynı ögedir" diyor; ikinci `add` çağrısı bu yüzden bir ekleme
değil, birinci ögenin **üzerine yazma** girişimi sayılıyor ve boyut `1`'de kalıyor.
Kusur derlenirken görünmüyor ve hiçbir istisna da fırlamıyor — `kivi` sessizce kayboluyor.

Bu, önceki derste ölçülen "equals yazıp hashCode yazmamak" kusuruyla aynı aileden ama ayrı
bir mekanizmadan geliyor. Orada eksik olan `hashCode`'du ve sonuç aramanın **bulamamasıydı**.
Burada hiçbir yöntem eksik değil; `equals` de `compareTo`/`Comparator` da tam yazılmış,
ama ikisi **birbiriyle tutarsız**. `TreeSet` ve `TreeMap`'in kendi belgeleri bu tutarlılığı
açıkça talep eder: karşılaştırıcının `equals` ile "uyumlu" olması beklenir, ama bu beklenti
derleyicinin denetleyebileceği bir tip kısıtı değildir — yalnızca bir sözleşme cümlesidir
ve çağıran onu okumazsa hiçbir uyarı almaz.

#### Geçişli Olmayan Karşılaştırıcı: Küçükte Sessiz, Büyükte İstisna

Bir karşılaştırıcının ikinci yükümlülüğü **geçişliliktir**: `a` `b`'den küçükse ve `b`
`c`'den küçükse, `a` da `c`'den küçük olmalıdır. Bu tutulmazsa ne olur?

- **KO24** — Aynı sayılar üç gruba (`% 3` kalanına göre) ayrılır ve karşılaştırıcı bu
  üç grubu döngüsel bir sırayla karşılaştırır: 0. grup 1'den küçük, 1. grup 2'den küçük,
  2. grup 0'dan küçük. Bu döngü geçişliliği baştan bozar. Aynı sabit tohumla üretilmiş
  rastgele sayılar iki ayrı büyüklükte (`50` ve `100`) sıralanır.

```java
// TamOlcum.java — gecisli olmayan karsilastirici kucukte sessiz, buyukte istisna
import java.util.*;

public class TamOlcum {
    static int karsilastir(int x, int y) {
        int tx = x % 3, ty = y % 3;
        if (tx == ty) return 0;
        if ((tx == 0 && ty == 1) || (tx == 1 && ty == 2) || (tx == 2 && ty == 0)) return -1;
        return 1;
    }

    static int ihlalSayisi(Integer[] dizi) {
        int sayac = 0;
        for (int i = 0; i < dizi.length; i++)
            for (int j = i + 1; j < dizi.length; j++)
                if (karsilastir(dizi[i], dizi[j]) > 0) sayac++;
        return sayac;
    }

    static Integer[] rasgeleDizi(int n) {
        Integer[] dizi = new Integer[n];
        Random r = new Random(42);
        for (int i = 0; i < n; i++) dizi[i] = r.nextInt(1_000_000);
        return dizi;
    }

    public static void main(String[] args) {
        Integer[] kucuk = rasgeleDizi(50);
        Arrays.sort(kucuk, TamOlcum::karsilastir);
        System.out.println("n=50  -> istisna yok, ihlal sayisi=" + ihlalSayisi(kucuk));

        Integer[] buyuk = rasgeleDizi(100);
        try {
            Arrays.sort(buyuk, TamOlcum::karsilastir);
            System.out.println("n=100 -> istisna yok (beklenmiyordu)");
        } catch (IllegalArgumentException e) {
            System.out.println("n=100 -> " + e.getClass().getSimpleName());
        }
    }
}
```

```
n=50  -> istisna yok, ihlal sayisi=195
n=100 -> IllegalArgumentException
```

Elli ögelik dizi sessizce sıralanıyor — hiçbir istisna yok — ama sonuç, karşılaştırıcının
**kendi kuralına göre bile** yanlış: her biri bir çift ögenin döngüsel kuralı ihlal ettiği
`195` durum var, yani dizi tutarlı bir sıraya hiç oturmamış, yalnızca öyleymiş gibi
görünüyor. Yüz ögelik dizide ise algoritmanın iç birleştirme adımları kendi varsaydığı
düzenin tutmadığını **tespit ediyor** ve adıyla bir istisna fırlatıyor: "karşılaştırma
yöntemi kendi genel sözleşmesini ihlal ediyor." Eşik veriye bağlıdır ve burada `50` ile
`100` arasında bir yerdedir; küçük diziler sıralama algoritmasının iç tutarlılık
denetimini tetiklemeye yetecek kadar iş yapmadan bitebiliyor, büyük diziler bu denetime
takılıyor. Geçişsizlik iki ayrı bedel ödetiyor: küçük ölçekte **sessiz yanlış sonuç**,
büyük ölçekte **istisna**.

Bu iki bedel arasındaki fark, sıralama algoritmasının kendi iç yapısından geliyor. Küçük
girdilerde sıralama algoritması veriyi daha az bölüp birleştirerek işliyor ve
karşılaştırıcının döngüsel çelişkisiyle hiç karşılaşmadan bitebiliyor; girdi büyüdükçe
algoritmanın kendi ürettiği ara parçaları birleştirirken tuttuğu bir iç varsayım
(birleştirilen iki parçanın kendi içinde tutarlı sıralı olduğu) karşılaştırıcının çelişkisi
yüzünden bozuluyor ve algoritma bunu kendi kontrolüyle yakalıyor. Bu denetim `Comparator`
arayüzünün bir sözü değil, seçilen sıralama algoritmasının bir iç güvenlik önlemi;
başka bir algoritma aynı veriyle hiç istisna vermeden, yalnızca yanlış bir sonuçla
dönebilirdi.

#### Sınırlayıcı Ölçüm: Kusuru Doğuran Karşılaştırıcı Değil, Kabın Kullanımı

Bir önceki iki ölçüm karşılaştırıcının kendisini kusurlu gösteriyor gibi görünebilir. Aynı
uzunluk karşılaştırıcısı bu kez yalnızca **sıralamak** için, bir `TreeSet`e değil bir
`List`'e verilirse ne olur?

- **KO25** — Aynı uyumsuz karşılaştırıcı (`Uyumsuz.java`'daki uzunluk karşılaştırıcısı) bu
  kez üç kayıtlık bir listeyi sıralamak için kullanılır; kayıtlardan ikisi aynı uzunlukta
  ama `equals` ile farklıdır.

```java
// ListedeGorunmuyor.java — ayni karsilastirici listede hicbir ogeyi kaybetmiyor
import java.util.*;

public class ListedeGorunmuyor {
    record Kayit(String ad, int uzunluk) {}

    public static void main(String[] args) {
        List<Kayit> liste = new ArrayList<>(List.of(
                new Kayit("armut", 5), new Kayit("elma", 4), new Kayit("kivi", 4)));
        liste.sort(Comparator.comparingInt(Kayit::uzunluk));
        System.out.println("siralanmis liste boyutu : " + liste.size());
        System.out.println("siralanmis liste        : " + liste);
    }
}
```

```
siralanmis liste boyutu : 3
siralanmis liste        : [Kayit[ad=elma, uzunluk=4], Kayit[ad=kivi, uzunluk=4], Kayit[ad=armut, uzunluk=5]]
```

Üç kayıt da yerinde; `elma` ile `kivi` aynı uzunlukta olduğu için yan yana duruyor, ama
ikisi de listede kalıyor ve kararlılık gereği `elma` (girdide önce gelen) `kivi`'den önce
yazılıyor. `TreeSet` ölçümünde `kivi` kaybolmuştu; burada kaybolmuyor. Fark
karşılaştırıcıda değil, karşılaştırıcının **nasıl kullanıldığındadır**: `List.sort` onu
yalnızca bir sıralama kuralı olarak okuyor ve `0` dönmesi hiçbir ögeyi silmiyor, yalnızca
iki ögeyi yan yana bırakıyor. `TreeSet` ise aynı `0` dönüşünü **tekillik kararı** olarak
okuyor. Aynı nesne, aynı yöntem, aynı dönüş değeri — iki ayrı kap onu iki ayrı soruya
yanıt sayıyor. Kusuru doğuran karşılaştırıcının kendisi değil, onu tekillik kararı olarak
kullanan kaptır.

Bu son ölçüm dersin bütün ipliklerini birbirine bağlıyor. Bir karşılaştırıcı yazmak
başlı başına bir kusur değildir; kusur, o karşılaştırıcının hangi kabın elinde, hangi
soruya yanıt vermek için kullanıldığında ortaya çıkar. `List.sort` ona yalnızca "hangi
önce gelsin" diye sorar ve `0` dönüşünü "ikisi de aynı yerde durabilir" diye okur.
`TreeSet` aynı `0` dönüşünü "bunlardan yalnız biri var olabilir" diye okur. Karşılaştırıcı
tek bir yöntemdir ve tek bir kararı taşır; onu hangi sorunun yanıtı sayacağına karar veren
çağıranın seçtiği kaptır.

#### Özet

- Doğal sıra bir tipin kendi `compareTo`'sunda durur ve tipin tanımının parçasıdır;
  karşılaştırıcı çağıranın çağrı yerinde yazdığı, sınıfa hiç dokunmayan ayrı bir nesnedir.
  Aynı veri iki ayrı sırayla dizilebilir.
- Kararlı sıralama `List.sort`'un kendi sözüdür: `ArrayList` ve `LinkedList`'te birebir
  aynı sonucu, eşit anahtarlı ögelerin göreli sırasını bozmadan, üretiyor.
- Çağıranın kuralı: bir karşılaştırıcı `equals`'la uyumsuzsa, sıralı bir kümede `0` dönen
  iki öge tek bir öge olarak kalıyor — kusur hiçbir istisna vermeden, sessizce oluşuyor.
- Geçişli olmayan bir karşılaştırıcı küçük girdide sessizce yanlış bir sıra üretiyor,
  yeterince büyük girdide ise sıralama algoritmasının kendi tutarlılık denetimine takılıp
  adıyla düşüyor; eşik veriye bağlıdır.
- Sınırlayıcı ölçüm: aynı uyumsuz karşılaştırıcı bir listede hiçbir öge kaybetmiyor.
  Kusuru üreten karşılaştırıcının kendisi değil, onun dönüş değerini tekillik kararı
  olarak okuyan kaptır.

#### Sonraki Adım

Bu konuda ölçülen her şey, sıra kuralının artık kabın içinde değil, dışarıdan verilen **bir
nesnede** durduğunu gösterdi — `Comparator` da, `Comparable`'ı gerçekleştiren bir tip de,
kabın kendisinden bağımsız yazılmış küçük birer sözleşmeydi. Sıradaki konu o nesnenin
kendisine bakıyor: `Comparator` de dahil olmak üzere kütüphanenin tek yöntemli tipleri
(fonksiyonel arayüzler) ne söz veriyor ve bu söz çağrı yerinde bir lambda ile
yazıldığında hangi garanti yerinde kalıyor, hangisi kayboluyor.

## Fonksiyonel Java

Tek soyut yöntemli tiplerin arayüz olarak taşıdığı söz, lambda ifadelerinin yakalama kuralı, akış API'sinin ara ve sonlandırıcı işlemlerinde neyi garanti edip neyi etmediği, toplayıcıların gruplama ve indirgeme davranışı, yokluğun tiple ifade edilmesi.

### Fonksiyonel Arayüzler

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/fonksiyonel-arayuzler

Tek soyut yöntem bir derleme zamanı sözüdür: iki yöntemli bir arayüz asla lambda hedefi olamaz ve işaret yalnız bu sözü erken bozan tanımı yakalar. Arayüzün ikinci sözü — davranış — hiçbir yerde denetlenmez: tip sözü ailenin dördünde de derlenir, saflık ve yan etkisizlik sözü ise Predicate ve Function'da sessizce kırılabilir.

Önceki konunun son dersi sıra kuralını dışarıdan verilen bir nesneye taşımıştı: karşılaştırıcı,
çağıranın koleksiyona taktığı, tek yöntemli bir tipti. Kütüphane onu çağırdı, kararı vermedi.
Bu ders o nesnenin kendisine bakar — tek soyut yöntemli bir tipin, fonksiyonel arayüzün,
derleyiciye ne söylediğini ve hangi sözünün hiç kimse tarafından denetlenmediğini ölçer.

Standart kütüphanenin her satırı bir sözdür ve üç kaynaktan biri o sözü verir: arayüzün
kendisi, seçilen gerçekleştirim ya da çağıranın uyduğu bir kural. Fonksiyonel arayüzler bu
üçlünün en dar örneğidir — yalnızca bir soyut yöntem taşırlar — ve tam da bu darlık
yüzünden lambda ifadelerinin hedefi olabilirler. Bu darlığı derleyici mi tutuyor, yoksa
yalnız belgede mi duruyor? Arayüzün taşıdığı ikinci bir söz daha var: yöntemin nasıl
davranması gerektiği. O söz nerede duruyor, kim denetliyor?

#### Tek Soyut Yöntem Derleme Zamanı Bir Sözdür

- **FN1** — Dört kap biçimi denenir: tek yöntemli ve iki yöntemli arayüz, her ikisi de
  `@FunctionalInterface` işaretli ve işaretsiz olarak. Kaynak metinler dersin içinde yazılır.
- **FN2** — Bir arayüzün tanımı derlenip derlenmediği ayrı, o arayüzü lambda hedefi olarak
  kullanan bir sınıfın derlenip derlenmediği ayrı ölçülür; ikisi karıştırılmaz.
- **FN3** — Derleyicinin hata iletisinin metni basılmaz; yalnız derlenip derlenmediği
  bildirilir, çünkü ölçülen şey iletinin içeriği değil kapının açık olup olmadığıdır.

```java
// Derleme.java — ortak çekirdek: verilen kaynak metni derleniyor mu
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.*;
import java.util.spi.ToolProvider;

class Derleme {
    static boolean derlenirMi(String kaynak, String sinif) throws Exception {
        Path d = Files.createTempDirectory("olcek");
        Path k = d.resolve(sinif + ".java");
        Files.writeString(k, kaynak);
        PrintWriter bos = new PrintWriter(Writer.nullWriter());
        return ToolProvider.findFirst("javac").orElseThrow()
                .run(bos, bos, "-d", d.toString(), k.toString()) == 0;
    }
}
```

```java
// Isaret.java — @FunctionalInterface isareti neyi yakaliyor
public class Isaret {
    static final String TEK = "interface Islem { int uygula(int x); }";
    static final String TEK_ISARETLI = "@FunctionalInterface\ninterface Islem { int uygula(int x); }";
    static final String IKI = "interface Islem { int uygula(int x); int geriAl(int x); }";
    static final String IKI_ISARETLI = "@FunctionalInterface\ninterface Islem { int uygula(int x); int geriAl(int x); }";

    static final String[][] DENEMELER = {
        {"tek yontem, isaretsiz", TEK},
        {"tek yontem, isaretli", TEK_ISARETLI},
        {"iki yontem, isaretsiz", IKI},
        {"iki yontem, isaretli", IKI_ISARETLI},
    };

    public static void main(String[] args) throws Exception {
        System.out.printf("%-24s %-20s %s%n", "bicim", "tanim derleniyor mu", "lambda hedefi olabiliyor mu");
        for (String[] d : DENEMELER) {
            String ad = d[0], govde = d[1];
            boolean tanim = Derleme.derlenirMi(govde, "Islem");
            boolean lambda = tanim && Derleme.derlenirMi(
                    govde + "\nclass Kullan { Islem i = x -> x; }", "Islem");
            System.out.printf("%-24s %-20s %s%n", ad,
                    tanim ? "derlendi" : "derlenmedi",
                    !tanim ? "-" : (lambda ? "derlendi" : "derlenmedi"));
        }
    }
}
```

```
bicim                    tanim derleniyor mu  lambda hedefi olabiliyor mu
tek yontem, isaretsiz    derlendi             derlendi
tek yontem, isaretli     derlendi             derlendi
iki yontem, isaretsiz    derlendi             derlenmedi
iki yontem, isaretli     derlenmedi           -
```

Dört satırın yalnız biri tanım aşamasında düşüyor: iki yöntemli ve işaretli arayüz hiç
derlenmiyor, çünkü işaret derleyiciye "bu arayüzün tam olarak bir soyut yöntemi olsun"
demektir ve derleyici bunu kontrol eder. Geri kalan üç satırın tanımı derleniyor — işaretsiz
iki yöntemli arayüz de sıradan bir arayüz olarak geçerlidir; Java'da bir arayüzün birden çok
soyut yöntemi olması yasak değildir.

Asıl ayrım ikinci sütunda. Tek yöntemli arayüzlerin ikisi de — işaretli olsun olmasın —
lambda hedefi olabiliyor. İki yöntemli, işaretsiz arayüz ise tanımı geçse bile lambda hedefi
**olamıyor**: `x -> x` ifadesi hangi yönteme karşılık geldiğini belirtmiyor, derleyici bunu
çözemiyor. İşaretin yaptığı ile yapmadığı burada ayrılıyor.

#### İşaret Garantiyi Eklemez, Erken Yakalar

Tablo tek bir sonucu doğruluyor: bir arayüzün lambda hedefi olabilmesi **yapısal** bir
niteliktir — tam olarak bir soyut yöntem taşımak — ve bu nitelik işaretten bağımsız var
olur ya da olmaz. İki yöntemli, işaretsiz arayüz de tanım olarak derleniyor ama zaten
fonksiyonel değildi; yalnızca bunu kimse söylemedi, ta ki biri onu lambda ile kullanmaya
çalışana kadar. O andaki hata, arayüzün tanımlandığı satırdan uzakta, kullanıldığı her
noktada ayrı ayrı çıkar.

İşaret bu uzaklığı kapatıyor. İki yöntemli ve işaretli arayüz derleme **anında** düşüyor —
kullanımı beklemeden, tanımın kendisinde. İşaretin verdiği söz "bu arayüz fonksiyoneldir"
değil, "bu arayüz fonksiyonel olmaktan çıkarsa bana burada söyle" sözüdür. Garantiyi
**arayüz** zaten kendi yapısıyla veriyordu; işaret yalnız o garantinin bozulduğu anı, kullanım
yerine değil tanım yerine taşıyor. Standart kütüphanedeki `Runnable`, `Comparator`,
`Predicate` gibi tipler bu işareti taşır, ama işareti silseniz bile hâlâ birer soyut
yöntemli arayüzdürler ve hâlâ lambda hedefi olurlar — işaret onları fonksiyonel yapmaz,
yalnız bozulmalarını erkenden yakalar.

Nesneye Dayalı Java kursunda derleyicinin reddettiği dört şey sayılmıştı: erişim belirteci,
`final`, denetlenen istisna, tip parametresi. Tek soyut yöntem kuralı bu listeye eklenen
beşinci bir koruma değil, aynı korumanın standart kütüphaneye taşınmış hâlidir — burada da
derleyici bir yapıyı kabul etmiyor ve bunu tanım satırında bildiriyor. Ama bu korumanın
kapsamı dardır: yalnız arayüzün *şekline* bakar, kaç yöntem olduğuna. Şeklin arkasındaki
sözün ne olduğuna hiç bakmaz — ve bu ayrım aşağıdaki ölçümde ortaya çıkıyor.

Şeklin kendisi de sanıldığından daha az şey taşır. Nesneye Dayalı Java kursunda bir sınıfın
bir arayüzü uygulaması **adla** kurulur: `implements` satırında arayüzün adı geçer. Lambda
ifadesi bu adı hiç yazmaz; derleyici hedefin adına değil, yalnız biçimine bakar.

```java
// Yapisal.java — ayni lambda, birbirinden habersiz iki fonksiyonel arayuzun hedefi olabiliyor mu
public class Yapisal {
    static final String IKI_ARAYUZ = """
        interface Katlayici { int uygula(int x); }
        interface Donusturucu { int uygula(int x); }
        class Kullan {
            Katlayici k = x -> x * 3;
            Donusturucu d = x -> x * 3;
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("iki ayrı, birbirini bilmeyen arayuz, ayni govdeli lambda: "
                + (Derleme.derlenirMi(IKI_ARAYUZ, "Katlayici") ? "derlendi" : "derlenmedi"));
    }
}
```

```
iki ayrı, birbirini bilmeyen arayuz, ayni govdeli lambda: derlendi
```

`Katlayici` ile `Donusturucu` birbirini hiç bilmiyor, aralarında ne kalıtım var ne ortak bir
üst tip — ikisi de yalnızca aynı biçimde bir soyut yöntem taşıyor. Aynı lambda ifadesi ikisi
için de derleniyor. Bu, arayüzü uygulayan bir sınıfın davranışından ayrılır: orada ad
bağlayıcıdır, burada biçim yeterlidir. Derleyicinin denetlediği söz, tam olarak bu kadar
dardır — ne isim ne köken, yalnız parametre sayısı, parametre tipleri ve dönüş tipi.

#### Varsayılan, Statik ve Object Yöntemleri Sayılmaz

Tek soyut yöntem kuralı "tek yöntem" değil, "tek **soyut** yöntem" der. Bir arayüz varsayılan
gövdeli (`default`) ya da statik yöntemler taşıyabilir, bunlar soyut sayılmaz ve saymaya
girmez — çünkü çağıran onları uygulamak zorunda değildir, kütüphane onları zaten yazmıştır.

- **FN4** — İki kap denenir: biri bir soyut yöntemin yanına bir `default` ve bir `static`
  yöntem ekler; öteki bir soyut yöntemin yanına `Object` sınıfının kendi yöntemlerinden
  ikisini (`equals`, `toString`) soyut olarak yeniden bildirir. İkisi de `@FunctionalInterface`
  işaretlidir.
- **FN5** — Her iki kap için de hem tanımın derlenip derlenmediği hem lambda hedefi
  olabilip olmadığı ayrı ayrı ölçülür.

```java
// Sayilmaz.java — varsayilan, statik ve Object yontemleri soyut sayaca girmiyor mu
public class Sayilmaz {
    static final String COK_UYELI = """
        @FunctionalInterface
        interface Islem {
            int uygula(int x);
            default int ikiKat(int x) { return uygula(x) * 2; }
            static Islem ozdeslik() { return x -> x; }
        }
        """;

    static final String OBJECT_IMZALI = """
        @FunctionalInterface
        interface Islem {
            int uygula(int x);
            boolean equals(Object o);
            String toString();
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("bir soyut + iki varsayilan/statik yontem : "
                + (Derleme.derlenirMi(COK_UYELI, "Islem") ? "derlendi" : "derlenmedi"));
        System.out.println("bir soyut + Object'in iki yontemi        : "
                + (Derleme.derlenirMi(OBJECT_IMZALI, "Islem") ? "derlendi" : "derlenmedi"));

        boolean lambdaCokUyeli = Derleme.derlenirMi(
                COK_UYELI + "\nclass K { Islem i = x -> x; }", "Islem");
        boolean lambdaObjectImzali = Derleme.derlenirMi(
                OBJECT_IMZALI + "\nclass K { Islem i = x -> x; }", "Islem");
        System.out.println("ilkinde lambda hedefi olabiliyor mu       : "
                + (lambdaCokUyeli ? "derlendi" : "derlenmedi"));
        System.out.println("ikincisinde lambda hedefi olabiliyor mu   : "
                + (lambdaObjectImzali ? "derlendi" : "derlenmedi"));
    }
}
```

```
bir soyut + iki varsayilan/statik yontem : derlendi
bir soyut + Object'in iki yontemi        : derlendi
ilkinde lambda hedefi olabiliyor mu       : derlendi
ikincisinde lambda hedefi olabiliyor mu   : derlendi
```

Dört satır da derleniyor. İlk kap beklenen bir sonuç veriyor: `ikiKat` ve `ozdeslik`
soyut değildir, birer gövdeleri vardır, sayaca girmezler — arayüz hâlâ tek soyut yöntemlidir
ve işaret buna itiraz etmez. İkinci kap daha az sezgiseldir: `equals` ve `toString` soyut
olarak yeniden bildirilmiş olsalar bile derleyici onları saymaz, çünkü ikisi de `Object`
sınıfının **herkese açık** bir yöntemiyle eşleşir ve her gerçekleştirim `Object`'ten miras
aldığı için bu yöntemleri zaten dolaylı olarak sağlar. Kural üç soyut bildirimi değil, üç
bildirimden **ikisinin zaten `Object`'te karşılığı olduğunu** görür ve yalnız `uygula`'yı
sayar.

Bu ölçüm, tek soyut yöntem kuralının söylediğinden biraz daha dar bir kural olduğunu
gösteriyor: sayılan şey "arayüzdeki soyut bildirim sayısı" değil, "arayüzün her
gerçekleştirimin **yeniden** yazmak zorunda kalacağı yöntem sayısı"dır. `Object`'in
yöntemleri zaten her sınıfta vardır, o yüzden yeniden yazma yükümlülüğüne girmezler. Önceki
konudan tanıdık bir örnek burada açıklığa kavuşuyor: `Comparator<T>` bir düzine civarında
`default` ve `static` yöntem taşır (`reversed`, `thenComparing`, `naturalOrder`) ve yine de
fonksiyoneldir, çünkü çağıranın yeniden yazması gereken tek yer `compare` kalır. Üye
sayısıyla soyut yöntem sayısı aynı şey değildir.

#### Arayüzün Davranış Sözü Hiçbir Yerde Denetlenmez

Bir fonksiyonel arayüzün iki ayrı sözü vardır. Birincisi imzadır — kaç parametre, hangi
tipte, ne döndürüyor — ve bu söz yukarıdaki tablolarda ölçüldüğü gibi derleme zamanında
tutuluyor. İkincisi davranıştır: `Predicate` "aynı girdiye aynı sonucu ver", `Function`
"yan etki yaratma" bekler. Bu ikinci söz hangi katmanda duruyor?

- **FN6** — Standart ailenin dört üyesi örnek alınır: `Predicate`, `Function`, `Supplier`,
  `Consumer`. Ailenin geri kalanı aynı örüntüyü izler.
- **FN7** — Saflık ihlali bir çağrı sayacıyla, yan etki ihlali bir biriktiriciyle gözle
  görülür kılınır; ikisi de yalnızca bu ölçüm için yazılmış yardımcı durumdur.

```java
// Anlam.java — arayuzun tip sozu ile davranis sozu ayni yerde denetlenmiyor
import java.util.function.*;

public class Anlam {
    static int cagriSayaci = 0;

    public static void main(String[] args) {
        Predicate<Integer> p = x -> x > 0;
        Function<Integer, Integer> f = x -> x * 2;
        Supplier<Integer> s = () -> 42;
        Consumer<Integer> c = x -> { };
        System.out.println("dort tip sozu de eslesti, dordu de derlendi");
        System.out.println();

        Predicate<Integer> kararsiz = x -> { cagriSayaci++; return cagriSayaci == 1; };
        System.out.printf("ayni girdiyle ilk cagri   : %s%n", kararsiz.test(5));
        System.out.printf("ayni girdiyle ikinci cagri: %s%n", kararsiz.test(5));

        StringBuilder gunluk = new StringBuilder();
        Function<Integer, Integer> yanEtkili = x -> { gunluk.append(x).append(','); return x + 1; };
        yanEtkili.apply(1);
        yanEtkili.apply(2);
        System.out.printf("yan etki biriktirdi mi   : %s%n", !gunluk.isEmpty());
    }
}
```

```
dort tip sozu de eslesti, dordu de derlendi

ayni girdiyle ilk cagri   : true
ayni girdiyle ikinci cagri: false
yan etki biriktirdi mi   : true
```

İlk satır tip sözünün nerede durduğunu tekrar doğruluyor: dört değişken de kendi arayüzünün
imzasına uydu, derleyici bunu denetledi ve dördü de derlendi. Geri kalan üç satır ikinci
sözü sınıyor. `kararsiz` adlı `Predicate`, aynı girdiye (**5**) iki ayrı çağrıda iki ayrı
sonuç veriyor — önce `true`, sonra `false` — ve derleyici buna hiç karışmadı, çalışma
zamanı da bir istisna düşürmedi. `yanEtkili` adlı `Function` çağrıldıkça kendi kapsamının
dışındaki bir `StringBuilder`'a yazıyor; bu da ne derlenirken ne çalışırken engellendi.

Sonuç sessizdir: `Predicate.test` "aynı girdiye aynı sonucu ver" demez diye bir istisna
fırlatmaz, yalnızca yanlış davranan bir uygulamayı olduğu gibi çalıştırır. Ailenin tip sözü
yukarıdaki ilk satırda doğrulandı: dört üyenin dördü de kendi imzasına uydu, dördü de
derlendi. Davranış sözü ise ailede eşit dağılmaz. `Predicate` ve `Function` için saflık ve
yan etkisizlik gerçek birer belge sözüdür ve az önce ikisi de kırıldı, ikisi de sessiz
kaldı — derleyici karışmadı, çalışma zamanı bir istisna düşürmedi. `Supplier` için belge
daha da gevşektir: her çağrıda aynı ya da farklı bir değer üretmesi konusunda **hiçbir
zorunluluk yoktur**, yani orada kırılacak bir söz bile bulunmaz. `Consumer` ise tam tersi
yönde kuruludur — amacı zaten yan etkidir, belge onu bir kusur saymaz, bir söz gibi
davranmaz. Tabloya sığan tek genel kural şudur: derleyici tip sözünü ailenin **dördünde de**
eksiksiz tutar; davranış sözü ailede tektip değildir ve tutulduğu yerlerde bile hiçbir
katman onu denetlemez.

Bu, önceki dersle (Karşılaştırma ve Sıralama) kurulan bir gözlemin tekrarıdır ama farklı bir
yerde: orada geçişli olmayan bir karşılaştırıcının küçük girdide sessizce yanlış sıra
ürettiği ölçülmüştü. Burada aynı sessizlik, sıralamadan çok daha küçük ve çok daha sık
kullanılan bir yüzeyde — tek satırlık bir lambda ifadesinde — ortaya çıkıyor. Fonksiyonel
arayüz ne kadar dar olursa olsun, darlığın kendisi bir davranış garantisine dönüşmüyor.

#### Özet

- Bir arayüzün lambda hedefi olabilmesi yapısal bir koşuldur: tam olarak bir soyut yöntem.
  Bu koşul derleme zamanında tutulur, işaretten bağımsızdır.
- `@FunctionalInterface` işareti garanti eklemez; yalnız bu yapısal koşulun ihlalini,
  kullanım yerine değil tanım yerine taşıyarak erken yakalar.
- İki yöntemli, işaretsiz bir arayüz de derlenir ama lambda hedefi olamaz — fonksiyonel
  olmama hâli işaret olmadan da geçerlidir, yalnız daha geç fark edilir.
- Fonksiyonel arayüzlerin ikinci bir sözü vardır: davranış (saflık, yan etkisizlik). Bu söz
  ne derleyicide ne çalışma zamanında denetlenir; ihlali sessizdir, istisna düşürmez.
- Standart ailenin tip sözü dört üyede de derleyicide durur; davranış sözü ailede tektip
  değildir — `Predicate` ve `Function`'da kırılabilir ve kırıldığında sessiz kalır,
  `Supplier`'da böyle bir söz belgede bile yoktur.

#### Sonraki Adım

Bu ders fonksiyonel arayüzün kendisine baktı: tek soyut yöntemin nerede tutulduğunu, ikinci
sözün nerede tutulmadığını. Bir lambda ifadesi yazıldığında bu tip yerinde bir şey daha
yapar: çevresindeki adları **yakalar**. Sıradaki ders o tipin yerinde yazıldığında neyi
yakaladığını ölçer — yerel bir değişkenle bir alanın yakalanması aynı kısıta mı bağlıdır
ve ikisi aynı sonucu mu üretir.

### Lambda İfadeleri

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/lambda-ifadeleri

Bir lambda ifadesi çevresindeki bir yerel değişkeni yakaladığında o değişkenin etkin olarak final olması derleme zamanında zorunludur; bir alanı yakaladığında böyle bir kısıt yoktur ve nesne sonradan değişince aynı lambda başka bir sonuç verir. Yakalanan yerel değişkenin donan şey değeridir, gösterdiği nesne değil.

Önceki ders fonksiyonel arayüzün kendisine baktı: tek soyut yöntemin derleme zamanında
tutulduğunu, davranış sözünün hiçbir yerde tutulmadığını ölçtü. Bir lambda ifadesi yazıldığı
anda bu tipin yerinde bir şey daha olur — ifade, kendi gövdesinin dışındaki adlara başvurur
ve o adları **yakalar**. Yerel bir değişkenle bir alanın yakalanması aynı kısıta mı bağlıdır
ve ikisi aynı sonucu mu üretir?

Bu ders bu soruyu doğrudan koşumla yanıtlar: aynı gövdeli, aynı işi yapan iki lambda — biri
bir yerel değişkeni, öteki bir alanı okuyarak — iki ayrı çağrı biçiminde kaç ayrı sonuç
üretiyor? Kapanış (closure) kavramının kendisi — bir işlevin kendi tanım ortamını
beraberinde taşıması — Programlama Temelleri ve JavaScript'te Nesneler ve Fonksiyonlar
kurslarında kuruldu ve burada tekrar açılmaz. Ölçülen şey kapanışın ne olduğu değil,
Java'nın onu hangi kısıtla sınırladığıdır.

#### Lambda Bir Değişkeni Değeriyle Yakalar

- **FN8** — Aynı çarpma işlemi iki ayrı `IntUnaryOperator` ile yazılır: biri bir yerel
  değişkeni, öteki statik bir alanı okur. İkisi de kurulduğunda çarpan **2**'dir.
- **FN9** — Lambda kurulduktan sonra alan **5**'e, sonra **7**'ye değiştirilir; yerel
  değişkene ise hiç dokunulmaz — zaten dokunulamaz, aşağıdaki bölüm nedenini ölçer.
- **FN10** — Her iki lambda da aynı `applyAsInt(10)` çağrısıyla iki kez okunur; dönen
  değerler birebir karşılaştırılır.

```java
// Yakalama.java — degisken mi alan mi: lambda hangisini donduruyor
import java.util.function.IntUnaryOperator;

public class Yakalama {
    static int carpan = 2;

    public static void main(String[] args) {
        int carpanYerel = 2;
        IntUnaryOperator yerelYakalayan = x -> x * carpanYerel;
        System.out.println("yerel yakalama, ilk cagri   : " + yerelYakalayan.applyAsInt(10));
        carpan = 5;
        System.out.println("yerel yakalama, alan degisti: " + yerelYakalayan.applyAsInt(10));

        IntUnaryOperator alanYakalayan = x -> x * carpan;
        System.out.println("alan yakalama, carpan=5     : " + alanYakalayan.applyAsInt(10));
        carpan = 7;
        System.out.println("alan yakalama, carpan=7     : " + alanYakalayan.applyAsInt(10));
    }
}
```

```
yerel yakalama, ilk cagri   : 20
yerel yakalama, alan degisti: 20
alan yakalama, carpan=5     : 50
alan yakalama, carpan=7     : 70
```

`yerelYakalayan` iki çağrıda da **20** veriyor — aradaki satırda statik alan **5**'e
değişmiş olsa bile. Lambda kurulduğu anda `carpanYerel`'in o anki **değerini** almış ve o
değeri kendi içine gömmüş; dışarıda ne olursa olsun bu kopyayı okuyor. `alanYakalayan` ise
iki çağrıda iki ayrı sonuç veriyor: **50**, sonra **70**. Bu lambda bir değeri gömmedi,
`carpan` alanına giden bir **yolu** gömdü ve her çağrıda o yoldan güncel değeri okuyor.

Aynı gövde biçimi — `x -> x * bir_şey` — iki ayrı yakalama biçiminde iki ayrı sayıda
sonuç üretiyor: yerel değişkende **bir** sonuç, alanda **iki** ayrı sonuç. Sorunun yanıtı
burada net: yerel değişkenle alanın yakalanması aynı kısıta bağlı değildir ve ikisi aynı
sonucu üretmez.

#### Etkin Olarak Final Kısıtı Derleme Zamanındadır

Yerel değişkenin neden sabit kaldığı bir önceki ölçümde görüldü; peki bu sabitlik
derleyicinin bir kararı mı, yoksa yalnızca bu örnekte mi böyle çıktı? Java'nın kuralı açıktır:
bir lambda ifadesi çevreleyen kapsamdaki bir yerel değişkeni yakalıyorsa, o değişken **etkin
olarak `final`** olmak zorundadır — yani ilk atamasından sonra bir daha hiç yeniden
atanmamalıdır, `final` sözcüğü yazılmasa bile.

- **FN11** — Aynı yöntem gövdesi iki biçimde denenir: yerel değişken yalnız bir kez atanıp
  lambda tarafından okunur; ikinci biçimde lambda kurulduktan sonra aynı değişkene bir kez
  daha atama yapılır.
- **FN12** — Derleyicinin ürettiği hata iletisinin metni basılmaz, yalnız derlenip
  derlenmediği bildirilir.

```java
// Derleme.java — ortak çekirdek: verilen kaynak metni derleniyor mu
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.*;
import java.util.spi.ToolProvider;

class Derleme {
    static boolean derlenirMi(String kaynak, String sinif) throws Exception {
        Path d = Files.createTempDirectory("olcek");
        Path k = d.resolve(sinif + ".java");
        Files.writeString(k, kaynak);
        PrintWriter bos = new PrintWriter(Writer.nullWriter());
        return ToolProvider.findFirst("javac").orElseThrow()
                .run(bos, bos, "-d", d.toString(), k.toString()) == 0;
    }
}
```

```java
// EtkinFinal.java — yerel degiskeni yeniden atamak lambda'yi neden bozuyor
public class EtkinFinal {
    static final String TEMEL = """
        import java.util.function.IntSupplier;
        class Deneme {
            static IntSupplier yap() {
                int sayac = 0;
                IntSupplier s = () -> sayac;
                %s
                return s;
            }
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("degismeyen yerel            : "
                + (Derleme.derlenirMi(TEMEL.formatted(""), "Deneme") ? "derlendi" : "derlenmedi"));
        System.out.println("lambda sonrasi yeniden atama: "
                + (Derleme.derlenirMi(TEMEL.formatted("sayac = 1;"), "Deneme") ? "derlendi" : "derlenmedi"));
    }
}
```

```
degismeyen yerel            : derlendi
lambda sonrasi yeniden atama: derlenmedi
```

`sayac` değişkenine lambda kurulduktan **sonra** bir kez daha atama yapılan ikinci biçim
derlenmiyor — ve bu, çalışma zamanında değil derleme zamanında düşen bir hatadır. Kaynak
kodda `sayac = 1;` satırı lambda'nın çağrılmasından önce mi sonra mı çalışacağı önemli
değildir; derleyici denetim akışını yürütmez, yalnız değişkenin kapsamı içinde **birden
fazla atama** olup olmadığına bakar. Bir atama varsa değişken etkin olarak final'dir ve
yakalanabilir; ikinci bir atama görülürse — denetim akışında hiç ulaşılmayacak olsa bile —
yakalama derlenmez.

Bu, önceki dersle aynı örüntüdür: derleyici burada da bir **yapıya** bakıyor, bir davranışa
değil. Yerel değişkenin gerçekten değişip değişmediğini değil, kaynakta kaç kez atandığını
sayıyor. Alan için böyle bir sayım yoktur — `carpan` alanı istenildiği kadar yeniden
atanabilir ve derleyici hiç itiraz etmez, çünkü alan yakalaması yerel değişken yakalaması
gibi bir kopya çıkarmaz. Kısıtın gerekçesi de buradan çıkıyor: bir yerel değişken yöntem
döndükten sonra yığıttan silinir, ama lambda ondan sonra da yaşayabilir; derleyici bu
çelişkiyi değişkenin değerini kopyalayıp lambda'nın kendi içine gömerek çözer ve kopyayı
tutarlı tutabilmek için o değişkenin tek atamalık kalmasını ister. Alan ise yığıtta değil
nesnenin ya da sınıfın kendisinde yaşar; lambda hangi nesneye ait olduğunu tutar, kopya
çıkarmaya gerek kalmaz.

#### Klasik ve Gelişmiş Döngüde Aynı Ad İki Ayrı Değişkendir

Etkin olarak final kısıtının en görünür sonucu döngülerde çıkar. Gelişmiş `for` döngüsü
(`for (String s : liste)`) ile klasik `for` döngüsü (`for (int i = 0; ...; i++)`) aynı işi
yapıyor gibi görünür, ama bir lambda döngü değişkenini yakalamak istediğinde ikisi ayrışır.

- **FN13** — Üç ögelik bir listeden gelişmiş `for` ile geçilir; her yinelemede kurulan
  lambda, o yinelemenin değişkenini bir listeye ekler. Döngü bitince listedeki lambdaların
  hepsi çağrılır.
- **FN14** — Aynı deneme klasik `for` ile, artan bir sayaçla tekrarlanır; burada beklenen
  şey çalışma zamanı sonucu değil, derlenip derlenmediğidir.

```java
// Dongu.java — klasik ve gelismis for donguleri lambda yakalamasinda ayni mi
import java.util.*;
import java.util.function.Supplier;

public class Dongu {
    public static void main(String[] args) {
        List<String> kaynak = List.of("armut", "kiraz", "elma");
        List<Supplier<String>> yakalayanlar = new ArrayList<>();
        for (String s : kaynak) {
            yakalayanlar.add(() -> s);
        }
        System.out.print("gelismis for, uc lambdanin sonucu: ");
        for (Supplier<String> y : yakalayanlar) System.out.print(y.get() + " ");
        System.out.println();
    }
}
```

```
gelismis for, uc lambdanin sonucu: armut kiraz elma
```

Üç lambda üç ayrı değer veriyor: `armut`, `kiraz`, `elma`. Bu, "yakalama değeri dondurur"
kuralıyla ilk bakışta çelişir gibi görünür — döngü değişkeni `s` üç kez yeniden atanmadı
mı? Hayır: gelişmiş `for` her yinelemede `s` adında **yeni** bir yerel değişken kurar; o
tek atamalık değişken bir sonraki yinelemede başka bir değişkenle değiştirilir, aynı
değişken yeniden atanmaz. Üç lambda üç ayrı `s`'i yakaladı, bu yüzden üç ayrı değer
donduruldu.

```java
// DonguKlasik.java — klasik for'da sayac degiskeni yakalanabiliyor mu
public class DonguKlasik {
    static final String KAYNAK = """
        import java.util.*;
        import java.util.function.IntSupplier;
        class Deneme {
            static void yap() {
                List<IntSupplier> yakalayanlar = new ArrayList<>();
                for (int i = 0; i < 3; i++) {
                    yakalayanlar.add(() -> i);
                }
            }
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("klasik for, i yakalanabiliyor mu: "
                + (Derleme.derlenirMi(KAYNAK, "Deneme") ? "derlendi" : "derlenmedi"));
    }
}
```

```
klasik for, i yakalanabiliyor mu: derlenmedi
```

Klasik `for` döngüsünde durum tam tersidir. `i` tek bir değişkendir ve `i++` her
yinelemede onu yeniden atar; üç yineleme, aynı `i` üzerinde üç ayrı atama demektir. Bu,
etkin olarak final tanımını doğrudan bozar ve lambda `i`'yi yakalamaya çalıştığı satırda
derleme durur — daha önce ölçülen `sayac = 1;` örneğiyle birebir aynı kural, burada döngü
gövdesinin içinde. İki döngü sözdizimsel olarak benzer görünse de, gelişmiş `for` her turda
yeni bir ad kurarken klasik `for` tek bir adı tekrar tekrar atıyor — ve lambda yakalaması
tam olarak bu farkı ortaya çıkarıyor.

#### Dört Sözdizimi, Aynı Yakalama Kuralı

Bir lambda ifadesinin gövdesi tek bir ifade olabilir, süslü parantezli bir blok olabilir,
parametre tipi açıkça yazılabilir ya da hiç yazılmayabilir — ve aynı işi yapan bir yöntem
zaten varsa, lambda hiç yazılmadan bir **yöntem referansı** (`Sınıf::yöntem`) ile de
belirtilebilir. Bu dört biçim ayrı ayrı sözdizimidir; hedef arayüz aynı olduğu sürece
dördü de aynı derleme yolundan geçer.

- **FN15** — Aynı `Function<Integer, Integer>` dört ayrı biçimde yazılır ve aynı girdiyle
  (`6`) çağrılır.

```java
// Sozdizim.java — dort ayri yazim, ayni sonuc
import java.util.function.Function;

public class Sozdizim {
    static int kare(int x) { return x * x; }

    public static void main(String[] args) {
        Function<Integer, Integer> ifade = x -> x * x;
        Function<Integer, Integer> govdeli = x -> { return x * x; };
        Function<Integer, Integer> tipYazili = (Integer x) -> x * x;
        Function<Integer, Integer> yontemReferansi = Sozdizim::kare;

        System.out.println("ifade biciminde     : " + ifade.apply(6));
        System.out.println("govde biciminde     : " + govdeli.apply(6));
        System.out.println("tip yazilarak       : " + tipYazili.apply(6));
        System.out.println("yontem referansiyla : " + yontemReferansi.apply(6));
    }
}
```

```
ifade biciminde     : 36
govde biciminde     : 36
tip yazilarak       : 36
yontem referansiyla : 36
```

Dört satır da aynı sonucu veriyor, çünkü dördü de aynı hedefe — `Function<Integer,
Integer>`'ın tek soyut yöntemine — bağlanıyor. `govdeli` biçimi `return` sözcüğünü zorunlu
kılar ve birden çok deyime izin verir; `ifade` biçimi tek bir değeri örtük olarak döndürür.
`tipYazili` parametre tipini açıkça yazar; `ifade` ve `govdeli` bu tipi hedef arayüzden
çıkarır. `yontemReferansi` ise hiç lambda yazmaz — zaten var olan `kare` yöntemine bir
isim üzerinden işaret eder.

Yöntem referansı yakalama kuralından muaf değildir, yalnız yakalayacağı bir şey olmayabilir.
Statik bir yönteme işaret eden `Sozdizim::kare` hiçbir şey yakalamaz, çünkü statik bir
yöntemin bağlı olduğu bir örnek yoktur. Ama bir **örnek** yöntemine, o örnek üzerinden
işaret eden bir referans — `liste::size` gibi — tıpkı `() -> liste.size()` lambda'sı gibi
`liste` değişkenini yakalar ve aynı kısıtlara, aynı sonuca tabidir; iki yazım aynı derleme
adımından geçer, yalnızca biri kısaltmadır. Sözdizim değişse de yakalama kuralı değişmiyor
— kuralın koşulu lambda anahtar sözcüğü değil, çevreleyen kapsamdan bir adın okunmasıdır.

#### Donan Değer, Donmayan Nesne

Buraya kadarki ölçüm bir yanılgıya açıktır: "yerel değişken sabittir" cümlesi, yakalanan
nesnenin de sabit kaldığı izlenimini verebilir. Sınırlayıcı ölçüm bunu ayırıyor.

- **FN16** — Bir liste değişkeni bir kez atanıyor ve hiç yeniden atanmıyor — etkin olarak
  final kuralını sağlıyor. Lambda listenin **boyutunu** okuyor, listenin kendisini değil.

```java
// Referans.java — yerel degisken degeri donduruyor, nesneyi dondurmuyor
import java.util.*;
import java.util.function.Supplier;

public class Referans {
    public static void main(String[] args) {
        List<String> liste = new ArrayList<>(List.of("alfa"));
        Supplier<Integer> boyutOku = () -> liste.size();
        System.out.println("lambda kurulunca boyut : " + boyutOku.get());
        liste.add("beta");
        liste.add("gama");
        System.out.println("liste degisince boyut  : " + boyutOku.get());
    }
}
```

```
lambda kurulunca boyut : 1
liste degisince boyut  : 3
```

`liste` değişkeni bir kez atandı ve bir daha hiç yeniden atanmadı; etkin olarak final
kısıtı baştan sona sağlandı. Buna rağmen `boyutOku.get()` iki çağrıda iki ayrı sayı
veriyor: **1**, sonra **3**. Çelişki görünüşte; kısıtın dondurduğu şey `liste`
değişkeninin **kendisi** — yani hangi nesneyi gösterdiği — oldu, o nesnenin **içeriği**
değil. `liste` hâlâ aynı `ArrayList` nesnesini gösteriyor, ama o nesnenin içine iki öge daha
eklendi ve lambda her çağrıda güncel içeriği okuyor.

Bu, bir önceki bölümün "yerel yakalama sabit kalır" gözlemini sınırlıyor: sabit kalan
referanstır, referansın gösterdiği değişebilir nesne değil. Etkin olarak final kuralı bir
değişmezlik (immutability) garantisi vermez; yalnızca bir **adın** akış boyunca aynı nesneyi
göstermesini zorunlu kılar. `İlkel Tipler ve Sarmalayıcılar` dersinde ölçülen değiştirilemez
dizgi ile burada yakalanan değişebilir liste bu yüzden ayrı davranıyor — ilkinde nesnenin
kendisi hiç değişemez, ikincisinde nesne yerinde değişebiliyor ve yakalama bunu engellemiyor.

#### Özet

- Bir lambda ifadesi yerel bir değişkeni yakaladığında o değişkenin **değerini** kopyalayıp
  içine gömer; alan yakaladığında bir kopya çıkarmaz, alana giden yolu tutar.
- Aynı gövdeli iki lambda, iki yakalama biçiminde iki ayrı sonuç sayısı üretir: yerel
  yakalamada değişmeyen tek bir sonuç, alan yakalamada alan değiştikçe değişen ayrı sonuçlar.
- Yakalanan yerel değişkenin **etkin olarak `final`** olması derleme zamanında zorunludur;
  lambda kurulduktan sonra bile olsa aynı değişkene ikinci bir atama yapılması derlemeyi
  durdurur.
- Alan yakalamada böyle bir kısıt yoktur; alan yakalama tarihinden sonra da yeniden
  atanabilir ve derleyici buna karışmaz.
- Sınırlayıcı ölçüm: etkin olarak final kısıtı değişkenin gösterdiği nesneyi değil, yalnız
  değişkenin kendisini dondurur. Yakalanan referansın gösterdiği liste sonradan değişirse
  lambda değişmiş içeriği görür.
- Lambda ifadesinin dört sözdizimi (ifade, gövde, açık tip, yöntem referansı) aynı hedefe
  bağlanır ve aynı sonucu verir; yöntem referansı yakalama kuralından muaf değildir, yalnız
  yakalayacağı bir şey olmayabilir.

#### Sonraki Adım

Bu ders tek bir lambda ifadesinin çevresinden ne aldığını ve dört sözdiziminin aynı hedefe
nasıl bağlandığını ölçtü. 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 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. Sıradaki ders bu soruyu bir
akış zincirinin on ayrı işlemi üzerinden koşturarak yanıtlar.

### Akış API'si

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/akis-apisi

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.

Ö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.
- **FN18** — `sorted()` doğal sırayla çalışır; harf büyütme `Locale.ROOT` ile yapılır.
- **FN19** — `distinct()` 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.

```java
// 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 garantisi**dir, 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()` 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.

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

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

```java
// 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 kaynağın tipinden bağımsızdır, 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.

### Toplayıcılar

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/toplayicilar

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: üç 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. Dört 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 üç 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?

### `Optional`

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/optional

Yokluğu tiple ifade etmek çağıranın yükümlülüğünü derleme zamanına taşımaz: dört denetimsiz çağrının dördü de — boş değer dönen imzada da, Optional dönen imzada da — denetimsiz derlenir ve ikisi çalışma zamanında düşer. Kazanç imzada görünürlüktür; bu kazanç alanda ve parametrede kayboluyor ve of() boş değerde hemen düşerken ofNullable() düşmüyor.

Önceki ders bir akış zincirinin sonunda biriken kabın hangi sözlerle kurulduğunu ölçtü.
Toplama her zaman bir değer üretir — 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ı `Optional<T>` denen, yokluğu **tiple** ifade eden bir kap döndürür. `Optional`
kendisi bir arayüz değil, tek bir sınıftır; kursun "garantiyi kim veriyor" sorusu burada
üçüncü bir biçimde sorulur: yokluğu tipe taşımak, çağıranın "önce denetle, sonra oku"
yükümlülüğünü gerçekten ortadan kaldırıyor mu, yoksa yalnızca **görünür** mü kılıyor?
Yokluk tipe taşındığında çağıranın yükümlülüğü derleme zamanına mı geçiyor, yoksa denetim
yine çalışma zamanında mı kalıyor?

#### Aynı İş, İki İmza: Denetim Yine Çalışma Zamanındadır

- **FN27** — Aynı arama işi iki imzayla yazılır: biri boş değer döndürebilen bir `String`,
  öteki bir `Optional<String>`. İkisi de var olan ve olmayan birer girdiyle, hem denetimsiz
  hem güvenli idyomla çağrılır.

```java
// Ayni.java — ayni is bos deger donen imza ile Optional donen imzada cagirana kalan denetim
import java.util.*;

public class Ayni {
    static final Map<String, String> DEPO = Map.of("armut", "yesil");

    static String renkBul(String meyve) { return DEPO.get(meyve); }
    static Optional<String> renkBulO(String meyve) { return Optional.ofNullable(DEPO.get(meyve)); }

    public static void main(String[] args) {
        int denetimliCagri = 0, denetimsizCagri = 0;

        try {
            int u1 = renkBul("armut").length();
            denetimsizCagri++;
            System.out.println("bos deger donen imza, var olan girdi : " + u1 + " (denetimsiz derlendi)");
        } catch (NullPointerException e) { System.out.println("beklenmedik istisna"); }

        try {
            int u2 = renkBul("elma").length();
            System.out.println("bos deger donen imza, olmayan girdi  : " + u2);
        } catch (NullPointerException e) {
            denetimsizCagri++;
            System.out.println("bos deger donen imza, olmayan girdi  -> istisna: "
                    + e.getClass().getSimpleName() + " (denetimsiz derlendi, calisma zamaninda dustu)");
        }

        try {
            int u3 = renkBulO("armut").get().length();
            denetimsizCagri++;
            System.out.println("Optional donen imza, var olan girdi  : " + u3 + " (denetimsiz derlendi)");
        } catch (NoSuchElementException e) { System.out.println("beklenmedik istisna"); }

        try {
            int u4 = renkBulO("elma").get().length();
            System.out.println("Optional donen imza, olmayan girdi   : " + u4);
        } catch (NoSuchElementException e) {
            denetimsizCagri++;
            System.out.println("Optional donen imza, olmayan girdi   -> istisna: "
                    + e.getClass().getSimpleName() + " (denetimsiz derlendi, calisma zamaninda dustu)");
        }

        System.out.println("guvenli idyom, bos deger donen imza  : "
                + (renkBul("elma") != null ? renkBul("elma") : "bilinmiyor"));
        denetimliCagri++;
        System.out.println("guvenli idyom, Optional donen imza   : " + renkBulO("elma").orElse("bilinmiyor"));
        denetimliCagri++;

        System.out.println();
        System.out.println("denetimsiz derlenen cagri sayisi (dort cagrinin dordu de): " + denetimsizCagri);
        System.out.println("denetimli (guvenli idyomla yazilan) cagri sayisi         : " + denetimliCagri);
    }
}
```

```
bos deger donen imza, var olan girdi : 5 (denetimsiz derlendi)
bos deger donen imza, olmayan girdi  -> istisna: NullPointerException (denetimsiz derlendi, calisma zamaninda dustu)
Optional donen imza, var olan girdi  : 5 (denetimsiz derlendi)
Optional donen imza, olmayan girdi   -> istisna: NoSuchElementException (denetimsiz derlendi, calisma zamaninda dustu)
guvenli idyom, bos deger donen imza  : bilinmiyor
guvenli idyom, Optional donen imza   : bilinmiyor

denetimsiz derlenen cagri sayisi (dort cagrinin dordu de): 4
denetimli (guvenli idyomla yazilan) cagri sayisi         : 2
```

Dört denetimsiz çağrının **dördü de** derleniyor — imza `String` olsun `Optional<String>`
olsun, derleyici hiçbirine itiraz etmiyor. Olmayan girdiyle çağrıldıklarında ikisi de
çalışma zamanında düşüyor; yalnızca istisnanın adı değişiyor: `NullPointerException` yerine
`NoSuchElementException`. Ölçülen sayı burada nettir: aynı işi yapan iki imzanın çağırana
bıraktığı, denetimsiz geçilebilecek çağrı sayısı **eşittir**. `Optional` dönüş tipi bu
sayıyı sıfıra indirmiyor; denetim yine yazılmadığında yine çalışma zamanında düşüyor.

Kazanç başka bir yerdedir: **imzanın kendisinde**. `String renkBul(String)` imzasını okuyan
biri, dönen değerin boş olabileceğini bilemez — bunu yalnızca belgeden ya da kaynağı okuyarak
öğrenir. `Optional<String> renkBulO(String)` imzası ise yokluk olasılığını doğrudan
söyler; çağıran bunu görmek için ne belgeye ne kaynağa bakmak zorundadır. Denetimin kendisi
hâlâ isteğe bağlıdır, ama denetimin **gerekip gerekmediği** artık tipte yazılıdır.

Bu ayrım Nesneye Dayalı Java kursunda kurulan denetlenen istisna ile karşılaştırılabilir:
orada bir yöntemin fırlatabileceği istisna imzada yazılıydı ve çağıran onu ele almak
**zorundaydı**, aksi hâlde derleme durmuyordu — yakalamamak da bir seçenekti, ama seçenek
en azından görünürdü. `Optional` dönüş tipi benzer bir görünürlük sağlıyor, ama zorunluluk
tarafı yoktur: denetlenen istisnayı hiç ele almayan kod derlenmezken, bir `Optional`'ı hiç
denetlemeden `get()` çağıran kod dördüncü satırda görüldüğü gibi sorunsuz derleniyor. İki
düzenek de yokluğu ya da hatayı imzaya taşıyor; yalnızca biri bunu zorunlu kılıyor, öteki
yalnızca görünür kılıyor.

#### Zincirleme Denetimi Ortadan Kaldırmaz, Erteler

`Optional` yalnız bir kap değildir; `map`, `orElse`, `orElseGet` gibi yöntemlerle akış
zincirine benzer bir zincirleme sunar. Bu zincirleme boş değeri hiç yazılmamış bir denetime
dönüştürmez, ama en azından her adımda tekrarlanan bir `if (x != null)` bloğunu ortadan
kaldırır.

- **FN28** — Aynı iki girdiyle (`"armut"`, `"elma"`) `renkBulO` çağrılır ve sonuç `map` ile
  büyük harfe çevrilip `orElse` ile varsayılan bir dizgiye bağlanır; ara adımlarda hiç açık
  denetim yazılmaz.

```java
// Zincir.java — Optional zincirlemesi ara denetimi nasil kaldiriyor
import java.util.*;

public class Zincir {
    static final Map<String, String> DEPO = Map.of("armut", "yesil");
    static Optional<String> renkBulO(String meyve) { return Optional.ofNullable(DEPO.get(meyve)); }

    public static void main(String[] args) {
        String buyukVarOlan = renkBulO("armut").map(s -> s.toUpperCase(Locale.ROOT)).orElse("YOK");
        String buyukOlmayan = renkBulO("elma").map(s -> s.toUpperCase(Locale.ROOT)).orElse("YOK");
        System.out.println("var olan girdi, zincirin sonucu   : " + buyukVarOlan);
        System.out.println("olmayan girdi, zincirin sonucu    : " + buyukOlmayan);
    }
}
```

```
var olan girdi, zincirin sonucu   : YESIL
olmayan girdi, zincirin sonucu    : YOK
```

`map(s -> s.toUpperCase(Locale.ROOT))` çağrısı, kap boşsa hiç çalışmaz — `Predicate` ve
`Function` derslerinde ölçülen "arayüz garantisi" burada da geçerlidir: `map`'in kendi
sözü, boş bir kap üzerinde işlev hiç çağırmamaktır. Bu yüzden `renkBulO("elma")` boş kap
döndürdüğünde `s.toUpperCase(...)` hiç çalışmıyor ve bir istisna düşmüyor; zincir doğrudan
`orElse("YOK")`'a düşüyor. Kazanç burada gerçek: aynı işi boş değerle yazsaydık, her adımda
ayrı bir `if` gerekirdi.

Ama bu zincirlemenin kendi bir tuzağı var. `orElse` ile `orElseGet` aynı işi yapıyor
görünür — ikisi de "yoksa şunu ver" der — fakat argümanlarını **ne zaman** hesapladıkları
farklıdır.

- **FN29** — Aynı pahalı varsayılan değer üretici hem dolu hem boş bir kapta, hem
  `orElse` hem `orElseGet` ile çağrılır; üreticinin kaç kez çalıştığı sayılır.

```java
// OrElse.java — orElse pahali varsayilani her zaman hesaplar, orElseGet yalnizca gerektiginde
import java.util.*;

public class OrElse {
    static int cagriSayaci = 0;

    static String pahaliVarsayilan() {
        cagriSayaci++;
        return "varsayilan";
    }

    public static void main(String[] args) {
        Optional<String> dolu = Optional.of("armut");

        cagriSayaci = 0;
        dolu.orElse(pahaliVarsayilan());
        System.out.println("dolu kapta orElse(), varsayilan kac kez cagrildi   : " + cagriSayaci);

        cagriSayaci = 0;
        dolu.orElseGet(OrElse::pahaliVarsayilan);
        System.out.println("dolu kapta orElseGet(), varsayilan kac kez cagrildi: " + cagriSayaci);

        Optional<String> bos = Optional.empty();
        cagriSayaci = 0;
        bos.orElse(pahaliVarsayilan());
        System.out.println("bos kapta orElse(), varsayilan kac kez cagrildi    : " + cagriSayaci);

        cagriSayaci = 0;
        bos.orElseGet(OrElse::pahaliVarsayilan);
        System.out.println("bos kapta orElseGet(), varsayilan kac kez cagrildi : " + cagriSayaci);
    }
}
```

```
dolu kapta orElse(), varsayilan kac kez cagrildi   : 1
dolu kapta orElseGet(), varsayilan kac kez cagrildi: 0
bos kapta orElse(), varsayilan kac kez cagrildi    : 1
bos kapta orElseGet(), varsayilan kac kez cagrildi : 1
```

`orElse` dört satırın **ikisinde de** bir kez çağrılıyor — kap dolu olsa bile. Bunun
nedeni sözdizimindedir: `orElse(pahaliVarsayilan())` yazıldığında `pahaliVarsayilan()`
çağrısı bir Java yöntem çağrısının argümanı olarak, `orElse`'in kendisi çalışmadan **önce**
değerlendirilir — kap dolu olsa bile üretici çalışır; sonucu `orElse` yalnızca kullanmaz.
`orElseGet(OrElse::pahaliVarsayilan)` ise bir üretici **referansı** alır ve onu yalnızca
kap boşken çağırır; dolu kapta üretici hiç çalışmıyor. Bu, önceki konudaki tembellik
ölçümünün burada tekrarıdır: bir işlemin ne zaman çalıştığı, o işlemi saran yöntemin adına
bakılarak değil, koşturularak anlaşılır.

#### Kazanç Alanda ve Parametrede Kayboluyor

`Optional`'ın kazandırdığı görünürlük, yalnızca dönüş tipi olarak kullanıldığında işler.
Bir alan ya da parametre tipi olarak kullanıldığında aynı kazanım kayboluyor.

- **FN30** — `Optional<String>` tipinde bir alan taşıyan bir sınıf kurulur ve o alana
  doğrudan boş değer atanır — derleyici buna izin verir, çünkü `Optional` da referans
  tipli sıradan bir sınıftır.

```java
// Alan.java — Optional bir alanda kullanilinca kazanc yok oluyor mu
import java.util.*;

public class Alan {
    static class Kayit {
        Optional<String> etiket;
    }

    public static void main(String[] args) {
        Kayit k = new Kayit();
        k.etiket = null;
        System.out.println("alanin kendisi bos deger olabiliyor mu: " + (k.etiket == null));
        try {
            boolean var = k.etiket.isPresent();
            System.out.println("alan uzerinde isPresent(): " + var);
        } catch (NullPointerException e) {
            System.out.println("alan uzerinde isPresent() -> istisna: " + e.getClass().getSimpleName());
        }
    }
}
```

```
alanin kendisi bos deger olabiliyor mu: true
alan uzerinde isPresent() -> istisna: NullPointerException
```

`Optional<String> etiket` alanına doğrudan `null` atamak **derleniyor**. `Optional` bir
tip parametresi taşıyan sıradan bir sınıftır; kendisi de bir referanstır ve her referans
gibi boş olabilir. Alan boşken `isPresent()` çağırmak bu kez `NullPointerException`
düşürüyor — tam olarak, `Optional`'ın önlemek için var olduğu istisnanın aynısı, yalnızca
bir kat dışarıda.

Bu, dersin sınırlayıcı ölçümüdür: `Optional`'ın kazancı yalnızca bir yöntemin **dönüş
tipi** olarak kullanıldığı dar bir bağlamda geçerlidir. Bir alanda ya da bir parametrede
kullanıldığında iki kat yokluk olasılığı doğar — alanın kendisi boş olabilir, alanın
taşıdığı değer de boş olabilir — ve kazanılan görünürlük, kazandığından fazlasını geri
alan bir karmaşıklığa dönüşür. Standart kütüphanenin kendi belgesi bu kullanımı önerilmez
sayar; burada ölçülen, o önerinin gerekçesidir.

Nedeni birinci bölümdeki ölçümle birleştiğinde netleşiyor: `Optional`'ın tek koruması
imzada duran bir **hatırlatmadır**, çalışma zamanında uygulanan bir kural değil. Dönüş
tipi olarak kullanıldığında bu hatırlatma her çağrı yerinde yeniden görünür — yöntemi her
çağıran, dönen değerin `Optional` olduğunu görür ve karar verir. Alan olarak kullanıldığında
ise hatırlatma yalnız sınıfın **tanımında** bir kez görünür; sınıfı kullanan kod alana her
eriştiğinde o hatırlatmayı yeniden okumaz, çünkü erişim bir alan okuması olarak görünür,
`Optional`'ın kendisi görünmez hâle gelir. Görünürlüğün kazancı, tam olarak görünürlüğün
kaybolduğu yerde biter.

#### `of` Hemen Düşer, `ofNullable` Düşmez

`Optional` bir kap kurmanın iki ana yolu var: `Optional.of(deger)` ve
`Optional.ofNullable(deger)`. İkisi de aynı tipte bir kap döndürür, ama boş değerle
çağrıldıklarında ayrışırlar.

- **FN31** — `Optional.of(null)` ve `Optional.ofNullable(null)` ayrı ayrı denenir;
  ikincisinin sonucundan `get()` çağrılarak denetimin ne zaman düştüğü ölçülür.

```java
// OfOfNullable.java — of hemen dusuyor, ofNullable dusmuyor
import java.util.*;

public class OfOfNullable {
    public static void main(String[] args) {
        try {
            Optional<String> o = Optional.of(null);
            System.out.println("of(null) kurulurken: dustu");
        } catch (NullPointerException e) {
            System.out.println("of(null) kurulurken -> istisna: " + e.getClass().getSimpleName());
        }

        Optional<String> on = Optional.ofNullable(null);
        System.out.println("ofNullable(null) kurulurken: dustu degil, sonuc=" + on);
        try {
            on.get();
            System.out.println("ofNullable(null).get(): basarili");
        } catch (NoSuchElementException e) {
            System.out.println("ofNullable(null).get() -> istisna: " + e.getClass().getSimpleName());
        }
    }
}
```

```
of(null) kurulurken -> istisna: NullPointerException
ofNullable(null) kurulurken: dustu degil, sonuc=Optional.empty
ofNullable(null).get() -> istisna: NoSuchElementException
```

`Optional.of(null)` **kurulma anında** düşüyor: `of` çağrısı "bu değerin boş olmadığını
biliyorum" demektir ve boş bir değerle çağrıldığında bu iddia derhal yanlışlanıyor.
`Optional.ofNullable(null)` ise düşmüyor — boş olabileceğini zaten kabul ediyor ve boş bir
kap (`Optional.empty()`) döndürüyor. Kırılma orada değil, kabın **içeriği okunduğunda**
oluyor: `get()` çağrısı `NoSuchElementException` ile düşüyor. İki yöntem de sonunda aynı
noktaya varabiliyor — boş bir kaptan `get()` çağırmak — ama `of` bunu hemen, kurulum
satırında yakalarken `ofNullable` bu riski kabın kendisiyle birlikte taşıyor ve denetimi
kabın kullanıldığı satıra erteliyor.

Bu erteleme bir kusur değil, tam olarak `Optional`'ın işidir: `ofNullable` çağrıldığı
yerde henüz kimse kabın nasıl kullanılacağını bilmiyor olabilir — belki hemen `map`
zincirine girecek, belki bir alana atanacak, belki hiç okunmayacak. `of` ise tersini
varsayar: burada, tam bu satırda, değerin dolu olduğunu iddia eder ve bu iddiayı
doğrulamak için beklemez. İkisi arasındaki fark, dersin başında ölçülen çağıranın
yükümlülüğü sorusuna küçük bir yanıt veriyor: yükümlülük hiçbir zaman ortadan kalkmıyor,
yalnızca **ne zaman** düşeceği çağıranın seçtiği yönteme göre değişiyor. Hangisinin
seçileceği çağıranın kararıdır: girdinin boş olamayacağından eminse `of` erken bir uyarı
sistemi kurar; girdinin boş olabileceğini zaten biliyorsa `ofNullable` bunu bir istisna
değil, sıradan bir durum olarak temsil eder.

#### Özet

- Boş değer dönen bir imza ile `Optional` dönen bir imza, denetimsiz bırakılan çağrı
  sayısı bakımından eşittir: dört denetimsiz çağrının dördü de derlenir, ikisi çalışma
  zamanında düşer. `Optional` bu sayıyı azaltmaz.
- Kazanılan şey imzanın görünürlüğüdür: `Optional<T>` dönüş tipi yokluk olasılığını
  belgeye bakmadan, doğrudan imzada gösterir.
- `Optional` zincirlemeyi destekler: `map` boş bir kap üzerinde hiç çalışmaz, ara adımlarda
  ayrı bir boş değer denetimi yazmaya gerek kalmaz. Ama `orElse` argümanını kap dolu olsa
  bile her zaman hesaplar; `orElseGet` yalnızca kap boşken çalışır — ikisi aynı işi
  yapıyor görünse de bedelleri farklıdır.
- Sınırlayıcı ölçüm: `Optional` bir alan ya da parametre olarak kullanıldığında kazanç
  kaybolur — alanın kendisi boş olabilir ve bu, önlenmek istenen istisnayı bir kat dışarı
  taşır.
- `Optional.of(null)` kurulma anında `NullPointerException` ile düşer; `Optional.ofNullable(null)`
  düşmez, boş bir kap döndürür ve denetim `get()` çağrısına ertelenir. Seçim çağıranındır.

#### Sonraki Adım

Bu konuda tek bir tipin — fonksiyonel arayüzün, lambda'nın, akış zincirinin, toplayıcının,
`Optional`'ın — kütüphaneye ne söz verdiği ölçüldü ve her seferinde söz derleyicinin
denetlediği kadar dardı: tek soyut yöntem tutulur, yerel yakalama dondurulur, sıra
çoğunlukla kaynaktan gelir, kabın tipi çoğunlukla belirsizdir. `Optional` de aynı dar
sözün son örneğidir — yokluk tiple söylendi, yükümlülük imzaya taşındı, ama denetim yine
çalışma zamanında kaldı. Sıradaki konu aynı soruyu artık dilin kendi tipleri üzerinde değil,
dış dünyayla kurulan sınırda sorar: baytın, dosyanın, saatin ve metnin standart kütüphaneye
ne söz verdiğini ölçer.

## Girdi/Çıktı ve Zaman

Bayt ve karakter akışlarının ayrıldığı yer ve kodlamanın çağırana bıraktığı karar, dosya sistemi API'sinin yol ve dizin işlemlerinde verdiği sözler, nesne dizileştirmenin kurucu düzeneği atlaması, tarih–zaman tiplerinin taşıdığı ve taşımadığı bilgi, örüntü derleme ile eşleştirmenin davranışı.

### Akış Tabanlı Girdi/Çıktı

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/akis-tabanli-girdi-cikti

Bayt akışının write(int) için düşük sekiz bitten başka söz vermediği, karakter akışının bir kodlamaya yaslandığı ve tamponlamanın sözü değil yalnız alt akışa varan çağrı sayısını değiştirdiği ölçülür. Sınırlayıcı ölçüm, boşaltılmamış bir tamponun veriyi eksik bıraktığını gösterir.

Önceki konunun son dersi yokluğu bir tipe taşıdı: `Optional` boş olduğunda okuma yine çalışma
zamanında düşüyordu; denetim ortadan kalkmadı, yalnız yeri değişti. Bu konu aynı soruyu **dış
dünyaya** sorar. Bir bayt dizisini ya da bir metni okurken kütüphanenin sana verdiği söz nedir
ve o sözü kimin taşıdığını — arayüzün mü, seçtiğin sınıfın mı, yoksa senin mi — çağrı yazımına
bakarak ayırt edebilir misin?

Bu derste iki ayrı aile karşılaştırılır: bayt akışı ve karakter akışı. İkisi de `java.io`
girdi-çıktı akışıdır; Java'nın `Stream` veri akışı ayrı bir API'dir ve bu konu boyunca hep
niteleyiciyle anılacaktır. Soru hep aynı biçimde sorulur: bir davranış iki ayrı
gerçekleştirimde de görülüyorsa arayüzün sözüdür; yalnız birinde görülüyorsa seçilen sınıfa
aittir; hiç görülmüyor ama kod hâlâ çalışıyorsa çağıranın uyduğu bir kuraldır.

Beş bölüm sırayla ilerliyor. İlk ikisi bayt seviyesinde neyin garanti edildiğini ölçüyor —
yazmada ve okumada. Üçüncüsü tamponlamanın bu iki sözü hiç bozmadan yalnız çağrı sayısını
değiştirdiğini gösteriyor. Dördüncüsü karakter akışının bayt akışına eklediği tek şeyin, yani
kodlamanın, çağırana yüklediği kararı ölçüyor. Beşincisi ise hiçbir sınıfın vermediği, yalnız
çağıranın uyması gereken bir kuralı: bir tamponun boşaltılması.

#### Bayt Akışının Verdiği Tek Söz

- **IO1** — `OutputStream.write(int)` bir tamsayı alır ama yalnız düşük sekiz bitini yazar;
  kalan yirmi dört bit yok sayılır. Bu ölçüm, iki ayrı gerçekleştirimin aynı değeri yazıp aynı
  düşük sekiz biti üretip üretmediğine bakar.
- **IO2** — Yazılan bayt geri okunurken `& 0xFF` ile işaretsiz tamsayıya çevrilir; imzalı
  `byte` değeri doğrudan karşılaştırılmaz.

```java
// TekSoz.java — bayt akisinin write(int) icin verdigi tek soz: dusuk sekiz bit
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class TekSoz {
    public static void main(String[] args) throws Exception {
        int deger = 321;
        int beklenen = deger & 0xFF;
        System.out.printf("%-24s%-10s%s%n", "gerceklestirim", "yazilan", "beklenenle ayni mi");

        ByteArrayOutputStream dizi = new ByteArrayOutputStream();
        dizi.write(deger);
        int diziSonuc = dizi.toByteArray()[0] & 0xFF;
        System.out.printf("%-24s%-10d%s%n", "ByteArrayOutputStream", diziSonuc, diziSonuc == beklenen);

        Path dosya = Path.of("tek-soz.bin");
        try (OutputStream dosyaAkisi = new FileOutputStream(dosya.toFile())) {
            dosyaAkisi.write(deger);
        }
        int dosyaSonuc = Files.readAllBytes(dosya)[0] & 0xFF;
        System.out.printf("%-24s%-10d%s%n", "FileOutputStream", dosyaSonuc, dosyaSonuc == beklenen);
        Files.delete(dosya);
    }
}
```

```
gerceklestirim          yazilan   beklenenle ayni mi
ByteArrayOutputStream   65        true
FileOutputStream        65        true
```

Yazılan değer 321; ikilik tabanda alt sekiz biti `01000001`, yani 65. Bellekte tutan bir
gerçekleştirim ile bir dosyaya yazan gerçekleştirim, aralarında hiçbir ortak kod satırı
olmamasına rağmen aynı düşük sekiz biti üretiyor. Bu, `OutputStream` soyut sınıfının
belgelediği bir sözdür — gerçekleştirim seçimi burada hiçbir şeyi değiştirmiyor, çünkü
davranış zaten üst sınıfın kendisinde tanımlı.

Bayt akışının verdiği söz tam olarak bu kadardır: sekiz bitlik bir birimi olduğu gibi taşımak.
Değerin ne anlama geldiği — bir karakter mi, bir sayı mı, bir görüntünün parçası mı —
akışın hiç bilmediği bir şeydir. Bayt akışı hiçbir **yorum** sözü vermez; yalnız birim taşır.
`OutputStream` bir Java `interface`'i değil soyut sınıftır, ama ölçünün sorduğu soruyu
değiştirmiyor: davranış üst tipte tanımlıysa, o tip bir arayüz gibi konuşuyor demektir —
onu konuşan her alt sınıf sözü aynen taşımak zorunda.

#### Okuma Ucunda Aynı Söz

- **IO3** — `InputStream.read()` de bir sözdür: okunan bayt işaretsiz bir tamsayı olarak,
  0 ile 255 arasında döner; girdi-çıktı akışı bittiğinde dönen değer bir bayt değil, akış
  dışı bir işaretçidir: **-1**.
- **IO4** — Ölçülen dizi üç bayt taşıyor ve üçüncüsü, imzalı `byte` olarak okunsaydı negatif
  çıkacak bir değer (255). `read()`'in işaretsize çevirmesi bu değeri de doğru taşıyıp
  taşımadığı burada görülüyor.

```java
// Okuma.java — read() icin verdigi tek soz: 0-255 arasi deger, sonda -1
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class Okuma {
    static List<Integer> oku(InputStream a) throws Exception {
        List<Integer> degerler = new ArrayList<>();
        int d;
        while ((d = a.read()) != -1) degerler.add(d);
        degerler.add(a.read());
        return degerler;
    }

    public static void main(String[] args) throws Exception {
        byte[] veri = { 10, (byte) 200, (byte) 255 };

        List<Integer> diziSonuc = oku(new ByteArrayInputStream(veri));

        Path dosya = Path.of("okuma.bin");
        Files.write(dosya, veri);
        List<Integer> dosyaSonuc;
        try (InputStream dosyaAkisi = new FileInputStream(dosya.toFile())) {
            dosyaSonuc = oku(dosyaAkisi);
        }
        Files.delete(dosya);

        System.out.printf("%-24s%s%n", "gerceklestirim", "okunan degerler (son ikisi: son bayt, dosya sonu)");
        System.out.printf("%-24s%s%n", "ByteArrayInputStream", diziSonuc);
        System.out.printf("%-24s%s%n", "FileInputStream", dosyaSonuc);
        System.out.println("iki dizi ayni mi: " + diziSonuc.equals(dosyaSonuc));
    }
}
```

```
gerceklestirim          okunan degerler (son ikisi: son bayt, dosya sonu)
ByteArrayInputStream    [10, 200, 255, -1]
FileInputStream         [10, 200, 255, -1]
iki dizi ayni mi: true
```

Üç bayttan üçüncüsü, imzalı `byte` olarak yorumlansaydı **-1** çıkardı; `read()` onu
işaretsiz bir değere çevirip **255** olarak döndürüyor. Bellekteki dizi de dosya da aynı üç
değeri ve ardından aynı işaretçiyi veriyor. Bu `-1` okunmuş bir bayt değildir — hiçbir gerçek
bayt bu değeri taşıyamaz, çünkü işaretsize çevrilmiş bir bayt her zaman 0 ile 255 arasındadır.
`-1`, akışın **bittiğini** söyleyen, veri uzayının dışında duran bir işarettir.

Yazmada düşük sekiz bitin korunması ile okumada işaretsize çevirme ve dosya sonu işareti,
birlikte bayt akışının bütün sözleşmesini kuruyor. İkisi de gerçekleştirimden bağımsız, çünkü
ikisi de `InputStream` ve `OutputStream`'in kendisinde tanımlı; bir alt sınıf bu davranışı
değiştiremez, yalnız *nasıl* sağladığını — bellekte mi, diskte mi — seçebilir.

#### Tamponlamanın Sözü Değil Çağrı Sayısını Değiştirmesi

- **IO5** — Alt akışa varan her yazma çağrısını sayan bir sarmalayıcı yazılır; bu sarmalayıcı
  yalnız sayaç tutar, veriyi değiştirmez.
- **IO6** — Aynı beş parça, önce doğrudan bu sarmalayıcıya, sonra `BufferedOutputStream`
  üzerinden aynı sarmalayıcıya yazılır. Karşılaştırılan iki şey ayrıdır: alt akışa varan çağrı
  sayısı ve alt akışın topladığı içerik.

```java
// SayacAkis.java — alttaki akisa varan yazma cagrisini sayan sarmalayici (yardimci sinif, main yok)
import java.io.IOException;
import java.io.OutputStream;

class SayacAkis extends OutputStream {
    private final OutputStream alt;
    int cagriSayisi = 0;

    SayacAkis(OutputStream alt) { this.alt = alt; }

    @Override public void write(int b) throws IOException {
        cagriSayisi++;
        alt.write(b);
    }

    @Override public void write(byte[] veri, int bas, int uzunluk) throws IOException {
        cagriSayisi++;
        alt.write(veri, bas, uzunluk);
    }

    @Override public void flush() throws IOException { alt.flush(); }
    @Override public void close() throws IOException { alt.close(); }
}
```

```java
// Tamponlama.java — tamponlamanin sozu degil, alt akisa varan cagri sayisini degistirdigi
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.util.List;

public class Tamponlama {
    static final List<byte[]> PARCALAR = List.of(
            "veri".getBytes(), "-".getBytes(), "kaydi".getBytes(),
            "-".getBytes(), "01".getBytes());

    public static void main(String[] args) throws Exception {
        ByteArrayOutputStream altDogrudan = new ByteArrayOutputStream();
        SayacAkis sayacDogrudan = new SayacAkis(altDogrudan);
        for (byte[] parca : PARCALAR) sayacDogrudan.write(parca, 0, parca.length);
        sayacDogrudan.close();

        ByteArrayOutputStream altTamponlu = new ByteArrayOutputStream();
        SayacAkis sayacTamponlu = new SayacAkis(altTamponlu);
        try (BufferedOutputStream tamponlu = new BufferedOutputStream(sayacTamponlu)) {
            for (byte[] parca : PARCALAR) tamponlu.write(parca, 0, parca.length);
        }

        System.out.printf("%-14s%-18s%s%n", "yazim", "alt cagri sayisi", "teslim edilen icerik");
        System.out.printf("%-14s%-18d%s%n", "dogrudan", sayacDogrudan.cagriSayisi,
                altDogrudan.toString());
        System.out.printf("%-14s%-18d%s%n", "tamponlu", sayacTamponlu.cagriSayisi,
                altTamponlu.toString());
        System.out.println("icerik ayni mi: " + altDogrudan.toString().equals(altTamponlu.toString()));
    }
}
```

```
yazim         alt cagri sayisi  teslim edilen icerik
dogrudan      5                 veri-kaydi-01
tamponlu      1                 veri-kaydi-01
icerik ayni mi: true
```

Doğrudan yazımda beş parça, sayaç akışına beş ayrı çağrı olarak ulaşıyor: her `write` alt
akışa hemen düşüyor. Tamponlu yazımda aynı beş parça sayaç akışına **bir** çağrı olarak
ulaşıyor, çünkü `BufferedOutputStream` parçaları kendi belleğinde biriktirip yalnız
boşalttığında alt akışı çağırıyor. İki yazımın da teslim ettiği içerik birebir aynı.

Bu ölçüm tamponlamanın ne değiştirip ne değiştirmediğini ayırıyor. `OutputStream`'in verdiği
söz "yazdığın baytlar sonunda alt akışa ulaşacak" biçimindedir; "her `write` çağrısı alt akışa
anında dokunacak" biçiminde değildir. Tamponlama bu ikinci cümleyi hiç ihlal etmiyor, çünkü o
cümle zaten sözün bir parçası değildi. Değişen şey yalnız çağıranın göremeyeceği bir sayı:
alt akışa varan çağrı sayısı.

Bu yüzden tamponlama bir garanti değil bir seçimdir. Alt akış gerçekten pahalıysa — bir dosya,
bir ağ bağlantısı — beş ayrı çağrı beş ayrı sistem çağrısına dönüşebilir; bir tampon araya
girdiğinde aynı iş tek çağrıya iner. Seçim çağırana ait olduğu için `BufferedOutputStream`
hiçbir zaman kendiliğinden boşalmaz: tamponu ne zaman dolduracağına, ne zaman erken
boşaltacağına yalnız çağıran karar verir. Bu dersin son bölümündeki sınırlayıcı ölçüm tam
olarak bu kararın atlandığı durumu gösteriyor.

#### Karakter Akışının Kodlamaya Yaslanması

- **IO7** — Bir metin tek bir kodlamayla bayta çevrilip yazılır. Aynı bayt dizisi iki ayrı
  kodlamayla geri okunur: yazıldığı kodlamayla ve başka bir kodlamayla.
- **IO8** — Karşılaştırılan üç şey: bayt sayısı (sabit kalır, çünkü kaynak aynıdır), karakter
  sayısı ve okunan metnin yazılanla eşitliği. Metnin kendisi değil, uzunluk ve eşitlik basılır.

```java
// Kodlama.java — karakter akisinin verdigi soz kodlamaya baglidir, bayt akisininki degildir
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class Kodlama {
    public static void main(String[] args) throws Exception {
        String turkce = "ağırlık";
        Path dosya = Path.of("kodlama.bin");

        byte[] bayt = turkce.getBytes(StandardCharsets.UTF_8);
        Files.write(dosya, bayt);

        String ayniKodlama = new String(Files.readAllBytes(dosya), StandardCharsets.UTF_8);
        String farkliKodlama = new String(Files.readAllBytes(dosya), StandardCharsets.ISO_8859_1);

        System.out.printf("%-26s%-12s%-16s%s%n", "okuma kodlamasi", "bayt sayisi", "karakter sayisi", "yazilanla ayni mi");
        System.out.printf("%-26s%-12d%-16d%s%n", "UTF_8 (yazildigi gibi)", bayt.length,
                ayniKodlama.length(), ayniKodlama.equals(turkce));
        System.out.printf("%-26s%-12d%-16d%s%n", "ISO_8859_1 (baska kodlama)", bayt.length,
                farkliKodlama.length(), farkliKodlama.equals(turkce));

        Files.delete(dosya);
    }
}
```

```
okuma kodlamasi           bayt sayisi karakter sayisi yazilanla ayni mi
UTF_8 (yazildigi gibi)    10          7               true
ISO_8859_1 (baska kodlama)10          10              false
```

Yedi harflik metin UTF-8 ile on bayt üretiyor, çünkü `ğ` ve `ı` bu kodlamada iki baytla
gösteriliyor. Aynı on bayt UTF-8 ile geri okunduğunda yedi karaktere dönüyor ve yazılanla
birebir eşleşiyor. Aynı on bayt ISO-8859-1 ile okunduğunda her bayt kendi başına bir karakter
sayılıyor ve sonuç on karakter — yazılan metinle eşleşmiyor.

Bu ölçüm çağıranın bir kuralını gerçekten kırıyor: yazma ve okuma kodlamasının aynı olması.
Kırılmanın sonucu ne bir istisnadır ne de bir uyarı; **sessiz** bir sonuçtur. Program
hatasız çalışır, bir dizgi üretir ve o dizgi yanlıştır. Bayt akışının hiçbir yorum sözü
vermediği önceki bölümde kuruldu; karakter akışı tam olarak bu boşluğu dolduruyor, ama
doldurduğu kodlamayı **sen** vermek zorundasın. Kodlama verilmediğinde çalışma zamanı bir
varsayılana düşer; Java 18'den beri bu varsayılan, işletim sisteminden ve yerel ayardan
bağımsız olarak UTF-8'dir — bu derste her okuma ve yazmada kodlamanın açıkça verilmesinin
nedeni tam olarak yukarıdaki tablodur: iki taraf farklı kodlama kullandığında hata hiçbir
yerde durmaz, yalnız sayı değişir.

Bu yükün nerede taşındığı da ölçülebilir bir seçimdir. `OutputStreamWriter` ve
`InputStreamReader` — bayt akışını karakter akışına çeviren iki sınıf — kurucularında bir
`Charset` parametresi ister; `Files.write` ve `Files.readAllBytes` ise kodlamadan tamamen
habersizdir, çünkü ikisi de yalnız bayt taşır ve kodlama kararını çağırana, yukarıdaki gibi
`getBytes` ve `String` kurucusuna bırakır. Hangi katmanda verilirse verilsin, karar sözün bir
parçası değildir; her okuma ve her yazma kendi kodlamasını açıkça taşımak zorundadır.

#### Boşaltılmamış Tampon

- **IO9** — Bir `BufferedOutputStream` yazıldıktan hemen sonra, `flush` çağrılmadan, alttaki
  akışın içeriğine doğrudan bakılır. İkinci ölçümde aynı adımlar `flush` ile tamamlanır.

```java
// Bosaltma.java — bosaltilmamis tampon veriyi tasimaz, bosaltma karari cagirana aittir
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;

public class Bosaltma {
    public static void main(String[] args) throws Exception {
        ByteArrayOutputStream altKapatilmadan = new ByteArrayOutputStream();
        BufferedOutputStream tamponluKapatilmadan = new BufferedOutputStream(altKapatilmadan);
        tamponluKapatilmadan.write("teslim edilmeyecek".getBytes());

        ByteArrayOutputStream altBosaltilan = new ByteArrayOutputStream();
        BufferedOutputStream tamponluBosaltilan = new BufferedOutputStream(altBosaltilan);
        tamponluBosaltilan.write("teslim edilecek".getBytes());
        tamponluBosaltilan.flush();

        System.out.printf("%-26s%-12s%s%n", "olcum", "alt uzunluk", "alt akista veri var mi");
        System.out.printf("%-26s%-12d%s%n", "flush cagrilmadan",
                altKapatilmadan.size(), altKapatilmadan.size() > 0);
        System.out.printf("%-26s%-12d%s%n", "flush cagrildiktan sonra",
                altBosaltilan.size(), altBosaltilan.size() > 0);
    }
}
```

```
olcum                     alt uzunluk alt akista veri var mi
flush cagrilmadan         0           false
flush cagrildiktan sonra  15          true
```

`flush` çağrılmadan alttaki akışın uzunluğu sıfır: on sekiz baytlık metin hâlâ
`BufferedOutputStream`'in kendi belleğinde bekliyor, alt akışa hiç dokunulmamış. `flush`
çağrıldıktan sonra aynı düzenek on beş baytı eksiksiz teslim ediyor. Aradaki fark kodun
doğruluğunda değil, **bir çağrının yapılıp yapılmadığındadır** — ve o çağrıyı yapmak
tamamen çağıranın kararıdır.

Kaynakla deneme (M08/K02 Nesneye Dayalı Java, `java-generics/04`) bu çağrıyı otomatikleştirir:
bir `BufferedOutputStream` `try (...)` içinde açılırsa, bloktan çıkarken `close` çalışır ve
`close` içeriden `flush`'ı çağırır. Ama bu otomasyonun devreye girmesi için tamponlu akışın
**kaynak olarak bildirilmiş olması** gerekir; yukarıdaki ilk ölçümde `tamponluKapatilmadan`
hiçbir `try (...)` bloğuna girmediği için kapatma hiç çalışmadı ve veri kayboldu. Kaynakla
deneme kaybı önler, ama önleyecek satırı yazmak yine çağırana kalır.

Bazı sınıflar bu kararı kendi üzerlerine alır: `PrintWriter`'ın bir kurucusu her satır
sonunda otomatik boşaltma isteyip istemediğini bir `boolean` parametreyle sorar.
`BufferedOutputStream`'in böyle bir seçeneği yok — o hep manuel kalır. Bu fark bile aynı
kuralı doğruluyor: otomatik boşaltma bir sınıfın *tercih ettiği* bir davranıştır, girdi-çıktı
akışı sınıflarının **hepsinin** uyduğu bir arayüz sözü değildir. Hangi sınıfı seçtiğin, boşaltma
kararını kendine mi bırakacağını yoksa sınıfa mı devredeceğini de belirliyor.

#### Özet

- Bayt akışının `write(int)` için verdiği tek söz düşük sekiz biti taşımaktır; bu söz farklı
  gerçekleştirimlerde aynı çıkıyor, çünkü üst sınıfın kendisinde tanımlıdır.
- Tamponlama alt akışa varan çağrı sayısını değiştirir, teslim edilen içeriği değiştirmez;
  beş çağrı bire iniyor ama sonuç birebir aynı kalıyor.
- Karakter akışı bir kodlamaya yaslanır; bayt akışı hiçbir yorum sözü vermez. Kodlama
  verilmediğinde çalışma zamanının varsayılanı devreye girer; Java 18'den beri bu
  varsayılan UTF-8'dir.
- Yazma ve okuma kodlaması ayrıldığında hata hiçbir yerde durmaz: yedi karakterlik bir metin
  sessizce on karaktere dönüşür.
- Sınırlayıcı ölçüm: boşaltılmamış bir tampon veriyi eksik bırakır. Kaynakla deneme bu kaybı
  önler, ama tamponlu akışı kaynak olarak bildirme kararı çağırana aittir.

#### Sonraki Adım

Bu derste ölçülen her şey bir akışın içinden geçen içerikle ilgiliydi: hangi bayt yazıldı,
hangi karakter okundu. Ama bir akış açılmadan önce bir soru daha var: akışın ucundaki şey
gerçekten var mı ve ona giden yol ne anlatıyor? Sıradaki ders dosya sistemi API'sine bakar —
bir yolun kendisinin ayrıştırılmış bir değer olduğunu, bu değer üzerindeki hangi işlemlerin
dosya sistemine hiç dokunmadan yanıt verdiğini ve hangilerinin gerçekten diske indiğini ölçer.

### Dosya Sistemi API'si

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/dosya-sistemi-apisi

Bir yolun ayrıştırılmış bir değer olduğu, normalleştirme ve çözümlemenin dosya sistemine hiç dokunmadan yanıt verdiği iki ayrı sağlayıcıda koşturularak gösterilir. Dizin gezinme sırasının belirtilmemiş olduğu ve varlık denetimiyle açmanın iki ayrı işlem olduğu ölçülür.

Önceki ders bir girdi-çıktı akışının içinden geçen içerikle ilgiliydi. Ama akış açılmadan önce
bir soru daha var: akışın ucundaki şey gerçekten var mı ve ona giden yol ne anlatıyor? Bu ders dosya
sistemi API'sine bakar — bir yolun kendisinin ayrıştırılmış bir değer olduğunu, bu değer
üzerindeki hangi işlemlerin dosya sistemine hiç dokunmadan yanıt verdiğini ve hangilerinin
gerçekten diske indiğini ölçer.

`Path` bir dizgi değildir. `"a/../b"` yazısı içinde tutulan bir metin gibi görünür, ama
üzerinde çağrılan yöntemler bir metin işleme kütüphanesinin değil, bir **dosya sistemi
sağlayıcısının** kurallarını izler. Bu dersin sorusu kursun sorusuyla aynı biçimde sorulur:
hangi davranış sağlayıcı seçiminden bağımsızdır — yani arayüzün sözüdür; hangisi sağlayıcının
kendi deposuna sorulmadan yanıtlanamaz — yani depo sorgusudur; ve hangi güvence hiçbir
yöntemin vermediği, yalnız çağrıları doğru sırada yapmakla elde edilen bir kuraldır.

Dört bölüm sırayla ilerliyor. İlk ikisi aynı yol nesnesi üzerinde çağrılan iki tür yöntemi
ayırıyor: depoya hiç dokunmayanlar ve depoya gerçekten sorulanlar. Üçüncüsü bir dizinin
listelenme sırasının nereden geldiğini ve neyi garanti etmediğini ölçüyor. Dördüncüsü, tek
bir yöntemin verdiği sözü iki ayrı çağrıya bölmenin bedelini gösteriyor. Dördü de aynı
ilkeye çıkıyor: bir yöntemin adı değil, ne zaman ve hangi kaynağa sorduğu, o yöntemin
verdiği sözü belirliyor.

#### Yol Cebiri: Dosya Sistemine Hiç Dokunmadan

- **IO10** — Aynı üç işlem — normalleştirme, çözümleme, dosya adı ayırma — iki ayrı dosya
  sistemi sağlayıcısında çalıştırılır: varsayılan yerel sağlayıcı ve standart kitaplığın kendi zip
  sağlayıcısı (bir zip arşivini bir dosya sistemi gibi açan, standart kitaplığın parçası
  olan sağlayıcı).
- **IO11** — Karşılaştırılan şey iki sonucun **eşitliğidir**; hiçbir yol dizgisinin kendisi
  yorum gerektirmez, çünkü sonuçlar zaten insan tarafından okunabilir kısa dizgilerdir.
  Aynı ölçüm `equals` ve `compareTo` için de tekrarlanır ve ayrıca yazı olarak aynı iki yol
  farklı sağlayıcılardan geldiğinde `equals`'ın ne dediğine bakılır.

Bu ölçüm için iki ayrı sınıf değil, iki ayrı **sağlayıcı** karşılaştırılıyor: varsayılan
yerel sağlayıcı ve standart kitaplığın kendi zip sağlayıcısı, bir zip arşivini bir dosya sistemi gibi
açan, standart kitaplığın parçası olan `FileSystemProvider` gerçekleştirimi. Kursun önceki
derslerinde "gerçekleştirim" bir `List` ya da `Map` seçimiydi; burada aynı rolü sağlayıcı
seçimi oynuyor — hangi dosya sisteminin arkasında çalıştığın, `Path` arayüzünün üzerinde
çağırdığın yöntemleri değiştirmiyor.

```java
// SaglayiciDeneme.java — yol cebiri iki dosya sistemi saglayicisinda da ayni mi
import java.net.URI;
import java.nio.file.*;
import java.util.Map;

public class SaglayiciDeneme {
    public static void main(String[] args) throws Exception {
        Path arsiv = Path.of("arsiv.zip");
        Files.deleteIfExists(arsiv);
        try (FileSystem zip = FileSystems.newFileSystem(
                URI.create("jar:" + arsiv.toUri()), Map.of("create", "true"))) {

            System.out.printf("%-14s%-20s%-20s%s%n", "islem", "yerel", "zip", "ayni mi");

            String yerelNorm = Path.of("a/./b/../c").normalize().toString();
            String zipNorm = zip.getPath("a/./b/../c").normalize().toString();
            System.out.printf("%-14s%-20s%-20s%s%n", "normalize", yerelNorm, zipNorm,
                    yerelNorm.equals(zipNorm));

            String yerelCoz = Path.of("kok").resolve("alt/dosya.txt").toString();
            String zipCoz = zip.getPath("kok").resolve("alt/dosya.txt").toString();
            System.out.printf("%-14s%-20s%-20s%s%n", "resolve", yerelCoz, zipCoz,
                    yerelCoz.equals(zipCoz));

            String yerelAd = Path.of("dizin/dosya.txt").getFileName().toString();
            String zipAd = zip.getPath("dizin/dosya.txt").getFileName().toString();
            System.out.printf("%-14s%-20s%-20s%s%n", "getFileName", yerelAd, zipAd,
                    yerelAd.equals(zipAd));

            String yerelGoreli = Path.of("a/b/c").relativize(Path.of("a/x")).toString();
            String zipGoreli = zip.getPath("a/b/c").relativize(zip.getPath("a/x")).toString();
            System.out.printf("%-14s%-20s%-20s%s%n", "relativize", yerelGoreli, zipGoreli,
                    yerelGoreli.equals(zipGoreli));

            boolean yerelEsit = Path.of("a/b").equals(Path.of("a/b"));
            boolean zipEsit = zip.getPath("a/b").equals(zip.getPath("a/b"));
            System.out.printf("%-14s%-20s%-20s%s%n", "equals (kendi)", yerelEsit, zipEsit,
                    yerelEsit == zipEsit);

            boolean yerelSira = Path.of("a/b").compareTo(Path.of("a/c")) < 0;
            boolean zipSira = zip.getPath("a/b").compareTo(zip.getPath("a/c")) < 0;
            System.out.printf("%-14s%-20s%-20s%s%n", "compareTo<0", yerelSira, zipSira,
                    yerelSira == zipSira);

            System.out.println("yerel a/b, zip a/b'ye esit mi (ayni yazi, farkli saglayici): "
                    + Path.of("a/b").equals(zip.getPath("a/b")));
        }
        Files.deleteIfExists(arsiv);
    }
}
```

```
islem         yerel               zip                 ayni mi
normalize     a/c                 a/c                 true
resolve       kok/alt/dosya.txt   kok/alt/dosya.txt   true
getFileName   dosya.txt           dosya.txt           true
relativize    ../../x             ../../x             true
equals (kendi)true                true                true
compareTo<0   true                true                true
yerel a/b, zip a/b'ye esit mi (ayni yazi, farkli saglayici): false
```

Altı işlemin altısı da iki sağlayıcıda birebir aynı sonucu veriyor. `normalize` `.` ve `..`
segmentlerini aynı kuralla eritiyor, `resolve` bir yolu bir başkasının üzerine aynı biçimde
ekliyor, `relativize` bir yoldan diğerine giden göreli yolu aynı biçimde hesaplıyor,
`equals` ve `compareTo` da segment listesini karşılaştırıyor. Bu altı işlemin **hiçbiri**
çalışırken sağlayıcının deposuna bakmıyor — zip arşivi henüz boş, yerel dizinde de bu adlarda
hiçbir şey yok ve yine de altısı da sonuç üretiyor. Sonuç sağlayıcıdan bağımsız olduğu için
bu davranış `Path`'in kendi arayüz sözüdür: yol cebiri bir segment listesi üzerinde çalışan
saf bir hesaptır, deponun içeriğine hiç sormaz.

Son satır bu sözün sınırını gösteriyor. Yazı olarak birebir aynı iki yol — biri yerel
sağlayıcıdan, biri zip sağlayıcısından — `equals` ile karşılaştırıldığında **eşit değil**.
`Path.equals` yalnız segmentlere değil, hangi sağlayıcıya ait olduğuna da bakıyor; bu da
sağlayıcıdan bağımsız bir kuraldır, çünkü iki sağlayıcı da bu kısıtlamayı aynı biçimde
uyguluyor. Yol cebirinin verdiği söz "aynı segmentler aynı sonucu üretir" değil, "aynı
sağlayıcıdaki aynı segmentler aynı sonucu üretir" biçimindedir.

Bu saflığın ölçülebilir bir sonucu var: yol cebiri üzerine yazılan bir kural — bir uzantının
doğrulanması, bir üst dizine çıkışın engellenmesi, bir göreli yolun hesaplanması — hiçbir
dosya oluşturmadan, hiçbir dizin kurmadan sınanabilir. Yukarıdaki ölçümün kendisi de bunu
gösteriyor: zip arşivi boş açıldı, yerel dizinde ilgili adlarda hiçbir şey yoktu ve altı
karşılaştırmanın altısı da hatasız tamamlandı. Depoya dokunan bir işlem olsaydı, aynı ölçüm
önce bir kurulum gerektirirdi.

#### Aynı Sorunun Depoya Dokunan Yanıtı

- **IO12** — Aynı iki sağlayıcıda bu kez `Files.exists` çağrılır. Zip sağlayıcısındaki yola
  önce bir kayıt yazılır, yerel sağlayıcıdaki aynı adlı dosya ise hiç oluşturulmaz.

```java
// DepoyaDokunma.java — depoya dokunan islemler saglayiciya bakar, yol cebiri hicbir zaman bakmaz
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.Map;

public class DepoyaDokunma {
    public static void main(String[] args) throws Exception {
        Path arsiv = Path.of("arsiv.zip");
        Files.deleteIfExists(arsiv);
        try (FileSystem zip = FileSystems.newFileSystem(
                URI.create("jar:" + arsiv.toUri()), Map.of("create", "true"))) {

            Path zipDosya = zip.getPath("kayit.txt");
            Files.writeString(zipDosya, "veri", StandardCharsets.UTF_8);

            Path yerelDosya = Path.of("kayit.txt");
            Files.deleteIfExists(yerelDosya);

            System.out.printf("%-14s%s%n", "saglayici", "Files.exists sonucu");
            System.out.printf("%-14s%s%n", "yerel", Files.exists(yerelDosya));
            System.out.printf("%-14s%s%n", "zip", Files.exists(zipDosya));
        }
        Files.deleteIfExists(arsiv);
    }
}
```

```
saglayici     Files.exists sonucu
yerel         false
zip           true
```

Aynı ad, iki ayrı sağlayıcıda iki ayrı yanıt veriyor — ve bu bir kusur değil, `Files.exists`
gerçekten ne yaptığının kanıtı. Bir önceki bölümdeki altı işlemden farklı olarak `exists`
sağlayıcının deposuna gerçekten sorar; yanıt "hayır" ya da "evet" olabilir ve bu yanıt
depoda o an ne olduğuna bağlıdır. `Files.newInputStream`, `Files.size`, `Files.readAllBytes`,
`Files.isDirectory` aynı ailededir: hepsi çağrıldıkları anda sağlayıcının deposuna gider. Bir
yol nesnesi üzerinde çağrılan yöntemin depoya dokunup dokunmadığını anlamanın kestirme yolu
şudur: sonuç depo boşken de üretilebiliyorsa cebirdir; depo dolu olmayı gerektiriyorsa depo
sorgusudur.

Bu ayrımın pratik bir sonucu var: bir yol nesnesi, gösterdiği dosya hiç yokken bile güvenle
kurulabilir, saklanabilir, başka yollarla birleştirilebilir. Henüz yazılmamış bir çıktı
dizinini `resolve` ile önceden adlandırmak hiçbir işlemi patlatmaz, çünkü cebir hiçbir zaman
depoya sormaz. Sorun ancak `Files.createDirectories` ya da `Files.newOutputStream` gibi
depoya dokunan bir çağrı geldiğinde ortaya çıkabilir — ve o an hatanın nerede arandığı
bellidir, çünkü depoya dokunan yöntemler kısıtlı ve adı bilinen bir kümedir.

Depoya dokunan her yanıtın bir de zaman boyutu var: `exists` çağrıldığı **anda** doğru olan
bir cevap veriyor, o cevabın çağrının bittiği andan sonra da doğru kalacağına dair hiçbir söz
vermiyor. Yol cebiri bu bakımdan daha güçlü bir garanti taşıyor — `normalize` bir kere
hesaplandıktan sonra bir daha hiç geçersiz olmaz, çünkü depoya hiç bakmadı. Depo sorgusunun
sonucu ise bir anlık görüntüdür ve bu dersin son bölümü tam olarak bu anlık görüntünün ne
kadar kısa ömürlü olduğunu ölçüyor.

#### Dizin Gezinme Sırası Belirtilmemiştir

- **IO13** — Bir dizine dört dosya belli bir sırayla oluşturulur. `Files.list` ile elde
  edilen gezinme sırası hem oluşturma sırasıyla hem alfabetik sırayla karşılaştırılır.
- **IO14** — Gezinme sırasının kendisi basılmaz; yalnız iki karşılaştırmanın sonucu ve iki
  ayrı gezinme yönteminin birbiriyle tutarlı olup olmadığı basılır. `Files.list`'in
  döndürdüğü şey de bir kaynaktır — arkasında bir dizin tanıtıcısı tutar — ve kaynakla
  deneme (M08/K02 Nesneye Dayalı Java, `java-generics/04`) ile açılır.

```java
// GezinmeSirasi.java — dizin gezinme sirasi belirtilmemistir, siralamayi cagiran yapar
import java.nio.file.*;
import java.util.*;
import java.util.stream.Collectors;

public class GezinmeSirasi {
    public static void main(String[] args) throws Exception {
        Path dizin = Path.of("gezinme");
        Files.createDirectories(dizin);
        List<String> olusturmaSirasi = List.of("cok.txt", "az.txt", "orta.txt", "bir.txt");
        for (String ad : olusturmaSirasi) Files.createFile(dizin.resolve(ad));

        List<String> gezinmeListe;
        try (var akis = Files.list(dizin)) {
            gezinmeListe = akis.map(p -> p.getFileName().toString()).collect(Collectors.toList());
        }
        List<String> gezinmeDizinAkisi = new ArrayList<>();
        try (DirectoryStream<Path> dizinAkisi = Files.newDirectoryStream(dizin)) {
            for (Path p : dizinAkisi) gezinmeDizinAkisi.add(p.getFileName().toString());
        }
        List<String> alfabetik = gezinmeListe.stream().sorted().collect(Collectors.toList());

        System.out.println("gezinme sirasi olusturma sirasiyla ayni mi: "
                + gezinmeListe.equals(olusturmaSirasi));
        System.out.println("gezinme sirasi alfabetik siralamayla ayni mi: "
                + gezinmeListe.equals(alfabetik));
        System.out.println("Files.list ile dizin akisi ayni sirayi mi veriyor: "
                + gezinmeListe.equals(gezinmeDizinAkisi));

        for (String ad : olusturmaSirasi) Files.deleteIfExists(dizin.resolve(ad));
        Files.deleteIfExists(dizin);
    }
}
```

```
gezinme sirasi olusturma sirasiyla ayni mi: false
gezinme sirasi alfabetik siralamayla ayni mi: false
Files.list ile dizin akisi ayni sirayi mi veriyor: true
```

Gezinme sırası ne oluşturma sırasıyla ne de alfabetik sırayla örtüşüyor; sağlayıcı kendi
deposunu kendi iç düzenine göre listeliyor ve bu düzen `Files.list`'in belgelenmiş bir sözü
değil. Buna karşılık `Files.list` (bir akış döndüren yöntem) ile `Files.newDirectoryStream`
(bir dizin akışı döndüren, girdi-çıktı akışı duyusunda bir yineleyici arayüzü) aynı gezinmede
**aynı** sırayı veriyor — çünkü ikisi de aynı sağlayıcının aynı iç listeleme çağrısını
sarmalıyor. İki API'nin birbirleriyle tutarlı olması, gezinmenin **belgelenmiş** bir sırası
olduğu anlamına gelmiyor; yalnız aynı sağlayıcı çağrısına dayandıklarını gösteriyor. Sıralı
bir sonuç isteyen çağıran listeyi kendisi sıralamak zorundadır; API bu sözü hiçbir yerde
vermiyor. Sıra dosya sistemine de bağlıdır: bazı dosya sistemlerinde, örneğin macOS'un
APFS'inde, az sayıda dosyada gezinme oluşturma sırasını koruyabilir; oluşturma sırası
satırındaki `false` bir garanti değil, bu koşumun dosya sisteminin sonucudur.

Belirsizliğin kaynağı sağlayıcının kendi depolama yapısıdır. Bir dizin girdisi çoğunlukla
adına göre değil, dosya sisteminin kendi iç dizinleme düzenine göre tutulur ve listeleme o
iç düzeni olduğu gibi dolaşır. `Files.list`'in belgesi bu düzeni hiç taahhüt etmez; taahhüt
ettiği tek şey her girdinin listede **bir kez** görüneceğidir. Bu, önceki dersteki tamponlama
ölçümüyle aynı biçimdedir: API'nin vermediği bir sözü kendiliğinden var saymak, kod çalıştığı
sürece fark edilmeyen ama taşındığında ortaya çıkan bir bağımlılık kurar.

#### Varlık Denetimi ile Açma İki Ayrı İşlemdir

Bu bölüm önceki bölümün son cümlesini sayısal bir ölçüme çeviriyor. `Files.exists` ile
`Files.newInputStream` iki ayrı depo sorgusudur ve aralarında geçen zaman sıfır değildir;
o boşlukta çalışan kod deponun durumunu değiştirebilir. Ölçüm bunu gerçek bir silme ile
kuruyor, ama kaynağı önemli değil — aynı boşluğu başka bir işlem, başka bir iş parçacığı ya
da kullanıcının kendisi de doldurabilir.

- **IO15** — Bir dosya yazılır, `Files.exists` ile varlığı denetlenir, hemen ardından dosya
  silinir — başka bir kod yolunun araya girdiği kabulü. Denetimden sonra dosya açılmaya
  çalışılır.
- **IO16** — Karşılaştırma için aynı açma çağrısı hiç var olmamış bir dosya üzerinde de
  doğrudan denenir; iki hatanın aynı sınıftan geldiği görülür.

```java
// IkiAdim.java — varlik denetimi ile acma iki ayri islemdir, aralarinda bosluk vardir
import java.nio.file.*;

public class IkiAdim {
    public static void main(String[] args) throws Exception {
        Path dosya = Path.of("gecici-kayit.txt");
        Files.writeString(dosya, "kayit");

        boolean denetimSonucu = Files.exists(dosya);
        Files.delete(dosya);

        String acmaSonucu;
        try {
            Files.newInputStream(dosya).close();
            acmaSonucu = "acildi";
        } catch (NoSuchFileException e) {
            acmaSonucu = "acilamadi: " + e.getClass().getSimpleName();
        }
        System.out.printf("%-18s%s%n", "denetim sonucu", denetimSonucu);
        System.out.printf("%-18s%s%n", "acma sonucu", acmaSonucu);

        String dogrudanSonuc;
        try {
            Files.newInputStream(Path.of("hic-olmayan.txt")).close();
            dogrudanSonuc = "acildi";
        } catch (NoSuchFileException e) {
            dogrudanSonuc = "acilamadi: " + e.getClass().getSimpleName();
        }
        System.out.printf("%-18s%s%n", "dogrudan acma", dogrudanSonuc);
    }
}
```

```
denetim sonucu    true
acma sonucu       acilamadi: NoSuchFileException
dogrudan acma     acilamadi: NoSuchFileException
```

Denetim `true` döndürdükten hemen sonra açma başarısız oluyor, çünkü ikisi arasında dosya
silindi. Bu kusur `Files.exists`'in yalan söylemesinden gelmiyor — denetim çağrıldığı anda
doğru cevabı verdi. Kusur, denetim ile açmanın **iki ayrı çağrı** olmasından, aralarında
geçen zamanda depoyu başka bir şeyin değiştirebilmesinden geliyor. Son satır bunu doğruluyor:
hiç var olmamış bir dosyayı doğrudan açmaya çalıştığımda da aynı istisna, aynı sınıftan
düşüyor. API zaten "aç ya da hata ver" sözünü **tek çağrıda** sunuyor; denetim adımını önce
ekleyen çağırandır ve o ek adım güvenlik kazandırmıyor, yalnız aradaki boşluğu büyütüyor.
Doğru düzen denetlemeden açmak ve açmanın kendi istisnasını yakalamaktır.

Bu, kütüphanenin bir eksikliği değil — arayüz zaten tek bir çağrıda hem sonucu hem hatayı
taşıyacak biçimde tasarlanmış. Aradaki boşluk yalnız çağıran iki çağrıyı birbirinden ayırıp
arasına başka kod koyduğunda ortaya çıkıyor; ne kadar kısa görünürse görünsün, iki adımın
arasında geçen her satır o boşluğu genişletiyor. Bu, kursun "çağıranın yükümlülüğü" dediği
şeyin en yalın biçimidir: kütüphane yanlış hiçbir şey söylemedi, çağıran doğru bilgiyi yanlış
zamanda kullandı.

#### Özet

- `Path` bir dizgi değil ayrıştırılmış bir değerdir; `normalize`, `resolve`, `getFileName`,
  `relativize`, `equals` ve `compareTo` dosya sistemine hiç dokunmadan çalışır ve bu davranış
  iki ayrı sağlayıcıda da aynıdır.
- Yazı olarak aynı iki yol farklı sağlayıcılardan geldiğinde `equals` ile eşit çıkmaz;
  eşitlik segmentlere değil, segmentler artı sağlayıcıya bakar.
- `Files.exists`, `Files.size`, `Files.readAllBytes` gibi işlemler sağlayıcının deposuna
  gerçekten sorar; yanıt depodaki gerçek duruma bağlıdır ve sağlayıcı değişince değişebilir.
- Dizin gezinme sırası ne oluşturma sırasıyla ne alfabetik sırayla örtüşür ve API bu sırayı
  hiçbir yerde belgelemez; sıralı bir sonuç isteyen çağıran listeyi kendisi sıralar.
- İki ayrı gezinme yöntemi aynı sırayı vermesi, sıranın belgelendiği anlamına gelmez — yalnız
  aynı sağlayıcı çağrısına dayandıklarını gösterir.
- Sınırlayıcı ölçüm: varlık denetimi ile açma iki ayrı işlemdir. Aralarında dosya silinirse
  denetim geçer, açma düşer; API zaten tek çağrıda "aç ya da hata ver" sunar, ek denetim
  adımı çağıranın kendi eklediği bir boşluktur.

#### Sonraki Adım

Bu derste bir dosyanın var olup olmadığı, hangi baytları taşıdığı ölçüldü — ama bir nesnenin
kendisi bayta çevrildiğinde ne olduğu hiç sorulmadı. Sıradaki ders dizileştirmeye bakar: bir
nesne bayt dizisine döküldüğünde ve geri okunduğunda, o nesnenin yapıcısının hiç çalışmadığını
ve sınıfın kurduğu değişmezlerin bu yüzden dışarıdan verilen baytlarla kırılabildiğini ölçer.

### Dizileştirme

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/dizilestirme

Dizileştirmenin yapıcıyı hiç çalıştırmadığı ve yapıcının kurduğu değişmezlerin bu yüzden dışarıdan gelen bir bayt dizisiyle atlanabildiği ölçülür. Bir geri okuma kancasının denetimi nasıl geri getirdiği ve alan adıyla sürüm kimliğinin nasıl bir sözleşmeye dönüştüğü gösterilir.

Önceki ders bir dosyanın var olup olmadığını ve hangi baytları taşıdığını ölçtü — ama bir
nesnenin kendisi bayta çevrildiğinde ne olduğu hiç sorulmadı. Bu ders dizileştirmeye bakar.
Nesneye Dayalı Java kursu (M08/K02, `java-classes/01`, `02`) yapıcı zincirini ve sınıf
değişmezlerini kurdu: bir nesne ancak yapıcısından geçerek var olabilir ve yapıcı değişmezi
zorlayan yerdir. Dizileştirme bu kuralın **dışında** durur — bir nesneyi bayta çevirip geri
okumak, yapıcıyı hiç çağırmadan bir nesne üretmenin standart kitaplıktaki yoludur.

Kursun sorusu burada yeniden sorulur: yapıcının kurduğu bir değişmez, geri okumada hâlâ
geçerli mi? Yanıtı bulmak için tek bir sınıf yeter, çünkü karşılaştırılan iki gerçekleştirim
değil, aynı sınıfın iki farklı yoldan — yapıcıdan ve dizileştirmeden — geçerek var olmasıdır.

Üç bölüm sırayla ilerliyor. İlk bölüm yapıcının atlandığını ve bunun bedelini sayıyor.
İkincisi bu bedelin nasıl geri kazanılabildiğini gösteriyor. Üçüncüsü ise değişmezlerin
ötesine geçip sınıfın **kendi şeklinin** — alan adlarının ve sürüm kimliğinin — geri okumada
nasıl bir sözleşmeye dönüştüğünü ölçüyor.

Bu, K03'ün "çağıranın yükümlülüğü" dediği kaynaklardan biridir, ama alışılmadık bir biçimde:
çağıran burada değişmezi kıran taraf değil, değişmezi **geri kazanmakla** yükümlü taraftır.
Kütüphane bir söz vermedi ve tutmadığı bir sözü de yok; boşluğu dolduracak satırı yazmak
baştan beri sınıfı tasarlayana aitti.

#### Yapıcı Hiç Çalışmıyor

- **IO17** — `Hesap` sınıfı yapıcısında iki değişmez zorluyor: sahip adı boş olamaz, bakiye
  negatif olamaz. İkisi de tek bir `if` satırıyla, yapıcının içinde denetleniyor.
- **IO18** — Elde, yapıcı hiç çağrılmadan üretilmiş bir bayt dizisi var: sahibi boş, bakiyesi
  negatif bir `Hesap` kaydı. Bu derste o bayt dizisinin **nasıl** üretildiği önemli değil;
  önemli olan geri okunduğunda ne olduğu. Risk, yapıcının kurduğu değişmezlerden kaçının
  geri okumada geçerli kaldığı sayısıyla ölçülüyor.

`Serializable`'ı uygulayan **her** sınıf bu ölçümün konusu olabilir; arayüz hiçbir yöntem
istemez, yalnız bir işarettir. `Hesap` burada seçilmiş özel bir örnek değil, kütüphanenin
herhangi bir dizileştirilebilir sınıfa uyguladığı genel bir davranışın tek bir örneğidir.
Aşağıdaki ölçüm bu davranışı elde bulunan bir bayt dizisiyle gösteriyor; o dizinin bir
dosyadan mı, bir ağ bağlantısından mı, yoksa programın kendi önceki bir çalışmasından mı
geldiği ölçüm açısından fark etmiyor — hangi kaynaktan geldiğine bakmaksızın, geri okuma
aynı yolu izliyor.

```java
// YapiciAtlanir.java — dizilestirme yapiciyi hic calistirmaz, yapicinin kurdugu degismezler korunmaz
import java.io.*;
import java.util.Base64;

class Hesap implements Serializable {
    private static final long serialVersionUID = 1L;
    private final String sahip;
    private final long bakiye;

    Hesap(String sahip, long bakiye) {
        if (sahip == null || sahip.isBlank()) throw new IllegalArgumentException("bos sahip");
        if (bakiye < 0) throw new IllegalArgumentException("negatif bakiye");
        this.sahip = sahip;
        this.bakiye = bakiye;
    }

    String sahip() { return sahip; }
    long bakiye() { return bakiye; }
}

public class YapiciAtlanir {
    // Yapici hic calistirilmadan uretilmis bir bayt dizisi: sahip bos, bakiye negatif.
    static final String BOZUK_KAYIT =
            "rO0ABXNyAAVIZXNhcAAAAAAAAAABAgACSgAGYmFraXllTAAFc2FoaXB0ABJMamF2YS9sYW5nL1N0cmluZzt4cP/////////OdAAA";

    static int gecerliDegismez(Hesap h) {
        int sayi = 0;
        if (h.sahip() != null && !h.sahip().isBlank()) sayi++;
        if (h.bakiye() >= 0) sayi++;
        return sayi;
    }

    public static void main(String[] args) throws Exception {
        int yapiciEngeli = 0;
        try {
            new Hesap("", -50L);
        } catch (IllegalArgumentException e) {
            yapiciEngeli = 1;
        }
        System.out.println("yapici iki degismezi de zorluyor mu: " + (yapiciEngeli == 1));

        byte[] bayt = Base64.getDecoder().decode(BOZUK_KAYIT);
        Hesap geriGelen;
        try (ObjectInputStream i = new ObjectInputStream(new ByteArrayInputStream(bayt))) {
            geriGelen = (Hesap) i.readObject();
        }
        System.out.println("geri okuma yapiciyi hic calistirmadan basarili mi: true");
        System.out.printf("yapicinin kurdugu iki degismezden gecerli kalan: %d / 2%n",
                gecerliDegismez(geriGelen));
    }
}
```

```
yapici iki degismezi de zorluyor mu: true
geri okuma yapiciyi hic calistirmadan basarili mi: true
yapicinin kurdugu iki degismezden gecerli kalan: 0 / 2
```

İlk satır, doğrudan yapıcıdan geçmeye çalışan bozuk bir çağrının reddedildiğini doğruluyor:
denetim orada çalışıyor. İkinci satır aynı sınıfın **aynı bozukluğu** taşıyan bir kaydı
geri okuyabildiğini gösteriyor — hiçbir istisna düşmüyor; `readObject` sorunsuzca
tamamlanıyor. Üçüncü satır bunun bedelini sayıyor: yapıcının kurduğu iki değişmezden
**sıfırı** geri okumada geçerli kalıyor.

Bunun nedeni `ObjectInputStream`'in nasıl çalıştığıdır. Varsayılan geri okuma, sınıfın
alanlarına girdi-çıktı akışındaki değerleri **doğrudan** yazar; bunu yaparken sınıfın kendi
yapıcısını hiç çağırmaz, hiçbir
`if` satırından geçmez. `final` alanlar bile bu yoldan yazılabilir, çünkü dizileştirme dilin
kendi düzeneğidir ve yapıcının önündeki kapıdan girmez, nesneyi doğrudan bellekte kurar. Bu,
K02'de kurulan "bir nesne ancak yapıcısından geçerek var olabilir" kuralının **tek**
istisnasıdır — ve istisna kuralın bir zayıflığı değil, dizileştirmenin **tanımının**
kendisidir: geri okumanın işi bir yapıcı çağırmak değil, bir nesneyi aynen yeniden kurmaktır.

Bu yol tamamen kapalı da değildir: `Hesap` `Serializable`'ı hiç uygulamasaydı, `writeObject`
çağrısının kendisi `NotSerializableException` ile düşerdi ve geri okuma sorusu hiç
gündeme gelmezdi. Arayüzü konuşmak, sınıfı bu ikinci yola **açan** karardır — ve bu karar
sınıfı tasarlayanın kendisine aittir, dizileştirmenin dayattığı bir şey değildir.

K02'nin kapsülleme dersi, bir alanı `private` yapmanın onu yalnız sınıfın kendi yöntemleri
aracılığıyla değiştirilebilir kıldığını kurmuştu. Dizileştirme bu duvarı yıkmaz ama
**yanından dolaşır**: `private` değiştirici hâlâ yoktur, hiçbir sıradan çağrı `bakiye`
alanına dışarıdan doğrudan atama yapamaz — ama geri okuma sıradan bir çağrı değil, sanal
makinenin kendi düzeneğidir ve erişim belirtecine hiç takılmaz. Kapsülleme dışarıdan gelen
**yöntem çağrılarını** kısıtlar; dizileştirme bir yöntem çağrısı değil, dilin kendisine ait
bir yeniden kurma işlemidir.

#### Geri Okuma Kancası Denetimi Geri Getiriyor

`ObjectInputStream`'in belgesi bir kaçış yolu tanımlar: bir sınıf, imzası tam olarak
`private void readObject(ObjectInputStream)` olan bir yöntem tanımlarsa, geri okuma
varsayılan alanı doldurma davranışını bırakıp bu yöntemi çağırır. İmza harfiyen uyulması
gereken bir sözleşmedir — yöntem `public` olursa, parametre tipi değişirse ya da adı farklı
yazılırsa geri okuma onu hiç görmez ve sessizce varsayılan davranışa döner.

- **IO19** — `Hesap`'a `readObject` adında özel bir yöntem eklenir. Bu yöntem önce
  `defaultReadObject` ile alanları her zamanki gibi doldurur, sonra yapıcıdaki **aynı iki**
  denetimi tekrarlar.

```java
// GeriOkumaKancasi.java — readObject kancasi yazildiginda iki degismez de geri kontrol ediliyor
import java.io.*;
import java.util.Base64;

class Hesap implements Serializable {
    private static final long serialVersionUID = 1L;
    private final String sahip;
    private final long bakiye;

    Hesap(String sahip, long bakiye) {
        if (sahip == null || sahip.isBlank()) throw new IllegalArgumentException("bos sahip");
        if (bakiye < 0) throw new IllegalArgumentException("negatif bakiye");
        this.sahip = sahip;
        this.bakiye = bakiye;
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        if (sahip == null || sahip.isBlank()) throw new InvalidObjectException("bos sahip");
        if (bakiye < 0) throw new InvalidObjectException("negatif bakiye: " + bakiye);
    }
}

public class GeriOkumaKancasi {
    static final String BOZUK_KAYIT =
            "rO0ABXNyAAVIZXNhcAAAAAAAAAABAgACSgAGYmFraXllTAAFc2FoaXB0ABJMamF2YS9sYW5nL1N0cmluZzt4cP/////////OdAAA";

    public static void main(String[] args) throws Exception {
        byte[] bayt = Base64.getDecoder().decode(BOZUK_KAYIT);
        String sonuc;
        try (ObjectInputStream i = new ObjectInputStream(new ByteArrayInputStream(bayt))) {
            i.readObject();
            sonuc = "acildi";
        } catch (InvalidObjectException e) {
            sonuc = "reddedildi: " + e.getMessage();
        }
        System.out.println("kanca yazildiktan sonra ayni bozuk kayit: " + sonuc);
    }
}
```

```
kanca yazildiktan sonra ayni bozuk kayit: reddedildi: bos sahip
```

Bir önceki bölümde okunan **aynı** bayt dizisi bu kez reddediliyor. Değişen tek şey sınıfın
içine iki satırlık bir yöntem eklenmesi: `defaultReadObject` çağrısı alanları her zamanki
gibi dolduruyor; ardından gelen iki `if` ise yapıcıdakiyle **birebir aynı** denetimi tekrar
kuruyor. `ObjectInputStream`, bir sınıfta `readObject` adında böyle bir yöntem bulursa
varsayılan davranış yerine onu çağırıyor; yöntem bir istisna fırlatırsa geri okuma da
yarım kalıyor ve nesne hiç üretilmiyor.

Bu ölçümün söylediği şey şudur: dizileştirmenin sözü **eksik değildi**. Söz zaten "bir
nesneyi aynen yeniden kurarım, alanlarını doğrudan doldururum" biçimindeydi ve bu sözde
hiçbir değişmez denetimi yoktu — çünkü o denetim yapıcının işiydi, dizileştirmenin değil.
Denetimin geri gelmesi için gereken şey kütüphaneden bir düzeltme değil, sınıfın **kendi**
`readObject`'ini yazmasıydı; bu satırı yazmak yine çağırana, yani sınıfı tasarlayan tarafa
kalıyor.

Kursun üçüncü iddiası burada da doğrulanıyor: bu boşluğun hiçbirini derleyici yakalamıyor.
Kancasız `Hesap` de, kancalı `Hesap` de aynı sorunsuzlukla derleniyor; derleyici bir sınıfın
`readObject`'i olup olmadığına, olan `readObject`'in yapıcıdaki denetimleri tekrarlayıp
tekrarlamadığına hiç bakmıyor. `Serializable` bir işaretleyici arayüzdür — hiçbir soyut
yöntem taşımaz — ve bu yüzden derleme zamanında zorlanabilecek hiçbir şey yoktur; bütün
denetim isteğe bağlı bir yöntemin isteğe bağlı gövdesine bırakılmıştır.

#### Alan Adı ve Sürüm Kimliği Bir Sözleşmedir

- **IO20** — Elde, sürüm kimliği `42` olan eski bir `Hesap` kaydı var; o zamanki sınıfta
  alanın adı `bakiye` idi. Şimdiki sınıfta aynı alan `miktar` olarak yeniden adlandırıldı,
  ama sürüm kimliği hâlâ `42`.
- **IO21** — Aynı eski kayıt, bu kez sürüm kimliği `43` olan bir sınıf tanımına karşı
  okunur — yani sürüm kimliği de değişmiş olsaydı ne olacağı ölçülüyor.

```java
// AlanAdiSozlesmesi.java — alan adi ve dizilestirme surum kimligi sozlesmeye donusuyor
import java.io.*;
import java.util.Base64;

class Hesap implements Serializable {
    private static final long serialVersionUID = 42L;
    private final String sahip;
    private final long miktar;
    Hesap(String sahip, long miktar) {
        if (miktar < 0) throw new IllegalArgumentException("negatif miktar");
        this.sahip = sahip;
        this.miktar = miktar;
    }
    long miktar() { return miktar; }
    String sahip() { return sahip; }
}

public class AlanAdiSozlesmesi {
    // Eski surumde ayni sinif "bakiye" adinda bir alan tasiyordu, "miktar" degil;
    // dizilestirme surum kimligi (42L) o zamandan bu yana degistirilmedi.
    static final String ESKI_KAYIT =
            "rO0ABXNyAAVIZXNhcAAAAAAAAAAqAgACSgAGYmFraXllTAAFc2FoaXB0ABJMamF2YS9sYW5nL1N0cmluZzt4cAAAAAAAAABkdAADYWRh";

    public static void main(String[] args) throws Exception {
        byte[] bayt = Base64.getDecoder().decode(ESKI_KAYIT);
        Hesap h;
        try (ObjectInputStream i = new ObjectInputStream(new ByteArrayInputStream(bayt))) {
            h = (Hesap) i.readObject();
        }
        System.out.println("gecti mi (istisna yok): true");
        System.out.println("sahip alani (adi degismedi): " + h.sahip());
        System.out.println("miktar alani (eskiden bakiye idi): " + h.miktar());
    }
}
```

```
gecti mi (istisna yok): true
sahip alani (adi degismedi): ada
miktar alani (eskiden bakiye idi): 0
```

Geri okuma hiç itiraz etmiyor. `sahip` alanı adı değişmediği için sorunsuz doluyor: `ada`.
Ama `miktar` alanı, akıştaki kaydın `bakiye` diye taşıdığı değeri **hiç görmüyor** — `long`
tipinin varsayılan değeriyle, sıfırla dolduruluyor. `ObjectInputStream` akıştaki her alanı
adına göre eşliyor; akışta `miktar` adında bir alan yoksa, sınıftaki `miktar` hiç
dokunulmamış sayılıyor. Sürüm kimliği aynı kaldığı için hiçbir uyarı da çıkmıyor: kütüphane
açısından bu geçerli bir geri okumadır.

```java
// SurumKimligiUyusmazligi.java — surum kimligi degistiginde ayni bayt dizisi reddedilir
import java.io.*;
import java.util.Base64;

class Hesap implements Serializable {
    private static final long serialVersionUID = 43L;
    private final String sahip;
    private final long miktar;
    Hesap(String sahip, long miktar) {
        if (miktar < 0) throw new IllegalArgumentException("negatif miktar");
        this.sahip = sahip;
        this.miktar = miktar;
    }
}

public class SurumKimligiUyusmazligi {
    static final String ESKI_KAYIT =
            "rO0ABXNyAAVIZXNhcAAAAAAAAAAqAgACSgAGYmFraXllTAAFc2FoaXB0ABJMamF2YS9sYW5nL1N0cmluZzt4cAAAAAAAAABkdAADYWRh";

    public static void main(String[] args) throws Exception {
        byte[] bayt = Base64.getDecoder().decode(ESKI_KAYIT);
        String sonuc;
        try (ObjectInputStream i = new ObjectInputStream(new ByteArrayInputStream(bayt))) {
            i.readObject();
            sonuc = "gecti";
        } catch (InvalidClassException e) {
            sonuc = "reddedildi: " + e.getClass().getSimpleName();
        }
        System.out.println("surum kimligi 43 iken ayni eski kayit: " + sonuc);
    }
}
```

```
surum kimligi 43 iken ayni eski kayit: reddedildi: InvalidClassException
```

Bu kez aynı eski kayıt daha derine bile inmiyor: sürüm kimliği eşleşmediği için geri okuma
alanlara hiç bakmadan reddediliyor. İki ölçüm yan yana konduğunda ters bir sonuç çıkıyor.
Sürüm kimliğini **değiştirmek**, uyumsuz bir kaydı gürültülü biçimde durduruyor. Sürüm
kimliğini **sabit tutmak** — sanki sınıf hiç değişmemiş gibi davranmak — yapı gerçekten
değiştiğinde sessizce yanlış bir değer üretiyor. Dizileştirme sürüm kimliği bu yüzden bir
sürüm numarası değil, "bu sınıf tanımıyla o akış birbirini tanıyor mu" sorusuna verilen
tek bitlik bir yanıttır; alan adlarının kendisi de bu yanıtın bir parçasıdır, çünkü
bağlama adla yapılır.

Sürüm kimliği hiç yazılmasaydı sonuç değişmezdi, çünkü derleyici o durumda kimliği sınıfın
o anki şekline bakarak kendisi hesaplar; iki farklı şekilden hesaplanan iki kimlik neredeyse
her zaman farklı çıkar ve sonuç yine `InvalidClassException` olurdu. Sürüm kimliğini elle ve
sabit yazmak bu otomatik korumayı bilinçli olarak kapatmaktır — geriye dönük uyumluluk
isteniyorsa gereklidir, ama isteniyorsa **her** alan değişikliğinin geri okumaya ne
yapacağını da düşünmeyi gerektirir. Aynı mantık eklenen bir alan için de geçerlidir: akışta
hiç bulunmayan yeni bir alan, tıpkı `miktar` gibi, sessizce kendi varsayılan değerini alır.

#### Özet

- Dizileştirme yapıcıyı hiç çalıştırmaz; `ObjectInputStream` alanları doğrudan doldurur ve
  `final` alanlar bile bu yoldan yazılabilir.
- Yapıcının kurduğu iki değişmezden hiçbiri geri okumada kendiliğinden korunmaz: elde
  bulunan bozuk bir kayıt hiçbir istisna vermeden geri okunur.
- Sınıfa özel bir `readObject` yöntemi eklenip yapıcıdaki denetimler tekrarlandığında,
  aynı bozuk kayıt artık reddedilir; denetim geri gelir çünkü çağıran onu yazmıştır.
- Alan adı geri okumada bağlama anahtarıdır: akıştaki bir alan sınıfta bulunmuyorsa yok
  sayılır, sınıftaki bir alan akışta bulunmuyorsa varsayılan değerle sessizce doldurulur.
- Dizileştirme sürüm kimliği sabit tutulduğunda yapı değişikliği sessizce geçer; sürüm
  kimliği değiştirildiğinde aynı uyumsuzluk gürültülü biçimde reddedilir.
- `Serializable` işaretleyici bir arayüzdür ve hiçbir soyut yöntem taşımaz; bu yüzden bir
  sınıfın değişmezlerini geri okumada da koruyup korumadığını derleyici hiçbir zaman
  denetlemez — denetim tamamen isteğe bağlı bir `readObject`'in gövdesine bağlıdır.

#### Sonraki Adım

Bu derste ölçülen her şey belleğe alınmış, sabit bir bayt dizisiydi — zaman hiç
karışmadı. Ama bir sistemin taşıdığı en yaygın veri türlerinden biri zamanın kendisidir.
Sıradaki ders tarih ve zaman API'sine bakar: bir anlık noktanın, bir yerel tarih-saatin ve
bir bölgesel tarih-saatin hangi bilgiyi taşıyıp hangisini taşımadığını ve bu farkın iki
değeri karşılaştırırken nasıl sessizce yanlış bir sonuca yol açabildiğini ölçer.

### Tarih ve Zaman API'si

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/tarih-ve-zaman-apisi

Üç tipin (anlık, yerel tarih-saat, bölgesel tarih-saat) 'aynı anı mı gösteriyor' sorusunu doğru yanıtlayıp yanıtlayamadığı koşturularak ölçülür. Saat dilimi verilmeden karşılaştırmanın sessizce yanlış sonuç verdiği ve yaz saati geçişindeki boşluk ile çakışmanın API'nin kural gereği seçtiği yer olduğu gösterilir.

Önceki ders bir nesnenin bayta çevrildiğinde yapıcının hiç çalışmadığını ölçtü — orada
zaman hiç karışmadı, çünkü ölçülen her şey belleğe alınmış sabit bir bayt dizisiydi. Bir
sistemin taşıdığı en yaygın veri türlerinden biri zamanın kendisidir ve zaman üç ayrı tiple
temsil edilir: `Instant` bir anlıktır, `LocalDateTime` bir yerel tarih-saattir,
`ZonedDateTime` bir bölgesel tarih-saattir. Bu üç tip aynı bilgiyi taşımaz — biri saat
dilimini hiç bilmez, biri her zaman bilir — ve bu ders o farkın nerede önemli olduğunu ölçer.

Kursun sorusu değişmedi: bir garanti — burada "bu iki değer aynı anı mı gösteriyor" sorusuna
doğru yanıt verme garantisi — hangi tarafın elinde? Yanıt bu kez bir sınıf seçimi değil, bir
**tip** seçimidir. Üç bölüm sırayla ilerliyor. İlki üç tipi aynı soruyla sınıyor. İkincisi bu
sorunun yanlış yanıtlandığı somut bir çağıran hatasını ölçüyor. Üçüncüsü ise saat dilimi
eklemenin bile her soruyu yanıtlamadığı bir sınır durumu — yaz saati geçişini — ölçüyor.

Bu üçü, bir önceki dersin ölçtüğü şeyin bir başka biçimidir. Dizileştirme dersinde bir bayt
dizisinin **hangi bilgiyi taşıdığı** ölçülmüştü; burada da aynı soru soruluyor, ama nesne
değil zaman temsil ediliyor. Bir `LocalDateTime`, tıpkı alan adı değiştirilmiş bir kayıt
gibi, sahip olmadığı bir bilgiyi taşıyormuş gibi kullanıldığında sessizce yanlış sonuç
üretiyor — fark, orada bir dizileştirme sürüm kimliğinin, burada bir saat diliminin eksik
olması.

**Belirlenimcilik notu:** bu derste gerçek saat hiç okunmaz. Bütün örnekler sabit bir
`Instant` üzerine ya da sabit bir metinden ayrıştırılmış bir tarihe kurulur; yaz saati
geçişleri de sistemin kendi saat dilimi verisinden **programatik olarak keşfedilir**, tarihi
düzyazıda iddia edilmez.

#### Üç Tip, Aynı Soru

- **IO22** — Tek bir `Instant` iki ayrı saat dilimine (İstanbul, New York) uygulanıp iki
  `ZonedDateTime` üretiliyor; bu iki değer **aynı anı** gösteriyor ama farklı yerel saatler
  taşıyor. Dört ayrı yöntem aynı soruyu ("bu ikisi aynı an mı") yanıtlamaya çalışıyor:
  `Instant.equals`, `LocalDateTime.equals`, `ZonedDateTime.equals`, `ZonedDateTime.isEqual`.

```java
// UcTip.java — uc tipten hangisi "ayni ani mi gosteriyor" sorusunu dogru yanitliyor
import java.time.*;

public class UcTip {
    public static void main(String[] args) {
        Instant an = Instant.parse("2026-06-15T13:00:00Z");

        ZonedDateTime istanbul = an.atZone(ZoneId.of("Europe/Istanbul"));
        ZonedDateTime newYork = an.atZone(ZoneId.of("America/New_York"));
        LocalDateTime istanbulYerel = istanbul.toLocalDateTime();
        LocalDateTime newYorkYerel = newYork.toLocalDateTime();

        System.out.println("istanbul yerel saat: " + istanbulYerel);
        System.out.println("newyork  yerel saat: " + newYorkYerel);
        System.out.println();

        System.out.printf("%-40s%s%n", "soru: ayni ani mi gosteriyor", "yanit");
        System.out.printf("%-40s%s%n", "Instant.equals",
                an.equals(Instant.parse("2026-06-15T13:00:00Z")));
        System.out.printf("%-40s%s%n", "LocalDateTime.equals (yerel saatler)",
                istanbulYerel.equals(newYorkYerel));
        System.out.printf("%-40s%s%n", "ZonedDateTime.equals (bolge+yerel+ofset)",
                istanbul.equals(newYork));
        System.out.printf("%-40s%s%n", "ZonedDateTime.isEqual (yalniz an)",
                istanbul.isEqual(newYork));
    }
}
```

```
istanbul yerel saat: 2026-06-15T16:00
newyork  yerel saat: 2026-06-15T09:00

soru: ayni ani mi gosteriyor            yanit
Instant.equals                          true
LocalDateTime.equals (yerel saatler)    false
ZonedDateTime.equals (bolge+yerel+ofset)false
ZonedDateTime.isEqual (yalniz an)       true
```

İki `ZonedDateTime` **aynı** `Instant`'tan üretildi, yani ikisi de birebir aynı anı gösteriyor
— biri saat 16.00, öteki saat 09.00 gösterse bile. Dört yöntemden ikisi bunu doğru söylüyor:
`Instant.equals` (zaten aynı anlığı karşılaştırıyor) ve `ZonedDateTime.isEqual` (yalnız
temsil ettiği anı karşılaştırmak için özel olarak yazılmış bir yöntem). `LocalDateTime.equals`
`false` diyor, ama bu soruyu yanlış yanıtlamıyor — **hiç yanıtlayamıyor**, çünkü elindeki
değerlerin hangi saat dilimine ait olduğuna dair hiçbir bilgi yok; yalnız iki takvim
okumasının birbirine eşit olup olmadığını söylüyor. En şaşırtıcı satır üçüncüsü:
`ZonedDateTime.equals`, elinde saat dilimi bilgisi olmasına rağmen `false` diyor, çünkü o
yöntem "aynı an mı" sorusunu değil, "bölge, yerel saat ve ofset birebir aynı mı" sorusunu
yanıtlıyor. Aynı tipte iki yöntem var ve ikisi **farklı** sorulara yanıt veriyor; doğru
yöntemi seçmek çağırana kalıyor.

Bu dördü, üç tipin taşıdığı bilginin doğrudan bir sonucudur. `Instant` bir sayı çizgisindeki
tek bir noktadır ve iki nokta aynı yerdeyse `equals` bunu doğrudan söyler; başka bir sorusu
yoktur. `ZonedDateTime` bu noktayı bir de takvim okumasıyla birlikte taşır — hangi bölgede,
hangi yerel saatte olduğunu — ve `equals` bu **ek** bilgiyi de karşılaştırmaya katar; iki
temsil aynı anı gösterse bile bölgeleri farklıysa `equals` onları ayrı sayar, çünkü o iki
değeri **birebir aynı temsil** olarak tanımlar, aynı an olarak değil. `isEqual` bilerek bu
ek bilgiyi göz ardı eder ve yalnız temsil edilen ana bakar. `LocalDateTime` ise bu ek
bilginin **kendisini hiç taşımaz**; elinde karşılaştıracağı bir bölge, bir ofset yoktur, bu
yüzden sorulan soruyu değil, sorabileceği tek soruyu — iki takvim okuması eşit mi — yanıtlar.

#### Çağıranın Kuralı: Saat Dilimi Vermeden Karşılaştırma

- **IO23** — İki kayıt aynı yerel saat değerini taşıyor (`09:00`) ama biri İstanbul'da,
  biri New York'ta girilmiş; hangi saat diliminde girildiği bilgisi kayıtla birlikte
  **tutulmuyor**. Bu bilgi olmadan yapılan bir karşılaştırma, bilgi verilerek yapılan bir
  karşılaştırmayla yan yana koşturuluyor.

Bu durum uydurma değildir: bir randevu sistemi, bir günlük kaydı, bir zamanlanmış görev —
zamanı kullanıcının kendi yerel saatiyle alan her sistem aynı seçimle karşılaşır. Saat
dilimini kayıtla birlikte saklamak bir tasarım kararıdır, dilin kendisi bunu dayatmaz;
`LocalDateTime` bilinçli olarak saat dilimsiz bir tip olarak var eder, çünkü çoğu zaman
gerçekten de yalnız bir takvim okuması yeterlidir. Bedel yalnız o okumayı "aynı an" anlamında
kullanmaya kalkışıldığında ortaya çıkıyor.

```java
// SaatDilimiEksik.java — saat dilimi verilmeden karsilastirma sessizce yanlis sonuc verir
import java.time.*;

public class SaatDilimiEksik {
    static boolean naifAyniZaman(LocalDateTime a, LocalDateTime b) {
        return a.equals(b);
    }

    static boolean dogruAyniZaman(LocalDateTime a, ZoneId aBolge, LocalDateTime b, ZoneId bBolge) {
        return a.atZone(aBolge).toInstant().equals(b.atZone(bBolge).toInstant());
    }

    public static void main(String[] args) {
        LocalDateTime aKaydi = LocalDateTime.of(2026, 6, 15, 9, 0);
        LocalDateTime bKaydi = LocalDateTime.of(2026, 6, 15, 9, 0);
        ZoneId aBolge = ZoneId.of("Europe/Istanbul");
        ZoneId bBolge = ZoneId.of("America/New_York");

        System.out.println("a kaydi (yerel): " + aKaydi + " (Istanbul)");
        System.out.println("b kaydi (yerel): " + bKaydi + " (New York)");
        System.out.println("naif karsilastirma (saat dilimi vermeden): "
                + naifAyniZaman(aKaydi, bKaydi));
        System.out.println("dogru karsilastirma (saat dilimi verilerek): "
                + dogruAyniZaman(aKaydi, aBolge, bKaydi, bBolge));

        Duration fark = Duration.between(aKaydi.atZone(aBolge).toInstant(),
                bKaydi.atZone(bBolge).toInstant());
        System.out.println("gercek fark: " + fark.toHours() + " saat");
    }
}
```

```
a kaydi (yerel): 2026-06-15T09:00 (Istanbul)
b kaydi (yerel): 2026-06-15T09:00 (New York)
naif karsilastirma (saat dilimi vermeden): true
dogru karsilastirma (saat dilimi verilerek): false
gercek fark: 7 saat
```

Naif karşılaştırma "aynı zaman" diyor, çünkü iki `LocalDateTime` değeri gerçekten eşit — ikisi
de `09:00` okuyor. Ama gerçekte aralarında **yedi saatlik** bir fark var. Bu kütüphanenin bir
kusuru değil: `LocalDateTime.equals` zaten yalnız iki takvim okumasını karşılaştırdığını
söylüyor, başka bir söz vermiyor. Kusur, saat dilimi bilgisini **hiç taşımayan** bir tipi,
sanki taşıyormuş gibi kullanmakta. Doğru karşılaştırma her iki tarafa da saat dilimi
ekleyip `Instant`'a çevirdikten sonra karşılaştırıyor ve doğru yanıtı — `false` ve altında
yatan yedi saatlik farkı — veriyor. Bu, bir önceki bölümün doğal sonucu: `LocalDateTime`
"aynı an mı" sorusunu yanıtlayamayan tipti; burada aynı eksiklik bir kayıt karşılaştırma
hatası olarak somutlaşıyor.

Bu kural derleyici tarafından hiç zorlanmıyor. `naifAyniZaman` iki `LocalDateTime` alıp bir
`boolean` döndüren, tamamen geçerli, tipi uyan bir yöntemdir; derleyici onun **anlamsal**
olarak yanlış bir soruya yanlış bir yanıt verdiğini bilemez, çünkü elindeki tek bilgi iki
parametrenin de aynı sınıftan olduğudur. Sonuç, önceki derste ölçülen kusurla aynı
biçimdedir: hata çalışma zamanında bile bir istisna olarak görünmez, yalnız yanlış bir
`boolean` olarak sessizce geçer. `Duration.between` çağrısı bu farkı görünür kılmak için
her iki tarafın da açıkça bir `Instant`'a çevrilmesini gerektiriyor — yani doğru
karşılaştırmayı yazmanın bedeli, saat dilimini **unutmamaktır**, unutulduğunda derleyicinin
hiçbir uyarı vermeyeceğini bilerek unutmamaktır.

#### Yaz Saati Geçişinde Boşluk ve Çakışma

- **IO24** — Sistemin New York saat dilimi kuralları, sabit bir başlangıç anından sonraki
  ilk iki geçiş için sorgulanıyor. İlk geçiş bir **boşluktur** (bir saatlik bir yerel saat
  aralığı hiç yaşanmaz), ikincisi bir **çakışmadır** (bir saatlik bir yerel saat aralığı
  iki kez yaşanır). Hangi tarihte olduğu düzyazıda iddia edilmiyor; program kendi bulduğu
  tarihi kullanıyor.

Önceki iki bölüm saat dilimini **eksik** bir durumu ele aldı; bu bölüm saat dilimi tamsa bile
sınırın nerede olduğunu ölçüyor. `ZoneId.getRules()` bir bölgenin bütün geçiş bilgisini
taşıyan bir `ZoneRules` nesnesi döndürüyor; bu bilgi kodun içinde yazılı bir sabit değil,
sanal makinenin kendi saat dilimi verisinden okunuyor. `nextTransition` bir andan sonraki
ilk geçişi buluyor — sonucu hangi anın verildiğine bağlı, ama bu bağımlılık ölçümü
belirsizleştirmiyor, çünkü başlangıç anı burada da sabit bir metinden ayrıştırılıyor.

```java
// KuralKesif.java — yaz saati gecisleri sistemin kendi saat dilimi verisinden kesfedilir
import java.time.*;
import java.time.zone.*;

public class KuralKesif {
    public static void main(String[] args) {
        ZoneId bolge = ZoneId.of("America/New_York");
        ZoneRules kurallar = bolge.getRules();
        Instant baslangic = Instant.parse("2020-01-01T00:00:00Z");

        ZoneOffsetTransition ilkGecis = kurallar.nextTransition(baslangic);
        ZoneOffsetTransition ikinciGecis = kurallar.nextTransition(ilkGecis.getInstant());

        System.out.println("ilk gecis bosluk mu: " + ilkGecis.isGap());
        System.out.println("ilk gecis oncesi/sonrasi yerel saat: "
                + ilkGecis.getDateTimeBefore() + " / " + ilkGecis.getDateTimeAfter());
        System.out.println("ikinci gecis cakisma mi: " + ikinciGecis.isOverlap());
        System.out.println("ikinci gecis oncesi/sonrasi yerel saat: "
                + ikinciGecis.getDateTimeBefore() + " / " + ikinciGecis.getDateTimeAfter());

        LocalDateTime boslukIcinde = ilkGecis.getDateTimeBefore().plusMinutes(30);
        ZonedDateTime boslukSonuc = boslukIcinde.atZone(bolge);
        System.out.println();
        System.out.println("bosluktaki yerel saat: " + boslukIcinde);
        System.out.println("varsayilan cozum: " + boslukSonuc);
        System.out.println("yerel saat ileri kaydirildi mi: "
                + !boslukSonuc.toLocalDateTime().equals(boslukIcinde));

        LocalDateTime cakismaIcinde = ikinciGecis.getDateTimeAfter();
        ZonedDateTime erkenOfset = cakismaIcinde.atZone(bolge);
        ZonedDateTime gecOfset = erkenOfset.withLaterOffsetAtOverlap();
        System.out.println();
        System.out.println("cakismadaki yerel saat: " + cakismaIcinde);
        System.out.println("varsayilan cozum (erken ofset): " + erkenOfset);
        System.out.println("gec ofset istendiginde: " + gecOfset);
        System.out.println("yerel saat ayni, aninlar ayni mi: "
                + erkenOfset.toLocalDateTime().equals(gecOfset.toLocalDateTime()) + ", "
                + erkenOfset.toInstant().equals(gecOfset.toInstant()));
    }
}
```

```
ilk gecis bosluk mu: true
ilk gecis oncesi/sonrasi yerel saat: 2020-03-08T02:00 / 2020-03-08T03:00
ikinci gecis cakisma mi: true
ikinci gecis oncesi/sonrasi yerel saat: 2020-11-01T02:00 / 2020-11-01T01:00

bosluktaki yerel saat: 2020-03-08T02:30
varsayilan cozum: 2020-03-08T03:30-04:00[America/New_York]
yerel saat ileri kaydirildi mi: true

cakismadaki yerel saat: 2020-11-01T01:00
varsayilan cozum (erken ofset): 2020-11-01T01:00-04:00[America/New_York]
gec ofset istendiginde: 2020-11-01T01:00-05:00[America/New_York]
yerel saat ayni, aninlar ayni mi: true, false
```

Program kendi başlangıç anından itibaren sistemin saat dilimi kurallarını tarayıp iki geçiş
buluyor: biri **boşluk**, biri **çakışma**. Boşlukta saat 02.00'den 03.00'e doğrudan atlıyor
— `02:30` diye bir yerel saat o bölgede o gün hiç yaşanmıyor. Bu yüzden `02:30`'u o bölgeye
bağladığımızda API onu **olduğu gibi** kabul etmiyor; boşluğun uzunluğu kadar ileri kaydırıp
`03:30`'a taşıyor. Çakışmada ise tam tersi olur: `01:00` o gün **iki kez** yaşanıyor, biri
yaz saatiyle (ofset −4), biri kışla (ofset −5). API bir seçim yapmak zorunda ve varsayılan
olarak **erken** ofseti seçiyor; `withLaterOffsetAtOverlap` çağrıldığında aynı yerel saat
bu kez bir saat **sonraki** gerçek ana bağlanıyor — yerel saat değişmiyor, ama gösterdiği
an değişiyor.

İki durumun neden farklı sonuçlandığı geçişin kendi tanımından çıkıyor. Boşlukta saatler
gerçekten **atlıyor** — 02.00'den sonra bir sonraki gerçek an 03.00 olarak okunuyor — bu
yüzden aradaki yerel saatlerin karşılığı olan tek gerçek an, atlamanın **bittiği** yerdir; API
bu yüzden ileri kaydırıyor, geri değil. Çakışmada ise saatler **geri sarılıyor**: aynı yerel
okuma iki farklı gerçek anda beliriyor ve aralarında bir tercih yapılması gerekiyor çünkü
her ikisi de eşit derecede geçerli. `getDateTimeBefore` ve `getDateTimeAfter`, bu ölçümde
kullanılan iki yöntem, geçişin **hangi yönde** olduğunu söylüyor: boşlukta ikinci değer
birinciden büyük, çakışmada küçük — geçişin bosluk mu çakışma mı olduğu buradan da
çıkarılabilir; `isGap`/`isOverlap` yalnız bunu adlandırıyor.

Bu bölümün sınırlayıcı ölçümü şudur: saat dilimi eklemek önceki bölümdeki hatayı çözüyor, ama
**her** soruyu yanıtlamıyor. Çakışma durumunda bölgeyle birlikte gelen yerel saat bile tek
başına yetmiyor — aynı yerel saat, aynı bölgede, iki ayrı gerçek ana karşılık gelebiliyor
ve hangisinin kastedildiğini API'nin kendisi seçmek zorunda kalıyor. Bu seçim keyfi değil:
sistemin saat dilimi kuralları hangi yerel saatlerin var olmadığını ve hangilerinin iki kez
var olduğunu tanımlıyor ve API da bu kurala göre **bir tarafı** seçiyor. Seçimin kendisi
sabit ve belgelenmiş; ama çağıranın istediği taraf bu olmayabilir ve bunu fark etmenin tek
yolu `isGap`/`isOverlap` gibi sorguları kullanmaktır.

#### Özet

- `Instant` bir anlıktır ve saat dilimi taşımaz; `LocalDateTime` bir yerel tarih-saattir ve
  saat dilimi taşımaz; `ZonedDateTime` bir bölgesel tarih-saattir ve saat dilimi taşır.
- "Aynı anı mı gösteriyor" sorusunu `Instant.equals` ve `ZonedDateTime.isEqual` doğru
  yanıtlar; `LocalDateTime.equals` bu soruyu hiç yanıtlayamaz, `ZonedDateTime.equals` ise
  farklı bir soruyu (bölge, yerel saat, ofset birebir aynı mı) yanıtlar.
- Saat dilimi verilmeden karşılaştırılan iki yerel tarih-saat, aralarında gerçek bir fark
  varken bile eşit çıkabilir; kusur kütüphanede değil, tipin taşımadığı bilgiyi varsaymakta.
- Yaz saati geçişindeki boşlukta bir yerel saat aralığı hiç yaşanmaz ve API onu geçişin
  uzunluğu kadar ileri kaydırır; çakışmada bir yerel saat aralığı iki kez yaşanır ve API
  varsayılan olarak erken ofseti seçer.
- Sınırlayıcı ölçüm: saat dilimi eklemek her soruyu yanıtlamaz. Çakışan bir yerel saat, aynı
  bölgede bile iki ayrı gerçek ana karşılık gelebilir ve hangisinin kastedildiği yalnız
  `isGap`/`isOverlap` gibi sorgularla anlaşılabilir.
- Boşluk ile çakışma geçişin yönünden gelir: saatler ileri atladığında ortada kalan yerel
  saatler hiç yaşanmaz, saatler geri sarıldığında bir yerel saat aralığı iki kez yaşanır.

#### Sonraki Adım

Bu derste ölçülen her şey sayısal ve takvimseldi. Ama standart kütüphanenin girdi-çıktı
katmanında bir de **metin** eşleştirme sorusu var: bir dizginin belli bir örüntüye uyup
uymadığını sormak. Sıradaki ve kursun son dersi düzenli ifadelere bakar — örüntünün derlenmiş
bir değer olduğunu, açgözlü ve isteksiz niceleyicinin aynı girdide ayrı yakalama ürettiğini
ve geri izlemenin adım sayısıyla ölçüldüğünü gösterir.

### Düzenli İfadeler

Source: https://academia.sh/tr/kurslar/java-standart-kutuphane/duzenli-ifadeler

Örüntünün derlenmiş bir değer olduğu, tam eşleşme ile aramanın ayrı sözleşmeler taşıdığı ve açgözlü ile isteksiz niceleyicinin aynı girdide ayrı yakalama ürettiği ölçülür. Örüntüyü her çağrıda yeniden derlemenin etkisiz olduğu ve geri izlemeli eşleştirmede adım sayısının girdiyle doğrusal artmadığı gösterilir.

Önceki ders üç zaman tipinin hangi bilgiyi taşıyıp hangisini taşımadığını ölçtü. Bu ders,
kursun ve kursun son konusunun sorusunu bu kez **metin** üzerinde sorar: bir dizginin bir
örüntüye uyup uymadığını sormak. Java'nın düzenli ifade API'si iki parçadan kurulur —
`Pattern`, örüntünün kendisi; `Matcher`, bir örüntüyü belirli bir girdiye uygulayan taraf —
ve bu ayrımın kendisi kursun sorusuna zaten bir ipucu verir: örüntü bir kere kurulup tekrar
tekrar kullanılabilecek bir **değerdir**, tek seferlik bir çağrı değil.

Dört bölüm sırayla ilerliyor: örüntünün bir değer oluşu ve iki eşleştirme yönteminin ayrı
sözleşmesi, iki niceleyici biçiminin aynı girdide ayrı sonucu, kursun tek **etkisiz** satırı
(kural kırılıyor, sonuç değişmiyor, yalnız bedel değişiyor) ve son sınırlayıcı ölçüm —
geri izlemenin adım sayısı girdiyle **doğrusal artmıyor**.

Düzenli ifadelerin sözdizimi Linux müfredatındaki Kabuk Programlama kursunda ve Python
Temelleri kursunda zaten kurulmuştu; o sözdizim burada tekrar anlatılmıyor. Bu dersin payı
Java'nın kendi düzeneğidir: örüntünün **derlenmiş bir nesne** olarak var olması, eşleştirmenin
bu nesne üzerinden ayrı yöntemlerle çağrılması ve bu yöntemlerin çağıranı hangi kararlara
zorladığı.

#### Örüntü Derlenmiş Bir Değerdir

- **IO25** — Tek bir `Pattern` nesnesi dört ayrı girdi üzerinde hem `matches` (tam eşleşme)
  hem `find` (arama) ile sınanıyor. Aynı örüntü, aynı girdi, iki ayrı yöntem.

```java
// TamEslesmeVeArama.java — oruntu derlenmis bir degerdir, matches ve find ayri sozlesme tasir
import java.util.regex.*;

public class TamEslesmeVeArama {
    public static void main(String[] args) {
        Pattern oruntu = Pattern.compile("[0-9]+");
        String[] girdiler = { "12345", "kod-12345", "12345-son", "abc" };

        System.out.printf("%-16s%-10s%s%n", "girdi", "matches", "find");
        for (String g : girdiler) {
            Matcher m1 = oruntu.matcher(g);
            boolean tamEslesme = m1.matches();
            Matcher m2 = oruntu.matcher(g);
            boolean arama = m2.find();
            System.out.printf("%-16s%-10s%s%n", g, tamEslesme, arama);
        }
    }
}
```

```
girdi           matches   find
12345           true      true
kod-12345       false     true
12345-son       false     true
abc             false     false
```

`Pattern.compile` çağrısı bir kere yapılıyor ve dönen `oruntu` dört ayrı `Matcher`'a temel
oluyor — örüntünün kendisi bir çağrı değil, bir **değer**. Bu değer üzerinde çağrılan iki
yöntem farklı sorular soruyor. `matches` girdinin **tamamının** örüntüye uyup uymadığını
soruyor; `kod-12345` girdisinde rakamlar var ama başında `kod-` fazlalığı olduğu için tüm
girdi uymuyor, `matches` `false` dönüyor. `find` ise girdinin **bir yerinde** örüntüye uyan
bir alt dizgi arıyor; aynı girdide `12345` alt dizgisi bulunduğu için `find` `true` dönüyor.
Yalnız son satırda ikisi de aynı yanıtı veriyor, çünkü `abc` girdisinde hiçbir rakam yok —
ne tam eşleşme ne arama bir şey bulabiliyor. `Pattern` aynı kalırken sözleşmeyi değiştiren
şey çağrılan yöntemdir, örüntünün kendisi değil.

#### Açgözlü ve İsteksiz Niceleyici Ayrı Yakalama Üretir

- **IO26** — Aynı yakalama grubu iki niceleyici biçimiyle yazılıyor: `(.*)` açgözlü,
  `(.*?)` isteksiz. İkisi de aynı girdiye, aynı köşeli parantez çiftlerine uygulanıyor.

```java
// AcgozluVeIsteksiz.java — acgozlu ve isteksiz niceleyici ayni girdide ayri yakalama uretir
import java.util.regex.*;

public class AcgozluVeIsteksiz {
    public static void main(String[] args) {
        String girdi = "[kayit-1][kayit-2]";

        Pattern acgozlu = Pattern.compile("\\[(.*)\\]");
        Pattern isteksiz = Pattern.compile("\\[(.*?)\\]");

        Matcher m1 = acgozlu.matcher(girdi);
        m1.find();
        Matcher m2 = isteksiz.matcher(girdi);
        m2.find();

        System.out.println("girdi: " + girdi);
        System.out.println("acgozlu  [(.*)]   yakalanan: " + m1.group(1));
        System.out.println("isteksiz [(.*?)]  yakalanan: " + m2.group(1));

        int acgozluAdet = 0;
        Matcher m3 = acgozlu.matcher(girdi);
        while (m3.find()) acgozluAdet++;
        int isteksizAdet = 0;
        Matcher m4 = isteksiz.matcher(girdi);
        while (m4.find()) isteksizAdet++;
        System.out.println("acgozlu toplam eslesme sayisi: " + acgozluAdet);
        System.out.println("isteksiz toplam eslesme sayisi: " + isteksizAdet);
    }
}
```

```
girdi: [kayit-1][kayit-2]
acgozlu  [(.*)]   yakalanan: kayit-1][kayit-2
isteksiz [(.*?)]  yakalanan: kayit-1
acgozlu toplam eslesme sayisi: 1
isteksiz toplam eslesme sayisi: 2
```

İki niceleyici de "sıfır ya da daha çok karakter" anlamına gelir; ikisi de **aynı** kümeyi
kabul eder. Aralarındaki fark kabul ettikleri küme değil, o kümeyi **hangi sırayla** dener.
Açgözlü niceleyici önce mümkün olan en uzun diziyi dener ve eşleşme başarısız olursa geri
çekilir; girdinin sonuna kadar giden `.*` ile başlıyor, sonra kapanış parantezi bulunana
kadar geriye doğru bırakıyor. Girdide iki kapanış parantezi olduğu için en son bulduğu
parantez kazanıyor — yakalanan dizgi iki kaydın arasındaki her şeyi, `kayit-1][kayit-2`'yi
kapsıyor. İsteksiz niceleyici tam tersini yapıyor: önce en kısa diziyi, boş dizgiyi dener ve
yalnız gerektiğinde uzatır; ilk kapanış parantezinde durur. Bu farkın somut sonucu eşleşme
sayısında görünüyor: açgözlü örüntü girdinin tamamını **bir** eşleşmede tüketirken, isteksiz
örüntü aynı girdide **iki** ayrı eşleşme buluyor.

#### Çağıranın Kuralı: Yeniden Derleme Etkisizdir

- **IO27** — Beş girdi iki yoldan işleniyor: her seferinde yeniden derlenen bir örüntüyle ve
  bir kere derlenip beş kez kullanılan bir örüntüyle. Derleme çağrılarının sayısı ayrıca
  sayılıyor.

```java
// YenidenDerleme.java — oruntuyu her cagrida yeniden derlemek etkisizdir: sonuc ayni, bedel ayri
import java.util.List;
import java.util.regex.*;

public class YenidenDerleme {
    static int derlemeSayaci = 0;

    static Pattern derle(String duzenli) {
        derlemeSayaci++;
        return Pattern.compile(duzenli);
    }

    static boolean herCagridaDerleyerek(String duzenli, String girdi) {
        return derle(duzenli).matcher(girdi).find();
    }

    public static void main(String[] args) {
        List<String> girdiler = List.of("alfa-01", "beta-02", "gamma-03", "delta-04", "epsilon-05");
        String duzenli = "^[a-z]+-[0-9]{2}$";

        derlemeSayaci = 0;
        boolean[] sonuc1 = new boolean[girdiler.size()];
        for (int i = 0; i < girdiler.size(); i++)
            sonuc1[i] = herCagridaDerleyerek(duzenli, girdiler.get(i));
        int herCagridaDerlemeSayisi = derlemeSayaci;

        derlemeSayaci = 0;
        Pattern tekOruntu = derle(duzenli);
        boolean[] sonuc2 = new boolean[girdiler.size()];
        for (int i = 0; i < girdiler.size(); i++)
            sonuc2[i] = tekOruntu.matcher(girdiler.get(i)).find();
        int birKereDerlemeSayisi = derlemeSayaci;

        System.out.println("sonuclar ayni mi: " + java.util.Arrays.equals(sonuc1, sonuc2));
        System.out.println("her cagrida derleyerek - derleme sayisi: " + herCagridaDerlemeSayisi);
        System.out.println("bir kere derleyerek    - derleme sayisi: " + birKereDerlemeSayisi);
    }
}
```

```
sonuclar ayni mi: true
her cagrida derleyerek - derleme sayisi: 5
bir kere derleyerek    - derleme sayisi: 1
```

Bu ölçüm kursun kurduğu üçüncü sınıfı örnekliyor: **etkisiz** kural kırılması. Beş girdi de
iki yoldan birebir aynı sonuca ulaşıyor — hangi girdinin örüntüye uyduğu, hangisinin
uymadığı iki yolda da özdeş. Yeniden derleme çağıranın kuralını kırıyor ("aynı örüntü tekrar
tekrar kullanılacaksa bir kere derlenmeli") ama bu kırılma ne bir istisna fırlatıyor ne
sessizce yanlış bir sonuç üretiyor; **doğruluk hiç bozulmuyor**. Bozulan şey yalnız bedel:
beş girdilik bir döngüde derleme çağrısı bire karşı beş oluyor. `Pattern.compile` küçük bir
örüntü için ucuz görünebilir, ama derleme işi örüntü metnini bir durum makinesine çeviren
gerçek bir çalışmadır ve her çağrıda yeniden yapılır. Kuralın kırılması burada görünmez
kalır, çünkü hiçbir çıktı satırı yanlış değildir — yalnız harcanan iş fazladır.

Kursun ortak tanımı üç sınıf tanımlıyordu: `istisna` (hemen ve adıyla düşer), `sessiz`
(sonuç yanlıştır, kimse şikâyet etmez), `etkisiz` (kural doğruluğa değil bedele ilişkindir).
On beş gözlemin on dördü ilk ikisine düşüyordu; bu ölçüm on beşincisi ve kursun tek
`etkisiz` satırı — en **kolay gözden kaçan** kırılma, çünkü görünür hiçbir izi yoktur;
yalnız aynı işi yapan iki kod arasındaki iş miktarı farkıdır.

#### Sınırlayıcı Ölçüm: Geri İzleme Adım Sayısı Doğrusal Artmaz

- **IO28** — Girdi, kendi `charAt` çağrılarını sayan bir dizi ile sarmalanıyor. Aynı örüntü,
  gitgide uzayan ama hiçbir zaman eşleşmeyen bir girdiye uygulanıyor; her denemede toplam
  okuma sayısı kaydediliyor. Ölçü **adım sayısıdır, süre değildir**.

```java
// SayanDizi.java — girdiyi sarmalayip charAt cagrisini sayan CharSequence (yardimci sinif, main yok)
class SayanDizi implements CharSequence {
    private final String temel;
    long okumaSayisi = 0;

    SayanDizi(String temel) { this.temel = temel; }

    @Override public int length() { return temel.length(); }

    @Override public char charAt(int i) {
        okumaSayisi++;
        return temel.charAt(i);
    }

    @Override public CharSequence subSequence(int b, int s) { return temel.subSequence(b, s); }

    @Override public String toString() { return temel; }
}
```

```java
// GeriIzlemeAdimlari.java — geri izlemeli eslestirmede adim sayisi girdiyle dogrusal artmaz
import java.util.Locale;
import java.util.regex.Pattern;

public class GeriIzlemeAdimlari {
    static long adimSay(Pattern p, String girdi) {
        SayanDizi d = new SayanDizi(girdi);
        p.matcher(d).matches();
        return d.okumaSayisi;
    }

    public static void main(String[] args) {
        Pattern oruntu = Pattern.compile("(a+)+b");

        System.out.printf(Locale.ROOT, "%-10s%s%n", "uzunluk", "charAt cagri sayisi");
        long onceki = 0;
        for (int uzunluk = 10; uzunluk <= 25; uzunluk += 5) {
            String girdi = "a".repeat(uzunluk);
            long adim = adimSay(oruntu, girdi);
            System.out.printf(Locale.ROOT, "%-10d%d%n", uzunluk, adim);
            if (onceki > 0) {
                double oran = (double) adim / onceki;
                System.out.printf(Locale.ROOT, "  (bir onceki uzunluga gore artis orani: %.1f)%n", oran);
            }
            onceki = adim;
        }
    }
}
```

```
uzunluk   charAt cagri sayisi
10        100
15        225
  (bir onceki uzunluga gore artis orani: 2.3)
20        400
  (bir onceki uzunluga gore artis orani: 1.8)
25        625
  (bir onceki uzunluga gore artis orani: 1.6)
```

Girdi `a` harflerinden oluşuyor ve `b` hiç içermiyor; örüntü `(a+)+b` ise sonunda mutlaka bir
`b` istiyor. Eşleşme baştan başarısız olacak, ama motor bunu anlayana kadar iç grup `(a+)`'yı
girdiyi **kaç harfte bir bölerek** deneyeceğini araştırıyor — bir harf mi, iki mi, üç mü —
ve her bölünüşte `b` bulunamadığı için bir öncekine geri dönüp başka bir bölünüşü deniyor.
Girdi uzunluğu 10'dan 25'e, yani iki buçuk katına çıktığında, okuma sayısı 100'den 625'e,
yani iki buçuğun karesi olan 6,25 katına çıkıyor: 10 uzunlukta 100, 15'te 225, 20'de 400,
25'te 625 — her satır bir
öncekinin **karesiyle orantılı** büyüyor. Girdi doğrusal (sabit adım sabit artış) uzarken
adım sayısı karesel büyüyor; bu, "girdiyle doğrusal artmaz" iddiasının ölçülmüş hâli. Ölçü
gerçek bir kronometre değil, `charAt` çağrısı sayan bir sarmalayıcı — bu yüzden sonuç bu
belgeyi kim, hangi makinede çalıştırırsa çalıştırsın **birebir aynı** çıkıyor.

`(a+)+b` örüntüsünün kendisi geçerli, sıradan bir düzenli ifade; onu **tehlikeli** yapan bir
kaçış ya da özel bir kurgu değil, iç içe geçmiş bir niceleyicinin aynı karakterleri birden
çok yoldan gruplayabilmesidir — bu ders bu geometriyi kurmuyor, yalnız sayıyor.

#### Özet

- `Pattern` derlenmiş bir değerdir; `matches` (tam eşleşme) ve `find` (arama) aynı örüntü
  üzerinde ayrı sözleşmeler taşır ve aynı girdide ayrı sonuç verebilir.
- Açgözlü niceleyici (`.*`) önce en uzun diziyi dener ve geriye doğru daralır; isteksiz
  niceleyici (`.*?`) önce en kısa diziyi dener ve gerektikçe uzar; aynı girdide ayrı
  yakalama ve ayrı eşleşme sayısı üretirler.
- Örüntüyü her çağrıda yeniden derlemek kursun tek `etkisiz` kural kırılmasıdır: eşleşme
  sonucu değişmez, yalnız derleme çağrısı sayısı artar.
- Geri izlemeli bir eşleştirmede adım sayısı girdiyle doğrusal artmayabilir; ölçülen örnekte
  girdi uzunluğu iki buçuk katına çıkarken adım sayısı 6,25 katına — tam olarak iki
  buçuğun karesine — çıktı.
- Adım sayısı gerçek bir kronometreyle değil, girdiyi sarmalayıp okuma çağrısını sayan bir
  `CharSequence` ile ölçüldü; sonuç bu yüzden makineden bağımsız ve yeniden üretilebilir.

#### Kurs Kapanışı

Kurs tek bir soruyla açıldı: yazılmış bir kütüphaneyi çağırdığın anda karar dilin dışına
çıkar — peki o kütüphanenin sana verdiği sözü kim veriyor? On beş ders bu soruyu on beş ayrı
davranışa sordu ve yanıtı her seferinde en az iki gerçekleştirimi ya da en az iki çağrı
biçimini yan yana koşturarak aldı. Ana iddia daha ilk derste sayıyla doğrulandı:
koleksiyon çerçevesinde ölçülen on beş gözlemin yedisi arayüz garantisiydi, sekizi yalnız
seçilen gerçekleştirimin davranışıydı — tek bir sınıfı gözleyerek
çıkarılan bir davranış listesinin çoğunluğu arayüzün sözü değildi. İkinci iddia da doğrulandı:
çağıranın kuralı kırıldığında en sık düşen şey bir istisna değil, sessizce yanlış bir
sonuçtu. Üçüncü iddia sınırıyla birlikte doğrulandı: derleyicinin koruması burada tümüyle
bitmiyor, koruduğu şey değişiyor. Tek soyut yöntem kısıtı ile etkin `final` kuralı hâlâ
derleme zamanında düşüyor; ikisi de bir **tipin şekline** ilişkin sözlerdir. Ama kursun
ölçtüğü çağıran yükümlülüklerinin hiçbiri derleme zamanında görünmedi — hepsi derlendi ve
ayrım çalışma zamanına kaldı. Derleyici tipin şeklini doğruluyor, sözün tutulduğunu değil.

| Ders | Ölçülen davranış | Garantiyi veren | Sınırlayıcı ölçüm |
|---|---|---|---|
| Koleksiyon Çerçevesi | 15 gözlemin 7'si arayüz garantisi, 8'i gerçekleştirim davranışı | arayüz (7) + gerçekleştirim (8) | 4 ögeli kaynakta `PriorityQueue`'nun gezinme sırası çıkış sırasıyla rastlantı eseri örtüşür; 5. öge ayrımı açar |
| Liste, Küme ve Kuyruk | 6 ögelik girdiden liste hiçbir bilgi, küme 3 bilgi, kuyruk yalnız konum erişimini düşürür | arayüz (düşen bilgi sözün bedeli) + çağıran (`remove(1)`) | `LinkedList` iki arayüzü birden konuşur; aynı `remove(1)` iki anlam taşır, kırılma sessizdir |
| Sözlükler | Üç söz (anahtar tekilliği, canlı görünüm, yok anahtarda boş değer) üç gerçekleştirimde aynı; sıra ve boş anahtar ayrışır | arayüz (3 söz) + gerçekleştirim (sıra) + çağıran (`equals`/`hashCode`) | Aynı kusurlu anahtar sıralı sözlükte bulunur; orada kullanılan söz `compareTo`'dur |
| Yineleyiciler ve Eşzamanlı Değişiklik | Tükenmede `NoSuchElementException` üç gerçekleştirimde aynı; gezinirken değişiklikte üç ayrı yanıt | arayüz (tükenme) + gerçekleştirim (değişiklik yanıtı) | `Iterator.remove` kuralı kırmaz; kural "yineleyicinin dışından değiştirme"dir |
| Karşılaştırma ve Sıralama | Doğal sıra tipin kendi sözü, karşılaştırıcı çağıranın verdiği ayrı nesne; kararlılık iki gerçekleştirimde aynı | arayüz (kararlılık) + çağıran (karşılaştırıcı tutarlılığı) | Uyumsuz karşılaştırıcı listede hiç öge düşürmez; kusuru üreten onu tekillik kararı olarak kullanan kaptır |
| Fonksiyonel Arayüzler | Tek soyut yöntem derleme zamanı sözüdür; davranış sözü hiçbir yerde denetlenmez | arayüz (tipin şekli); davranış sözü kimsenin | İşaret garantiyi eklemez, ihlali erken yakalar; işaretsiz arayüz de fonksiyoneldir |
| Lambda İfadeleri | Yerel değişken değeriyle yakalanır, etkin `final` derleme zamanında zorunlu; alanda kısıt yok | çağıran (hangi biçimi seçtiği) | Kısıt değişkeni dondurur, gösterdiği nesneyi değil; yakalanan liste değişirse lambda yeni içeriği görür |
| Akış API'si | On ölçümün 3'ü kaynaktan bağımsız garanti, 5'i kaynağın sırasına bağlı, 2'si çağıranın kuralı | arayüz (3) + kaynak (5) + çağıran (2) | "Veri akışı sırayı korur" cümlesi kaynağın sözüdür, veri akışının değil |
| Toplayıcılar | Dört parça (üretici, birikeç, birleştirici, bitirici); sonuç kabının tipi arayüzün sözü değildir | gerçekleştirim (kap tipi) + çağıran (yinelenen anahtar) | Çağıran kap üreticisini verdiğinde belirsizlik kapanır; bırakılmış söz geri alınabilir |
| `Optional` | Denetimsiz bırakılan çağrı sayısı iki imzada da eşit (4/4); kazanç imzada görünürlüktür | çağıran (denetim çalışma zamanında kalır) | Alanda ve parametrede kazanç kaybolur; `of(null)` hemen düşer, `ofNullable(null)` düşmez |
| Akış Tabanlı Girdi/Çıktı | `write(int)`in düşük sekiz bit ve `read()`in 0–255 artı `-1` sözü iki gerçekleştirimde aynı; tamponlama yalnız çağrı sayısını değiştirir | arayüz (bayt sözü) + çağıran (boşaltma) | Boşaltılmamış tampon veriyi eksik bırakır; boşaltma kararı çağıranındır |
| Dosya Sistemi API'si | Yol cebiri iki sağlayıcıda da aynı; `exists` ve `size` sağlayıcının deposuna sorar | arayüz (yol cebiri) + gerçekleştirim (depo) + çağıran (iki adım) | Varlık denetimi ile açma iki ayrı işlemdir; arada dosya silinirse denetim geçer, açma düşer |
| Dizileştirme | Yapıcı hiç çalışmaz; kurduğu iki değişmezden sıfırı geri okumada kendiliğinden geçerli kalır | çağıran (`readObject` yazılmazsa denetim yok) | Sürüm kimliği sabit tutulup alan adı değişince eski kayıt sessizce varsayılanla geçer |
| Tarih ve Zaman API'si | `Instant.equals` ve `ZonedDateTime.isEqual` "aynı an mı" sorusunu yanıtlar; `LocalDateTime.equals` yanıtlayamaz | tip (taşınan bilgi) + çağıran (saat dilimi vermeden karşılaştırma) | Yaz saati geçişinde çakışan yerel saat iki ayrı ana karşılık gelir; API erken ofseti seçer |
| Düzenli İfadeler | `Pattern` derlenmiş bir değerdir; `matches` ile `find` ayrı sözleşme, açgözlü ile isteksiz ayrı yakalama üretir | arayüz (`Pattern` sözleşmesi) + çağıran (yeniden derleme: tek `etkisiz` satır) | Geri izlemede adım sayısı girdiyle doğrusal artmaz; ölçü adım sayısıdır, süre değildir |

Standart Kütüphane ve Akışlar burada kapanıyor. Sıradaki kurs olan **JVM, Eşzamanlılık ve
Başarım** aynı sorulara üçüncü bir katmanda döner: bir garantinin arkasında artık tek bir
iş parçacığı değil, birden çok iş parçacığının aynı nesneye aynı anda eriştiği bir çalışma
zamanı vardır — ve o zaman "kim garanti veriyor" sorusunun yanıtı zamanlamaya bağlı hâle
gelebilir.

