Press "Enter" to skip to content

Effective Java Madde 61: Kutulanmış (boxed) Yerine Temel Türleri Tercih Edin

Java’da tür sistemi iki parçadan oluşur: int, double, boolean gibi temel türler ve String, List gibi referans türleri. Her temel türe karşılık gelen birer tane de kutulanmış temel tür (boxed primitive type) bulunmaktadır. Örneğin, int, double ve boolean temel türlerine karşılık gelen kutulanmış türler Integer, Double ve Boolean olmaktadır.

Madde 6’da anlatıldığı gibi, temel türlerle kutulanmış türler arasında yapılan otomatik dönüştürme işlemleri (autoboxing ve auto-unboxing), bu türler arasındaki farkı bir nebze saklasa da ortadan kaldırmaz. Bu türler arasında önemli farklar vardır ve kod yazarken bunlardan hangisini kullandığınızın farkında olmanız, bu ikisi arasında seçim yaparken dikkatli olmanız gerekmektedir.

Temel türler ve kutulanmış türler arasında üç tane önemli fark vardır. Birincisi, temel türlerin sadece değerleri vardır ancak kutulanmış türlerin hem değerleri hem de bundan bağımsız kimlikleri (identity) vardır. Başka bir deyişle, iki tane kutulanmış tür nesnesi aynı değere ama farklı kimliklere sahip olabilirler. İkinci olarak, temel türlerde sadece işlevsel değerlerden bahsedebiliyoruz ancak kutulanmış türlerde buna ek olarak bir de işlevsel olmayan bir null değeri mümkündür. Son olarak, temel türlerin kutulanmış türlere kıyasla daha verimli çalıştıklarını söyleyebiliriz. Bu üç farklılık da eğer dikkatli olmazsanız başınıza büyük sorunlar açabilir.

Örneğin, aşağıdaki karşılaştırıcıya (comparator) bir bakalım. Burada karşılaştırıcı Integer değerlerini sayısal olarak artan sırada ifade etmek istemektedir. (Hatırlayın, karşılaştırıcılarda compare metodu birinci değer ikinciden küçükse negatif bir sayı, büyükse pozitif bir sayı ve iki sayı eşitse 0 değerini döndürmelidir.) Gerçek hayatta böyle bir karşılaştırıcı yazmanıza gerek yoktur ama ortadaki sorunu açıklamak için güzel bir örnek oluşturmaktadır:

Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

Bu karşılaştırıcının doğru çalışması gerektiğini düşünebilirsiniz ve birçok testten de başarıyla geçecektir. Mesela, Collections.sort ile kullanıldığında içinde milyon tane eleman olan bir listeyi içerisinde tekrar eden değerler olsa bile doğru biçimde sıralayacaktır. Ancak ortada ciddi bir sorun var! Bunu anlamak için aşağıdaki kod satırını çalıştırıp sonucu yazdırabiliriz:

naturalOrder.compare(new Integer(42), new Integer(42))

Burada her iki Integer değeri de 42 değerine sahiptir ve dolayısıyla compare metodu 0 döndürmelidir değil mi? Beklentimiz bu ancak sonuç 1 çıkmaktadır yani birinci Integer ikinciden daha büyüktür!

Peki buradaki problem nedir? naturalOrder içindeki i < j aslında doğru çalışmaktadır. Bu karşılaştırma için i ve j ile temsil edilen Integer değerleri int türüne dönüştürülmekte (auto-unboxing) ve ikisi de 42 değerine sahip olduğu için i < j false değerini üretmektedir. Daha sonra ikinci karşılaştırma, yani i == j işletilmektedir. Bu ifade ile değer karşılaştırması değil kimlik karşılaştırması yapılmaktadır. Her ne kadar i ve j nesneleri aynı değeri taşısalar da, farklı Integer nesneleri oldukları için bu karşılaştırma false döndürecektir ve metodun sonucu 1 olarak döndürülecektir. Kutulanmış türlere == operatörünün uygulanması neredeyse her zaman yanlış sonuç almanıza neden olacaktır.

