İçeriğe geç
academia.sh

Ders 10 / 15

`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 biri ç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.

İçindekiler

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

  • FN30Optional<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.
// 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.

  • FN31Optional.of(null) ve Optional.ofNullable(null) ayrı ayrı denenir; ikincisinin sonucundan get() çağrılarak denetimin ne zaman düştüğü ölçülür.
// 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, biri ç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 payda 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.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat