Java 8’den önce, belli durumlarda değer döndüremeyen bir metot yazabilmek için iki farklı yöntem mevcuttu. Bunlardan birincisi aykırı durum (exception) fırlatmak, ikincisi ise metodun dönüş türü nesne referansı ise null
döndürmektir. Bu yaklaşımların ikisi de mükemmel değildir. Aykırı durumlar istisnai durumlar için kullanılmalıdır (Madde 69) ve aynı zamanda yaratılması masraflıdır. Null
döndürmenin ise başka problemleri vardır. Bu durumda istemciler her metot çağrısında null
denetimi yapmak zorunda kalacaklardır. Bunu unuttukları taktirde metottan dönen null
değeri bir yerde saklanabilir, programın alakasız bir yerinde, hiç beklenmedik bir zamanda NullPointerException
fırlatılabilir.
Java 8’de bu tarz metotları yazabilmek için üçüncü bir yol eklenmiştir. Optional<T>
sınıfı ya hiçbir şey ya da null
olmayan bir T
referansı tutabilen değiştirilemez bir taşıyıcı (container) sınıf olarak tanımlanmıştır. Başka bir deyişle bu taşıyıcı sınıf nesneleri boş olabilir veya var olan bir T
nesnesine işaret eden bir referans tutabilir. Dolayısıyla en fazla bir elemana sahip olabilirler ve değiştirilemez (immutable) taşıyıcılardır.
Bir metot eğer T
türünde değerler döndürüyorsa ancak hiçbir değer döndüremediği durumlar da varsa, dönüş türü olarak Optional<T>
kullanılabilir. Böylece metodun değer döndüremediği durumlarda için içi boş bir Optional<T>
döndürmesi mümkün olur. Optional döndüren metotlar aykırı durum fırlatan metotlara göre daha esnektirler ve daha kolay kullanılırlar, null
döndüren metotlara göre de hata yapılma ihtimalini düşürürler.
Madde 30’da bir koleksiyonda bulunan elemanların en büyüğünü bulan aşağıdaki metodu yazmıştır:
// Koleksiyondaki maksimum değeri bulur
// Eğer eleman yoksa aykırı durum fırlatır
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty()) {
throw new IllegalArgumentException("Empty collection");
}
E result = null;
for (E e : c) {
if (result == null || e.compareTo(result) > 0) {
result = Objects.requireNonNull(e);
}
}
return result;
}
Eğer metoda geçilen koleksiyon boş ise, bu metot IllegalArgumentException
fırlatmaktadır. Madde 30’da bahsettiğimiz gibi, buna alternatif olarak Optional<E>
döndürmeyi deneyebiliriz. Aşağıdaki kod bunu yapmaktadır:
// Koleksiyondaki maksimum değeri Optional<E> olarak döndürür
public static <E extends Comparable<E>>
Optional<E> max(Collection<E> c) {
if (c.isEmpty()) {
return Optional.empty();
}
E result = null;
for (E e : c) {
if (result == null || e.compareTo(result) > 0) {
result = Objects.requireNonNull(e);
}
}
return Optional.of(result);
}
Gördüğünü gibi optional döndürmek oldukça kolay. Tek yapmanız gereken uygun bir statik fabrika kullanarak bunu yaratmak. Yukarıdaki kodda bunlardan iki tanesini kullandık. Optional.empty()
boş bir optional, Optional.of(value)
ise value
değerine sahip bir optional döndürecektir. Ancak burada value
değeri null
ise NullPointerException
fırlatılır. Optional.ofNullable(value)
kullanırsanız value
değerinin null
olması durumunda boş bir optional döndürecektir. Optional dönüş türüne sahip bir metottan asla null
döndürmeyin, çünkü optional kullanmaktaki bütün amacımız zaten bunlardan kurtulmak.
Streamlerle kullanılan birçok sonlandırıcı işlem optional döndürür. Yukarıdaki max
metodunu stream kullanarak yazacak olsak, Stream içindeki max
metodu bütün işi yapacak ve bize optional döndürecektir:
// Stream kullanarak yazılmış max metodu - Optional<E> döndürür
public static <E extends Comparable<E>>
Optional<E> max(Collection<E> c) {
return c.stream().max(Comparator.naturalOrder());
}
Optional metotlar mantık olarak denetimli aykırı durumlara (checked exception) benzerler (Madde 71) ve istemciyi metottan herhangi bir değer dönmeme durumuyla yüzleşmek zorunda bırakırlar. Metottan null
döndürmek veya denetimsiz aykırı durum (unchecked exception) fırlatmak, istemcinin bu durumu görmezden gelmesine olanak sağlar. Bu da kötü sonuçlar doğurabilir. Denetimli aykırı durum fırlatmak ise istemciyi bu durumu ele almaya mecbur bırakır.
Eğer bir metot optional döndürüyorsa, istemci değer dönmeyen durumda ne yapılması gerektiğini seçebilir ve orElse
gibi bir metotla varsayılan (default) bir değer belirleyebilir:
// Varsayılan değer kullanılan optional kullanımı
String lastWordInLexicon = max(words).orElse("No words...");
Burada eğer max
metodunun döndürdüğü optional boş değilse onun değeri, boş ise No words…
değeri kullanılacaktır. Varsayılan bir değer kullanmak yerine uygun bir aykırı durum fırlatmak da isteyebilirsiniz. Dikkat ederseniz aşağıda orElseThrow
metoduna bir aykırı durum yaratıp onu geçmek yerine aykırı durum yaratan bir fabrika geçiyoruz. Böylece gerekli olmayan durumlarda aykırı durum yaratılmasını engellemiş oluyoruz:
// Optional kullanarak aykırı durum fırlatıyoruz
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
Eğer optional’ın boş olmadığını biliyorsanız direk içindeki elemanı alacak şekilde kodunuzu yazabilirsiniz. Ancak hata yaparsanız ve optional boş ise NoSuchElementException
fırlatılacaktır:
// Dolu olduğunu bildiğimiz bir optional'dan değerini okuyoruz
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
Bazen de varsayılan değer üretiminin masraflı olduğu durumlarla karşılaşabilirsiniz. Bu durumda, varsayılan değeri sadece gerekli olduğunda üretmek için Supplier<T>
parametresi kabul eden orElseGet
metodunu kullanabilirsiniz. Optional
sınıfı başka özel durumlar için tasarlanmış filter
, map
, flatMap
, ve ifPresent
gibi farklı metotlar da sunmaktadır. Java 9’da iki tane daha böyle metot eklenmiştir: or
ve ifPresentOrElse
. Eğer örneklerde kullandığımız nispeten basit metotlar ihtiyacınızı karşılamıyorsa, bu metotların dokümantasyonuna bakarak kullanmayı deneyebilirsiniz.
Eğer bu metotların hiç birisi ihtiyacınızı karşılamıyorsa, Optional
sınıfındaki isPresent()
metodunu kullanın. Bu metot çok basit olarak optional boş ise false
, dolu ise true
döndürecektir. Buradan okuduğunuz değerle optional üzerinde istediğiniz işlemleri yapabilirsiniz. Ancak unutmayın ki isPresent()
kullanarak yazılan kodların çoğu aslında yukarıdaki metotlar kullanılarak çok daha kısa ve kolay anlaşılacak biçimde yazılabilir.
Aşağıdaki kod örneğine bakalım. Burada bir işlem (process) için, eğer varsa üst işleminin (parent process) kodunu, yoksa da “N/A” yazdıran bir kod görüyoruz. ProcessHandle
Java 9’da dile eklenmiştir:
Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("Parent PID: " + (parentProcess.isPresent() ?
String.valueOf(parentProcess.get().pid()) : "N/A"));
Burada parentProcess
optional nesnesinin boş olup olmadığını isPresent()
ile kontrol ettikten sonra koşullu bir ifade yazdık. Ancak bunu yapmak yerine aşağıdaki gibi Optional
içindeki map
metodunu kullanabilirdik:
System.out.println("Parent PID: " +
ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
Stream kullanırken bazı durumlarda elinizde Stream<Optional<T>>
olabilir ve siz sadece içi dolu olanları kullanarak bunu Stream<T>
‘ye çevirmek isteyebilirsiniz:
streamOfOptionals
.filter(Optional::isPresent)
.map(Optional::get)
Java 9’da Optional
sınıfına stream()
metodu da eklenmiştir. Bu metot Optional
nesnelerini Stream
‘e çeviren bir adaptördür. Çıkan stream eğer optional doluysa tek elemana, değilse sıfır elemana sahip olacaktır. Stream
sınıfının flatMap
metoduyla (Madde 45) birlikte kullanırsak, yukarıdaki kodu daha kısa biçimde aşağıdaki gibi de yazabiliriz:
streamOfOptionals
.flatMap(Optional::stream)
Maalesef optional kullanımı bütün türler için faydalı değildir. Koleksiyonlar, streamler, diziler ve map gibi taşıyıcı türler Optional
ile sarmalandığında fayda sağlamazlar. Yani Optional<List<T>>
döndürmektense boş bir List<T>
döndürmek daha mantıklıdır. (Madde 54). Bu istemciler için de kolaylık sağlayacaktır çünkü istemci optional değerin boş olup olmadığıyla ilgilenmek zorunda kalmayacaktır. ProcessHandle
sınıfının Optional<String[]>
döndüren arguments
isimli bir metodu vardır ancak bu anormal bir durum olarak değerlendirilmeli, örnek alınmamalıdır.
Peki bir metodun dönüş türü hangi durumlarda T
yerine Optional<T>
olmalıdır? Bir kural olarak şunu söyleyebiliriz: eğer bir metodun değer döndüremediği durumlar varsa ve istemciler bu durumu ele almak zorundaysa T
değil Optional<T>
döndürmeliyiz. Tabi Optional<T>
kullanmanın da bir maliyeti vardır. Performansın çok kritik olduğu durumlarda optional kullanımından doğan ek nesne yaratma ve okuma maliyetleri küçük bir fark yaratabilir. Ancak bu performans farkını ölçüm yaparak kanıtlayamıyorsanız endişe etmenize gerek yoktur. (Madde 67)
Kutulanmış (boxed) temel türle sarmalanmış bir optional döndürmek, normal bir temel tür değeri döndürmeye kıyasla oldukça masraflı olmaktadır. Bu sebeple, kütüphane tasarımcıları int
, long
ve double
temel türleri için özel OptionalInt
, OptionalLong
, ve OptionalDouble
sınıfları yazmışlardır. Bu sınıflar Optional<T>
‘de bulunan metotların çoğunu barındırırlar. Bu sebeple, kutulanmış temel türleri Optional
ile sarmalayıp kullanmayın. Daha küçük temel türler olan Boolean
, Byte
, Character
, Short
, ve Float
ise bu kuralın istisnası olabilirler.
Şimdiye kadar nerelerde optional kullanabileceğimizi konuştuk ama nerelerde bunları kullanmamamız gerektiğinden bahsetmedik. Genel olarak söylemek gerekirse, optional nesneleri koleksiyonlarda veya dizilerde eleman, map nesnelerinde anahtar veya değer olarak kullanmak gereksiz karmaşıklığa yol açar ve tavsiye edilmez.
Burada cevaplamadığımız bir soru kaldı: Bir nesne alanı içinde (instance field) optional değer saklamak uygun olur mu? İlk bakışta bu “kötü kokan koda” (code smell) işaret etmektedir. Optional alanlar tanımlamak yerine bunları alt sınıflara taşıyabiliriz. Ancak Optional
alanlar tutmak bazen mantıklı olabilir. Madde 2’de kullandığımız NutritionFacts
sınıfını düşünelim. Bu sınıfın tanımlı alanlarının çoğu zorunlu değildir. Bu alanların her bir kombinasyonu için çok sayıda alt sınıf oluşturmak da çok saçma olurdu. Üstelik bu alanların bazıları temel türdeki değişkenlerle ifade edilmiş. NutritionFacts
için yazabileceğimiz en iyi API, zorunlu olmayan alanlar için yazdığımız erişim metotlarından optional nesneler döndürmelidir. Bu sebeple de bunların optional nesne alanları olarak tanımlanması uygun olacaktır.
Özetle, eğer her durumda bir değer döndürmesi mümkün olmayan bir metot yazıyorsanız ve istemcilerin bu ihtimali ele alması gerektiğini düşünüyorsanız optional dönüş türü kullanın. Eğer performans konusunda çok hassassanız ve optional kullanmanın getirdiği performans kaybını ölçebiliyorsanız null
döndürmek veya aykırı durum fırlatmak bir seçenek olarak kalabilir. Son olarak, optional nesneler çok büyük oranda dönüş türü olarak kullanılırlar. Bunun dışındaki kullanımlara çok nadiren başvurun.