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.