Pratikte, eğer bir türün doğal sıralanışını ifade etmek istiyorsanız Comparator.naturalOrder() metodunu çağırabilirsiniz. Kendiniz yazmak isterseniz de karşılaştırıcı yapıcı metotlarını kullanabilirsiniz veya temel türler üzerinde statik karşılaştırma metotlarını çağırabilirsiniz. (Madde 14) Ancak yine de yukarıdaki bozuk kodu düzeltmek isterseniz aşağıdaki gibi temel türde yeni değişkenler tanımlayıp karşılaştırmaları da bu değişkenler üzerinden yapabilirsiniz. Böylece hatalı olan kimlik karşılaştırması yapmaktan kaçınmış olursunuz:

Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
    int i = iBoxed, j = jBoxed; // Auto-unboxing
    return i < j ? -1 : (i == j ? 0 : 1);
};

Şimdi de aşağıdaki ufak programı ele alalım:

public class Unbelievable {
    static Integer i;
    public static void main(String[] args) {
        if (i == 42)
            System.out.println("Unbelievable");
    } 
}

Bu programın çıktısı Unbelievable değildir ama sonuç yine de çok ilginçtir. Program i == 42 işletilirken NullPointerException fırlatacaktır. Buradaki problem i değişkeninin int değil Integer türünde olmasıdır. Diğer referans türlerinde olduğu gibi burada da i‘nin ilk değeri null olacaktır. i == 42 işletilirken bir Integer değişkeni bir int değeriyle karşılaştırılmaktadır. Temel türler ve kutulanmış türler bu şekilde bir işleme sokulduğunda, işlemin yapılabilmesi için kutulanmış değişken temel türe dönüştürülecektir. Ancak i‘nin değeri null olduğu için bu dönüştürme başarısız olur ve NullPointerException fırlatılır. Bu programda gösterildiği gibi, bu problem her yerde karşınıza çıkabilir. Bunu çözmek içinse i değişkenini int temel türünde tanımlamak yeterli olacaktır.

Şimdi de Madde 6’da kullandığımız kod örneklerinden birine bakalım:

// Çok yavaş çalışır! Burada yaratılan nesneleri fark edebildiniz mi?
    public static void main(String[] args) {
        Long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }
       System.out.println(sum);
}

Bu program olması gerekenden çok daha yavaş çalışır çünkü hatalı bir biçimde sum değişkenilong yerine Long türünde tanımlanmıştır. Bu kod hatasız bir biçimde derlenir ancak değişken sürekli olarak temel tür ve kutulanmış tür arasında gereksiz yere dönüştürüldüğü için ciddi bir yavaşlık yaratır.

Bu maddede işlenen her üç kod örneğinde de problem aslında aynıdır: programcı temel tür ve kutulanmış tür arasındaki farkları dikkate almamış ve bunun acısını çekmiştir. İlk iki örnekte programlar hatalı sonuç üretmiş, üçüncü örnekte ise ciddi bir performans kaybı oluşmuştur.

Peki ne zaman kutulanmış türleri kullanmalıyız? Bu türlerin birkaç tane kullanım alanı vardır. Bunlardan bir tanesi koleksiyonlar içerisinde anahtar (key) veya değer (value) olarak kullanmaktır. Koleksiyonlar içerisine temel türleri koyamadığımız için mecburen kutulanmış tür kullanmamız gerekmektedir. Bununla beraber, üreysel tür ve metotları tanımlarken de kutulanmış türler kullanılır. Örneğin, ThreadLocal<int> şeklinde bir kullanıma izin verilmez, bu sebeple ThreadLocal<Integer> kullanmalıyız. Son olarak, reflection (yansıma) kullanarak yapılan metot çağrılarında da kutulanmış türleri kullanmak gerekmektedir. (Madde 65)

Özetle, seçme şansınız olan her yerde kutulanmış türlerin yerine temel türleri tercih edin. Temel türler hem daha basittir hem de daha verimli çalışırlar. Eğer kutulanmış türleri kullanmak zorundaysanız, çok dikkatli olun! Otomatik dönüştürme (autoboxing) kodun kalabalığını azaltsa da, kutulanmış türlerin sebep olabileceği sorunları engellemez. Programınız eğer kutulanmış türler ile == operatörünü kullanıyorsa, burada yapılan şey kimlik karşılaştırması olur ve çok büyük ihtimalle sizin istediğiniz şey bu değildir. Eğer null değere sahip bir kutulanmış değişkenin temel türe dönüştürülmesi gerekirse (auto-unboxing), sonuçta NullPointerException fırlatılacaktır. Son olarak da, siz farkında olmadan temel türdeki değişkenleriniz kutulanmış türlere dönüştürülüyorsa, gereksiz nesne yaratmış olursunuz ve performans kayıpları görebilirsiniz.

Share

Leave a Reply

%d bloggers like this: