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:
- varargs parametre dizisine hiçbir değer yazmamak
- 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.