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 birOptional<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çmapile büyük harfe çevriliporElseile 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
orElsehemorElseGetile ç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.
- 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üOptionalda 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.
- FN31 —
Optional.of(null)veOptional.ofNullable(null)ayrı ayrı denenir; ikincisinin sonucundanget()ç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
Optionaldö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.Optionalbu 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. Optionalzincirlemeyi destekler:mapboş bir kap üzerinde hiç çalışmaz, ara adımlarda ayrı bir boş değer denetimi yazmaya gerek kalmaz. AmaorElseargümanını kap dolu olsa bile her zaman hesaplar;orElseGetyalnı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:
Optionalbir 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ındaNullPointerExceptionile düşer;Optional.ofNullable(null)düşmez, boş bir kap döndürür ve denetimget()ç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.