Effective Java Madde 32: Üreysellerle varargs Metotları Birlikte Kullanırken Dikkatli Olun

Hem varargs metotlar (Madde 53) hem de üreyseller (generics) Java 5 ile birlikte dile eklenmiş olmalarına rağmen birbirleriyle pek de uyumlu çalışmazlar. Varargs metotların amacı istemcilerin metotlara değişken sayıda argüman geçebilmelerini sağlamaktır. Bir varargs metot çağırıldığı zaman, bu değişken sayıdaki argümanlar bir dizi yaratılarak onun içerisinde tutulur. Bu dizi, her ne kadar bir gerçekleştirim detayı olsa da gizlenmiş değil erişilebilir durumdadır. Sonuç olarak, varargs parametreler üreysel türler içerdiği zaman derleyiciden aşağıdaki gibi kafa karıştırıcı uyarılar alırız.

warning: [unchecked] Possible heap pollution from
       parameterized vararg type List<String>

Bu uyarıda bahsi geçen yığın kirliliği (heap pollution), parametreli bir tür değişkeninin başka bir türü temsil ettiği durumlarda ortaya çıkar. Bu da derleyicinin otomatik yaptığı tür dönüşümlerinin başarısız olmasına sebebiyet verebilir.

Bunu anlamak için aşağıdaki koda bakalım:

// varargs ile üreysel türleri karıştırmak 
// tür güvenliğini tehlikeye sokar
static void dangerous(List<String>... stringLists) {
    List<Integer> intList = List.of(42);
    Object[] objects = stringLists;
    objects[0] = intList;              // yığın kirliliği 
    String s = stringLists[0].get(0);  // ClassCastException
}

Bu metot görünürde bir tür dönüşümü yapmıyor olmasına rağmen bir veya daha fazla argüman ile çağrıldığında ClassCastException fırlatır. Son satırda derleyici tarafından eklenen bizim görmediğimiz bir tür dönüşümü vardır. Bu dönüşüm başarısız olmaktadır ve tür güvenliğinin sağlanamadığını göstermektedir. Tür dönüşümünün başarısız olmasının sebebi de bir önceki satırda içinde List<String> olması gereken stringLists dizisinin ilk elemanına List<Integer> atayarak yığın kirliliğine sebebiyet verilmesidir. Bu şekilde üreysel türlerin varargs parametre olarak kullanıldığı metotlarda, varargs dizisine değer eklemek güvenli değildir.

Bu örnek akıllara bir soruyu da getirmektedir. Biz üreysel dizileri new List<String>[1] gibi bir ifade ile yaratamıyorsak, o zaman üreysel varargs parametresi kullanan metotlar tanımlamak neden yasak değil? Bunun sebebi üreysel varargs metotların pratikte çok faydalı olabilmesidir. Bu sebeple dili tasarlayanlar getirdiği avantajlara karşılık bu uyumsuzlukla yaşamayı tercih etmişlerdir. Java kütüphaneleri de bundan sıklıkla faydalanmışlardır: Arrays.asList(T... a), Collections.addAll(Collection<? super T> c, T... elements), ve EnumSet.of(E first, E... rest) metotlarında olduğu gibi. Ancak bizim yazdığımız dangerous metodunun aksine bunlarda tür güvenliği vardır.

Java 7 ile birlikte programcıların üreysel varargs metotlar yazdıklarında ortaya çıkan derleyici uyarılarını gizleyebilmeleri için SafeVarargs notasyonu dile eklenmiştir. Bu notasyon, programcının metotta tür güvenliğinin var olduğunu dair bir taahhütüdür. Bu durumda derleyici uyarı vermeyi durduracaktır.

Bu notasyonu kullanmadan önce metotta gerçekten de tür güvenliği olduğundan emin olmamız şarttır. Peki bundan nasıl emin olabiliriz? Varargs parametreli bir metot çağrıldığında parametre değerlerini tutmak için bir dizi yaratıldığını hatırlayın. Eğer metot bu diziye hiçbir şey yazmıyorsa ve dizi referansını başka metotların erişimine açmıyorsa o zaman tür güvenliği vardır diyebiliriz. Başka deyişle, varargs parametre dizisi sadece parametre değerlerini taşımak için kullanılıyorsa o zaman güvenlidir. Zaten varargs metotların amacı da budur.

Belirttiğimiz gibi varargs parametre dizisine hiçbir değer yazmadan da tür güvenliğini bozabilirsiniz. Eğer bu dizinin referansını başka metotların erişimine açarsanız tür güvenliğini kaybetme ihtimali olacaktır. Aşağıda tek görevi parametre değerlerini geri döndürmek olan bir metot görüyoruz.

// varargs parametre dizisinin referansını açığa çıkardığı 
// için tür güvenliğini tehlikeye atmaktadır
static <T> T[] toArray(T... args) {
    return args;
}

Bu metot pek tehlikeli görünmese aslında öyledir. args dizisinin türü derleme anında bu metoda geçilen argümanların türüne göre belirlenir, ancak derleyici yeterli bilgiye sahip değilse bu mümkün olmayabilir. args dizisinin referansı geri döndürüldüğü için tür güvenliği tehlikeye atılmaktadır.

Bunu örneklemek için aşağıdaki üreysel metodu ele alalım. Bu metot T türünden 3 tane argüman alıp, bunların rastgele seçtiği iki tanesini bir dizi içerisinde geri döndürüyor.

static <T> T[] pickTwo(T a, T b, T c) {
    switch(ThreadLocalRandom.current().nextInt(3)) {
        case 0: return toArray(a, b);
        case 1: return toArray(a, c);
        case 2: return toArray(b, c);
    }
    throw new AssertionError(); // ulaşılamaz satır
}

Bu metodun kendisinde herhangi bir problem yok gibi görünüyor ancak yukarıda tehlikeli olduğunu belirttiğimiz toArray metodunu çağırmaktadır. toArray metodu varargs parametreli olduğu için, pickTwo metodu bunu çağırırken geçtiği argümanları otomatik olarak bir dizi oluşturup onun içinde saklayacak, bu dizinin referansı toArray metoduna geçilecektir. T türü derleme anında belli olmadığı için argümanlar için oluşturulan dizi Object[] türünde olacaktır. toArray bu dizinin referansını pickTwo metoduna, o da istemciye döndürecektir. Yani pickTwo metodunu ilk başta çağıran istemci ne geçerse geçsin Object[] türünde bir sonuç alacaktır.

Aşağıdaki gibi bir istemci olduğunu düşünelim:

public static void main(String[] args) {
    String[] attributes = pickTwo("Good", "Fast", "Cheap");
}

Bu istemci kodu da kusursuz görünüyor ama çalıştırdığınızda görünür bir tür dönüşümü olmadığı halde ClassCastException alırsınız. Bizim görmediğimiz ama derleyicinin pickTwo tarafından döndürülen Object[] sonucunu String[] türüne dönüştürmek için eklediği kod patlamaktadır. Bunun da sebebi açıktır çünkü Object dizisi String dizisinin alt türü değildir. Bu çok endişe verici bir durumdur çünkü yığın kirliliği (heap pollution) yaparak bozukluğa sebep olan toArray metodu iki seviye aşağıdadır ve varargs parametre dizisi yaratıldıktan sonra hiçbir yerde değişiklik yapılmamaktadır.

Bu örneğin anlatmak istediği şey şudur: üreysel varargs parametre dizilerinin referansı başka bir metodun erişimine açılmamalıdır. Bunun iki istisnası vardır. Bu dizinin referansını @SafeVarargs notasyonu ile işaretlenmiş başka bir varargs metoda veya varargs olmayan ve bu dizinin elemanlarını sadece bir hesaplama yapmak için kullanan bir metoda geçmekte bir sakınca yoktur.

Şimdi de üreysel varargs parametresinin güvenli kullanımına bir örnek verelim. Bu örnekte flatten metodumuz değişken sayıdaki listeyi parametre almakta, bunların elemanlarının hepsini tek bir listede toplayıp geri döndürmektedir. Metot @SafeVarargs notasyonunu kullandığı için derleyici uyarılarına sebep olmaz.

// üreysel varargs parametre kullanan güvenli metot
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
    List<T> result = new ArrayList<>();
    for (List<? extends T> list : lists) {
        result.addAll(list);
    }
    return result;
}

@SafeVarargs notasyonunu üreysel varargs parametre kullanan bütün metotlarda kullanmalısınız. Bu aynı zamanda şu anlama da gelmektedir: @SafeVarargs notasyonunu kullanamayacağınız dangerous ve toArray gibi güvenli olmayan varargs metotlar asla yazmamalısınız. Derleyici size yığın kirliliği (heap pollution) uyarısı verdiği her durumda, metodun güvenli olup olmadığını kontrol edin. Hatırlayalım, üreysel varargs metotların güvenli olması aşağıdaki iki koşula bağlıdır:

  1. varargs parametre dizisine hiçbir değer yazmamak
  2. diziyi kontrolümüzde olmayan başka kodların erişimine açmamak

Eğer bu iki koşuldan biri sağlanmıyorsa bunu düzeltmeniz gerekir.

Şunu da belirtelim ki @SafeVarargs notasyonu sadece geçersiz kılınamayan metotlar üzerinde kullanılabilir çünkü geçersiz kılan bütün metotların güvenli olacağını garanti edemeyiz. Java 8’de bu notasyon sadece static metotlar ve final nesne metotları için kullanılabiliyordu. Java 9’da artık private nesne metotlarında da kullanılabilmektedir.

SafeVarargs notasyonu kullanmak yerine Madde 28’deki öneriyi dikkate alarak varargs parametreyi (yani diziyi) List ile değiştirebiliriz. Yukarıdaki flatten metodunu bu yaklaşımla yazarsak aşağıdaki gibi olacaktır. Dikkat ederseniz sadece metodun parametresi değişti:

// üreysel vararg parametresi yerine List kullanımı
static <T> List<T> flatten(List<List<? extends T>> lists) {
    List<T> result = new ArrayList<>();
    for (List<? extends T> list : lists) {
        result.addAll(list);
    }
    return result;
}

Bu metodu çağırmak için değişken sayıda argüman alıp bunlardan oluşturduğu listeyi döndüren List.of statik fabrika metodunu kullanabiliriz. List.of kendisi @SafeVarargs notasyonuyla tür güvenliği sağladığını kanıtlamaktadır bu yüzden kullanılması uygundur.

audience = flatten(List.of(friends, romans, countrymen));

Bu yöntem kullanıldığında derleyici tür güvenliği olduğunu bize garanti edebilir. Bunu bizim @SafeVarargs kullanarak derleyiciye söylememize gerek yoktur. Tek dezavantajı ise istemci kodunun biraz daha karmaşık ve yavaş olmasıdır.

Güvenli bir varargs metot yazamadığımız durumlarda bu yöntemi kullanmak işimize yarayacaktır. Bizim yukarıda yazdığımız toArray metodu buna bir örnektir. Bunu kullanmak yerine Java kütüphanelerinin bize sağladığı List.of ile bunu çözebiliriz. Bu durumda pickTwo metodu aşağıdaki gibi olacaktır:

static <T> List<T> pickTwo(T a, T b, T c) { 
    switch(ThreadLocalRandom.current().nextInt(3)) {
        case 0: return List.of(a, b); 
        case 1: return List.of(a, c); 
        case 2: return List.of(b, c);
    }
    throw new AssertionError();
}

İstemci kodu da aşağıdaki gibi olacaktır:

public static void main(String[] args) {
    List<String> attributes = pickTwo("Good", "Fast", "Cheap");
}

Artık programımızda tür güvenliği vardır çünkü dizi olmadan sadece üreyselleri kullandık.

Özetle, varargs ve üreyseller birlikte düzgün çalışmazlar çünkü varargs arka tarafta bir dizi kullanmakta ve bu diziyi dışarı sızdırmaktadır. Üreysellerle beraber varargs kullanmak tür güvenliğini tehdit etse de geçerli bir kullanımdır. Böyle bir metot yazmak isterseniz, metodun tür güvenliği olduğundan emin olun ve @SafeVarargs ile bunu istemcilere ve derleyiciye bildirin.

Share

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