Press "Enter" to skip to content

Effective Java Madde 55: Optional Döndürürken Dikkatli Olun

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.

Share

Leave a Reply

%d bloggers like this: