Effective Java Madde 29: Üreysel Türleri Tercih Edin

JDK tarafından sağlanan üreysel türleri (generic types) ve metotları kullanmak çok zor olmasa da, kendiniz üreysel türler yazmak istediğinizde biraz zorlanabilirsiniz ama bunu öğrenmeniz çok faydalı olacaktır.

Şimdi Madde 7’deki basitleştirilmiş yığıt (stack) gerçekleştirimine bakalım:

// Object tabanlı yığıt - üreysel tür olmak için müsait!
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    
    public void push(Object e) { 
        ensureCapacity(); 
        elements[size++] = e;
    }

    public Object pop() { 
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null; // kullanılmayan referansı kaldır 
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }
    
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    } 
}

Bu sınıf en başından beri üreysel olmalıydı ama sonradan mevcut istemcilere zarar vermeden üreysele dönüştürmek de mümkündür. Bu haliyle, istemciler yığıttan okudukları değerleri kullandıkları türe kendileri dönüştürmek zorundadır. Bu tür dönüşümleri çalışma zamanında başarısız olup aykırı durum (exception) fırlatabilir.

Bir sınıfı üreysele çevirmek için ilk adım, sınıf tanımına bir veya daha çok tür parametresi eklemektir. Bizim örneğimizde eleman türünü temsil edecek şekilde tek bir tür parametresi yeterlidir. Eleman türünü temsil eden tür parametreleri genellikle E olarak isimlendirilir. (Madde 68)

Sonraki adım ise Object türü kullanılan yerlere yeni tanımladığımız tür parametresini yazıp programı derlemek olacaktır:

// Yığıtı üreysel türe çevirmek için ilk deneme. Derlenmez!
public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(E e) { 
        ensureCapacity(); 
        elements[size++] = e;
    }

    public E pop() { 
        if (size == 0) {
            throw new EmptyStackException();
        }
        E result = elements[--size];
        elements[size] = null; // kullanılmayan referansı kaldırın 
        return result;
    }
       ... // isEmpty ve ensureCapacity değişmiyor
}

Stack sınıfını yukarıdaki gibi üreysel türe dönüştürmeye çalışırsak aşağıdaki üreysel dizi yaratma (generic array creation) hatasını alırız:

Stack.java:8: generic array creation
        elements = new E[DEFAULT_INITIAL_CAPACITY];
                   ^

Madde 28’de anlatıldığı gibi E gibi tür parametrelerinden dizi yaratamayız. Bu problem, arka planda dizi kullanan üreysel türler yazmaya çalıştığınızda her zaman karşınıza çıkacaktır. Mantıklı bir şekilde çözmek için iki yol bulunur. Birincisi, diziyi E türünde yaratmak yerine Object türünde yaratıp sonradan E türüne dönüştürmektir. Yani yukarıdaki örnekte 8. satırı aşağıdaki gibi değiştirebiliriz:

elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];

Bu kullanım geçerlidir ancak derleyici bu sefer hata yerine tür güvenliğini sağlayamadığı için uyarı verecektir.

Stack.java:8: warning: [unchecked] unchecked cast
   found: Object[], required: E[]
         elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; 
                        ^

Derleyici burada tür güvenliği olup olmadığından emin olamasa da, siz bunu kanıtlayabilirsiniz. Tabi öncelikle buradaki kontrolsüz tür dönüşümünün programdaki tür güvenliğini tehlikeye düşürmediğinden emin olmalısınız. Burada söz konusu olan elements dizisi private bir alanda saklanıyor ve istemciye asla döndürülmüyor. Diziye eleman eklenen tek yer push metodu. Burada da E türünde geçilen parametreler diziye eklendiği için tür güvenliğini sıkıntıya sokacak bir durum yoktur, dolayısıyla yapılan tür dönüşümü güvenlidir.

Tür dönüşümünün güvenli olduğunu kanıtladığımıza göre, derleyici uyarısını @SuppressWarnings("unchecked") notasyonu ile gizlemeyi deneyebiliriz. (Madde 27) Burada uyarıya sebep olan yapıcı metodun (constructor) tek yaptığı iş dizi yaratmak olduğu için, notasyonu yapıcı metoda uygulamakta bir sakınca yoktur. Yapıcı metot aşağıdaki şekilde güncellendiğinde sınıf hata ve uyarı vermeden derlenir:

// elements dizisi sadece push(E) tarafından eklenen E nesneleri
// içermektedir. Bu, tür güvenliğinin sağlanması için yeterlidir.
// Çalışma zamanında dizinin türü Object[] olacaktır, E[] değil.
@SuppressWarnings("unchecked")
public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

Stack sınıfı içerisinde üreysel dizi yaratma hatasını ortadan kaldırmanın ikinci yolu ise elements dizisinin türünü E[] yerine Object[] yapmaktır. Ancak bunu deneyince aşağıdaki gibi başka bir hata alırsınız:

 Stack.java:19: incompatible types
   found: Object, required: E
           E result = elements[--size];
                              ^

Burada artık elements bir Object dizisi olduğu için, diziden okunan değeri E türündeki referansta saklamamız tür uyumsuzluğundan dolayı mümkün olmuyor. Tabi biz okunan değeri E türüne dönüştürerek hatadan kurtulabiliriz. Stack sınıfında 20. satırı aşağıdaki gibi değiştirirsek:

E result = (E) elements[--size];

Sınıfı tekrar derleyince hata kaybolacaktır ama yine bir uyarı alırsınız:

Stack.java:19: warning: [unchecked] unchecked cast
   found: Object, required: E
        E result = (E) elements[--size]; 
                               ^

