Effective Java Madde 28: Listeleri Dizilere Tercih Edin

Dizilerin, üreysel türlerden iki önemli farkı vardır. Birincisi, diziler covariant üreysel türler ise invariant olarak tanımlanır. Peki bu ne demektir? Örneğin, Sub türü Super türünün bir alt türü ise (kalıtıyorsa veya arayüz olarak uyguluyorsa), Sub[] dizi türü de Super[] dizi türünün alt türüdür. Üreysel türlerde ise Type1 ve Type2 herhangi iki tür olmak üzere, List<Type1> ve List<Type2> arasında alt/üst tür ilişkisi bulunmaz. (JLS 4.10) Burada üreysel türlerin eksik kaldığını düşünebilirsiniz ama aslında problemli olan dizi türleridir. Aşağıdaki kod geçerli olmasına rağmen bozuktur:

// Çalışma zamanında çöker!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // ArrayStoreException fırlatır

Ancak aşağıda List ile yazılmış benzer kod derlenmeyecektir:

// Derlenmez!
List<Object> ol = new ArrayList<Long>(); // Uyumsuz türler
ol.add("I don't fit in");

Her iki durumda da Long beklenen bir yere String ekleyemezsiniz. Ancak, diziler söz konusu olduğunda hata yaptığınızı çalışma zamanına kadar farketmezsiniz, liste kullanınca derleme zamanında yakalarsınız. Bu da büyük bir avantajdır.

Diziler ile üreysel türler arasındaki ikinci önemli farka gelelim. Diziler, eleman türlerini ancak çalışma zamanında bilirler ve gerekli tür denetimlerini o zaman uygularlar. Az önceki örnekte olduğu gibi, bir Long dizisine String koymak isterseniz çalışma zamanında ArrayStoreException alırsınız. Bunun tersine, üreysel türlerde tür denetimleri sadece derleme anında yapılır ve eleman türü bilgisi çalışma zamanında silinir (type erasure). Çalışma zamanında tür bilgisinin silinmesi sayesinde üreysel türler, Java 5’den önce yazılmış üreysel türleri kullanmayan kodlarla uyumlu bir biçimde çalışabilmiştir. (Madde 26)

Bu temel farklılıklar nedeniyle, diziler ve üreysel türler birbirleriyle uyumlu değildir. Örneğin, dizi yaratırken üreysel türler, parametreli türler veya tür parametreleri kullanamazsınız. Bu sebeple new List<E>[], new List<String>[], new E[] gibi dizi yaratan kullanımlar geçersizdir. Hepsi de derleme anında hata verecektir.

Peki neden üreysel türlerden bir dizi yaratamıyoruz? Bunun sebebi yine tür güvenliğini bozmasından ötürüdür. Eğer bunu yapabiliyor olsaydık, çalışma zamanında ClassCastException aykırı durumlarıyla karşılaşırdık. Bunu örneklemek için aşağıdaki koda bakalım:

// Bu kod derlenmez! stringLists tanımı geçersiz
List<String>[] stringLists = new List<String>[1]; 
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0);

Normalde 2. satırdaki stringLists tanımlayan ifade derlenmeyecektir ancak şimdilik bunun geçerli olduğunu farzedelim. 3. satırda tek elemanlı bir Integer listesi yaratıp intList değişkeninde saklıyoruz. 4. satırda ise ilk başta yarattığımız stringsList‘i bir Object dizisi referansında saklıyoruz. Yazının başından hatırlayacağınız üzere bu ifade geçerlidir. Sonra da 5. satırda, Object dizisinin tek elemanına daha önce yarattığımız List<Integer> türündeki diziyi atıyoruz. Bu da geçerli bir ifadedir çünkü List<Integer> türü çalışma zamanında List, List<String>[] ise List[] olarak değerlendirilecektir. (type erasure) Bu çok saçma bir durumdur çünkü ArrayStoreException aykırı durumu ile karşılaşmadan, List<String> nesneleri tutması gereken bir diziye, List<Integer> nesnesi eklemiş olduk. 6. satırda bu diziden eleman okurken tür uyuşmazlığı yüzünden ClassCastException alırız ve program çöker. Bütün bunları engellemek için, 2. satırdaki üreysel dizi tanımı yasaklanmıştır.