Bu ilk çözümde rastladığımıza çok benzer bir uyarı. Derleyici tür güvenliğini garanti edemediği için bizi uyarıyor. Ancak burada da biz tür güvenliği olduğundan eminiz, çünkü diziye eleman eklenen tek yer push metodu ve burada sadece E türünden elemanlar ekleniyor. Dolayısıyla okunan değer de E türünde olmak zorundadır. Madde 27’de anlatılan önerileri göz önünde bulundurarak, uyarıyı sadece buna sebep olan satır için gizliyoruz, pop metodu için değil:

// tür dönüşüm uyarısının doğru biçimde gizlenmesi
public E pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }

    // push metodu sadece E türünden elemanlar eklediğinden 
    // tür dönüşümü güvenlidir
    @SuppressWarnings("unchecked") 
    E result = (E) elements[--size];
    elements[size] = null; // kullanılmayan referansı kaldırın
    return result;
}

Burada anlattığımız iki yöntem de kullanılabilir. Birinci yöntemde okunabilirlik daha yüksektir çünkü dizi sadece E türünden elemanları tuttuğunu açıkça göstermek için E[] türünde tanımlanmıştır. Ayrıca sadece bir yerde, dizi yaratılırken tür dönüşümü yapılmaktadır. Bu şekilde üreysel türler yazılırken çoğunlukla birden fazla yerde diziden okuma yapılır. İkinci yöntem bu sebeple dezavantajlıdır çünkü her okuma yapılan yerde tür dönüşümü de yapılması gerekir. Bu yüzden de pratikte genelde birinci yöntem tercih edilir. Ancak birinci yöntemin dezavantajı da dizinin çalışma zamanındaki türü ile derleme zamanındaki türü uyuşmadığı için yığın kirliliğine (heap pollution – Madde 32) sebep olmasıdır. (E türünün Object‘i temsil ettiği durumda bu problem yaşanmaz ) Bazı programcılar bu konuda çok hassas davranıp ikinci yöntemi tercih etmektedirler, ancak bizim örneğimizde yığın kirliliği zararsızdır.

Aşağıdaki program üreysel Stack sınıfının kullanımını örneklemektedir. Komut satırından geçilen argümanları ters sırada ve büyük harflere çevirerek yazdırmaktadır. String‘in toUpperCase metodunu çağırırken tür dönüşümü yapmaya gerek kalmamıştır:

// Üreysel Stack kullanımı
public static void main(String[] args) {
    Stack<String> stack = new Stack<>();
    for (String arg : args)
        stack.push(arg);
    while (!stack.isEmpty())
        System.out.println(stack.pop().toUpperCase());
}

Bu maddede verdiğimiz Stack örneği, diziler yerine listeler kullanmayı önerdiğimiz Madde 28 ile çelişebilir. Üreysel türler içerisinde dizi yerine liste kullanmak her zaman mümkün olmayabilir veya istenmeyebilir. ArrayList gibi bazı üreysel türler mecburen dizileri kullanacak şekilde yazılmıştır. HashMap gibi örnekler de performans kazanımı için dizilerden faydalanmaktadır.

Bizim Stack örneğinde olduğu gibi, üreysel türlerin büyük çoğunluğu herhangi bir kısıtlamaya gitmeden istediğiniz tür parametrelerini kullanmanıza izin verirler. Örneğin Stack<Object>, Stack<int[]>, Stack<List<String>> gibi kullanımların hepsi geçerlidir, istediğiniz herhangi bir referans türünden elemanlar tutan bir Stack yaratabilirsiniz. Ancak int, double gibi temel türleri tutan bir Stack yaratmaya çalışırsanız derleme hatası alırsınız. Bu, Java’da üreysel tür sisteminin kısıtlarından birisidir. Ancak bunların yerine Integer, Double gibi kutulanmış temel türleri (boxed primitive type) kullanarak bu sorunu aşabilirsiniz. (Madde 61)

Kullanılabilecek tür parametrelerinde kısıtlama yapan üreysel türler de mevcuttur. Örneğin java.util.concurrent.DelayQueue sınıfının tanımı aşağıdaki gibidir:

class DelayQueue<E extends Delayed> implements BlockingQueue<E>

Burada parametre olarak tanımlanan <E extends Delayed>, ifadesinin anlamı şudur: istemci tür parametresi olan E yerine sadece Delayed türünün bir alt türünü geçebilir. Bu şekilde DelayQueue sınıfı ve bu sınıfın istemcileri, DelayQueue‘nun elemanları için Delayed türünden gelen metotları da tür dönüşümüne gerek kalmadan ve ClassCastException riski olmadan kullanabilirler. Buradaki tür parametresi E aynı zamanda sınırlandırılmış tür parametresi (bounded type parameter) olarak da anılır. Java’da her tür kendisinin alt türü kabul edildiği için (JLS 4.10), DelayQueue<Delayed> geçerli bir ifadedir.

Özetle, istemcilerin kullanırken tür dönüşümü yapmak zorunda kaldığı türlere kıyasla üreysel türleri kullanmak hem daha güvenlidir hem de istemcilere kullanım kolaylığı sağlar. Yeni türler tanımlarken, istemcilerin tür dönüşümü yapmadan bunları kullanabileceğinden emin olun. Bu çoğu zaman bu türlerin üreysel olarak tasarlanması anlamına gelmektedir. Eğer üreysel olması gerektiği halde olmayan türleriniz varsa bunları güvenle üreysele dönüştürebilirsiniz çünkü mevcut istemciler etkilenmeyecek ve yeni istemciler için kolaylık sağlayacaktır. (Madde 26)

Share

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