Üreysel dizi tanımlamanın yasak olması size sinir bozucu gelebilir. Bu yüzden mesela üreysel bir koleksiyonun aynı eleman türünden bir dizi döndürmesi genellikle mümkün olmaz. (Madde 33’de bunun kısmi bir çözümü var) Bu durum aynı zamanda üreysel türlerle beraber varargs (metotlarda üç nokta kullanarak aynı türden değişken sayıda parametre geçmenizi sağlayan şey) kullanırken kafa karıştırıcı derleyici uyarılarıyla karşılaşmanıza sebep olur. (Madde 53) Bunun sebebi de şudur: varargs kullanan bir metodu çağırdığınız zaman, bu değişken sayıdaki parametreler biz dizi içinde tutulurlar. Bu dizinin elemanları çalışma zamanında tür bilgisini kaybediyorsa (type erasure), derleyici uyarı verecektir. Bununla ilgili de SafeVarargs notasyonunun kullanımı Madde 32’de anlatılmıştır.

Derleyiciden üreysel dizi yaratma hatası veya dizi türlerine dönüşüm yaparken derleyici uyarıcı alırsanız, en iyi çözüm E[] türü yerine List<E> kullanmak olacaktır. Bu şekilde ufak bir performans kaybınız olsa da daha tür güvenliği sağlanacaktır.

Örneğin, yapıcı metodunda Collection alan ve choose() metodunda bu Collection içerisinden rastgele bir elemanı döndüren bir Chooser sınıfı yazalım. Üreysel türler kullanılmadığında aşağıdaki gibi bir kod çıkabilir:

// Chooser - üreysel türlere ihtiyacı olan bir sınıf!
public class Chooser {
    private final Object[] choiceArray;
    public Chooser(Collection choices) {
        choiceArray = choices.toArray();
    }
    
    public Object choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}  

Bu sınıfı kullanmak için, choose() metodundan dönen Object nesnesini her seferinde ihtiyacınız olan türe dönüştürmeniz gerekir. Eğer yanlış türe çevirirseniz de hata verir. Madde 29’daki öneriyi dikkate alarak, Choose sınıfını üreysel yapmaya çalışalım:

// Sınıfı üreysele çevirmek için ilk deneme - derlenmez! 
public class Chooser<T> {
    private final T[] choiceArray;
    public Chooser(Collection<T> choices) { 
        choiceArray = choices.toArray();
    }

    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}

Bu sınıfı derleyince aşağıdaki hatayı alırsınız:

   Chooser.java:5: error: incompatible types: Object[] cannot be
   converted to T[]
           choiceArray = choices.toArray();
                                        ^
     where T is a type-variable:
       T extends Object declared in class Chooser

Çok da problem değil, Object dizisini T dizisine çeviririm derseniz:

choiceArray = (T[]) choices.toArray();

Yukarıdaki hatadan kurtulursunuz ama bu sefer de uyarı alırsınız:

Chooser.java:9: warning: [unchecked] unchecked cast
           choiceArray = (T[]) choices.toArray();
                                              ^
T extends Object declared in class Chooser

Burada derleyici tür dönüşümünün düzgün çalışacağını garanti edemeyeceğini söylüyor çünkü T‘nin çalışma zamanında hangi türü temsil edeceğini bilmiyor. Hatırlayın, üreysel türlerde tür bilgisi çalışma zamanında silinmektedir. Peki program çalışır mı? Evet ama derleyici bundan emin olamıyor. Siz buna kendinizi ikna edip uyarıyı SuppressWarning notasyonu ile gizleyebilirsiniz, ancak uyarıdan da kurtulmak en doğrusu olacaktır. (Madde 27)

Derleyici hatasını gidermek için, dizi yerine liste kullanabiliriz. Aşağıda Chooser sınıfının hatasız ve uyarısız derlenen bir versiyonu verilmektedir:

// List kulanılan Chooser - tür güvenliği var
public class Chooser<T> {
    private final List<T> choiceList;
    public Chooser(Collection<T> choices) { 
        choiceList = new ArrayList<>(choices);
    }

    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceList.size()));
    } 
}

Bu versiyonda az bir performans kaybı bulunsa da, çalışma zamanında bize ClassCastException fırlatmayacağını garanti ettiği için tercih sebebidir.

Özetle, üreysel türler ve dizilerin uymak zorunda olduğu tür kuralları birbirlerinden farklıdır. Bu farklılıklar yüzünden, diziler çalışma zamanında tür güvenliği sağlarken derleme zamanında sağlayamazlar. Üreysel türler için bu durum tam tersidir. Bu yüzden de bu ikisi birbirleyle pek uyumlu çalışmazlar. Eğer bunları beraber kullanmaya çalışırken derleyiciden hata veya uyarı alırsanız, dizileri listelere dönüştürmeyi deneyin.

Share

3 Replies to “Effective Java Madde 28: Listeleri Dizilere Tercih Edin”

Bir Cevap Yazın