Effective Java Madde 26: Ham (raw) Türleri Kullanmayın

Tanımında bir veya birden fazla tür parametresi bulunduran sınıf veya arayüzlere üreysel (generic) sınıf veya arayüz denir. Örneğin, List arayüzü E isminde, listedeki elemanların türünü temsil eden bir tür parametresi bulundurur. Burada üreysel arayüzün tam adı List<E> olmaktadır. Üreysel sınıflar ve arayüzler toplu olarak üreysel türler olarak anılırlar.

Her üreysel tür, sınıf veya arayüz adından sonra < ve > karakterleri arasında tür parametreleri kullanarak bir takım parametreli türler (parameterized type) tanımlar. Örneğin, List<String> elemanları String türünden olan bir parametreli türü ifade eder. (JLS 4.4, JLS 4.5) Burada String yukarıda E ile temsil edilen tür parametresinin yerini doldurmaktadır.

Üreysel türler tür parametreleri kullanılmadığında ham tür (raw type) olurlar. Örneğin, List<E> yerine tür parametresi olmadan List tek başına bir ham türü temsil eder. Ham türler, üreysel türün tanımından bütün parametreler çıkartılmış gibi davranırlar. Bunların hala var olmasının ana sebebi, üreysel türler dile eklenmeden önce yazılmış kodlarla olan uyumluluğu bozmamaktır.

Java 5’le dile üreysel türler eklenmeden önce, bir Collection tanımlamak için aşağıdaki kod örnek olarak verilebilirdi. Java 9 itibariyle bu hala geçerli bir tanımdır ancak asla önerilmez:

// Ham Collection türü - kullanmayın!

// Pul koleksiyonum. Sadece pul (stamp) nesneleri içerir
private final Collection stamps = ... ;

Bu şekilde tanımlanmış bir Collection nesnesine pul yerine yanlışlıkla madeni para (Coin) eklemeye çalışırsanız, derleyici hatasız bir biçimde çalışacaktır ancak bir uyarı mesajı verecektir:

// pul koleksiyonuna hatalı olarak madeni para ekliyoruz
stamps.add(new Coin( ... )); // "unchecked call" uyarısı alırız

Burada yaptığınız hatayı ancak çalışma zamanında, Collection içerisindeki nesneleri Stamp olarak kullanmaya çalıştığınızda alırsınız:

// Ham iterator türü, kullanmayın!
for (Iterator i = stamps.iterator(); i.hasNext(); ) {
    Stamp stamp = (Stamp) i.next(); // ClassCastException fırlatılır
    stamp.cancel();
}

Yukarıdaki iterator, Collection nesneleri içinde tarama yaparken daha önceden yanlışlıkla eklenmiş olan Coin nesnesine denk geldiğinde bunu Stamp nesnesine çeviremeyeceği için (cast), çalışma zamanında ClassCastException fırlatacaktır.

Bu kitabın birçok bölümünde anlatıldığı üzere, yapılan hataları mümkün olan en kısa zamanda yakalamak (örneğin derleme anında) çok büyük avantajlar sağlar. Yukarıdaki örnekte bu mümkün olmamıştır, üstelik kodda hatanın yapıldığı yer ile hatanın yakalandığı yer farklıdır. Bu hatayı gören bir programcı bütün kaynak kodu tarayıp bu Collection nesnesine ekleme yapan kısımları kontrol etmek zorunda kalacaktır. Derleyici burada size yardım edemez çünkü tür bilgisine sahip değil. Aşağıdaki şekilde tür parametresi olarak Stamp belirtmiş olsaydık durum farklı olacaktı:

// Parametreli Collection türü.. Tür güvenliği var!
private final Collection<Stamp> stamps = ... ;

Bu tanımdan ötürü, derleyici artık bu Collection içerisinde sadece Stamp nesnelerinin olması gerektiğini bilmektedir. stamps parametreli bir tür olarak tanımlandığı için hatalı bir biçimde Coin nesnesi eklemeye çalışırsak derleme anında aşağıdaki gibi bir hata ile karşılaşırız. Görüldüğü gibi hata mesajı tam olarak neyin nerede yanlış olduğunu bildirmektedir:

Test.java:9: error: incompatible types: Coin cannot be converted to Stamp
       c.add(new Coin());
                 ^

Derleyici, koleksiyonlardan eleman okunan durumlar için bizim göremediğimiz bir tür dönüşüm (cast) kodu ekler ve bunun hata vermeyeceğini kodunuzun derleyici hatası üretmediği veya bu hataları gizlemediği (suppress) durumlar için (Madde 27) garanti eder. Bizim kullandığımız örnekte, Stamp nesnesi yerine Coin eklemek yapılması zor bir hata gibi görünse de, bu gerçek bir problemdir. Örneğin, BigDecimal nesneleri tutan bir koleksiyona yanlışlıkla BigInteger eklemek görece kolay yapılabilecek bir hatadır.

Daha önce belirtildiği gibi, ham türler dilde hala geçerli olsa da aşağıda anlatılan bir iki istisnai durum haricinde kullanmamalısınız. Ham türleri kullandığınız zaman, üreysel türlerin sağladığı tür güvenliği ve kolay okunabilirlik avantajlarından faydalanamazsınız. Peki madem bunları kullanmamız yanlış, neden dilde buna izin veriliyor? Bunun sebebi geriye uyumluluğu (backwards compatibility) bozmamak içindir. Üreysel türler dile eklendiğinde Java neredeyse 10 yaşındaydı. O zamana kadar üreysel türler olmadan yazılan tonla kodun, yeni versiyonlarla uyumlu çalışması zorunluluğu vardı. Ham tür bekleyen metotlara parametreli, parametreli tür bekleyen metotlara da ham tür geçebilmek mümkün olmalıydı. Bu gereklilik yüzünden, ham türlerin dilde desteklenmeye devam edilmesi ve üreysel türlerin erasure (Madde 28) mekanizması kullanılarak gerçekleştirilmesine karar verilmiştir.

List gibi ham türleri kullanmak önerilmese de, List<Object> ile istediğiniz nesneyi ekleyebileceğiniz parametreli bir tür tanımlamakta bir sakınca yoktur. Peki ikisine de istediğimiz nesneleri ekleyebiliyorsak List ve List<Object> arasındaki fark nedir? Kısaca şunu diyebiliriz: ilk kullanımda üreysel tür sistemini devre dışı bırakırken, ikincide derleyiciye farklı türlerden nesneleri karışık biçimde tutabilecek bir liste tanımladığımızı bildirmiş oluyoruz. Parametre olarak List bekleyen bir metoda List<String> geçebilirsiniz ama List<Object> bekleyen bir metoda bunu geçemezsiniz. Bunun sebebi üreysel türler için geçerli alt tür kurallarıdır. List<String>, List‘in bir alt türü olmasına rağmen List<Object>‘in bir alt türü değildir, bu sebeple List<Object> bekleyen metoda List<String> geçilemez. (Madde 28) Bu da demek oluyor ham tür olarak List kullanırsak tür güvenliğini kaybetmiş oluyoruz. Şimdi bunu örnekleyelim:

// Çalışma zamanında çöker - unsafeAdd metodu ham tür kullanıyor
public static void main(String[] args) {
    List<String> strings = new ArrayList<>(); 
    unsafeAdd(strings, Integer.valueOf(42));
    String s = strings.get(0); // Derleyici buraya tür dönüşümü ekler
}

private static void unsafeAdd(List list, Object o) { 
    list.add(o);
}

Bu program hatasız derlenir ama ham tür olan List kullandığı için bir uyarı verir:

Test.java:10: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
             list.add(o);
                     ^

Bu programı çalıştırdığınızda ClassCastException hatası alırsınız. Çünkü strings.get(0) işletilince sonuç olarak 42 değerine sahip bir Integer nesnesi elimize geçecek ancak bu nesne derleyicinin eklediği tür dönüşüm komutu yüzünden String türüne dönüştürülmeye çalışılacaktır. Normalde derleyici tarafından garanti edilmesi gereken bu tür dönüşüm işlemi başarısız olmaktadır çünkü biz derleyicinin en başta verdiği uyarıyı dikkate almadık ve bunun bedelini ödemiş olduk.

Burada unsafeAdd metodundaki List parametresini List<Object> ile değiştirirsek, derleyici artık uyarı yerine hata verecek ve program derlenmeyecektir.

Test.java:5: error: incompatible types: List<String> cannot be
         converted to List<Object>
             unsafeAdd(strings, Integer.valueOf(42));
                 ^

Tür parametresinin belli olmadığı veya varsa bile ne olduğunun önemli olmadığı durumlarda yine ham tür kullanmak aklınıza gelebilir. Örneğin, iki tane Set referansını alıp bunların kaç tane ortak elemanı olduğunu döndüren bir metodu aşağıdaki gibi yazmak isteyebilirsiz:

// Eleman türü bilinmediğinde ham tür kullanmayın!
static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1 : s1) {
        if (s2.contains(o1)) {
            result++;
        }
    }
    return result;
}

Bu metot çalışacaktır ama ham tür kullandığı için tehlikelidir. Bunun daha güvenli yolu sınırlandırılmamış joker tür (unbounded wildcard type) kullanmaktır. Üreysel bir tür kullanmak istiyorsanız ancak tür parametresini bilmiyorsanız veya ne olduğuyla ilgilenmiyorsanız, tür parametresi yerine ? koyabilirsiniz. Örneğin, Set<E> üreysel türü için sınırlandırılmamış joker türü Set<?> olarak tanımlanır. Set için tanımlanabilecek en genel parametreli türdür. Herhangi bir parametreli Set türü tutabilir ancak bizim için neyi tuttuğunun bir önemi yoktur. Şimdi joker türü ile numElementsInCommon metodunun imzası nasıl olur ona bakalım:

// sınırlandırılmamış joker türü kullanımı
// tür güvenliği ve esneklik sağlar
static int numElementsInCommon(Set<?> s1, Set<?> s2) { 
    ... 
}

Peki sınırlandırılmamış joker tür Set<?> ile ham tür Set arasındaki fark nedir? Kullanılan bir ? karakteri neyi değiştiriyor? Daha önce de belirtildiği gibi, joker türü kullanıldığında tür güvenliği sağlamış oluyoruz. Set nesnesine istediğiniz türde bir eleman ekleyebilirken, Set<?> nesnesine null haricinde hiçbir eleman eklemenize izin verilmez. Denerseniz aşağıdaki gibi bir derleme hatası alırsınız:

WildCard.java:13: error: incompatible types: String cannot be
   converted to CAP#1
       c.add("verboten");
             ^
     where CAP#1 is a fresh type-variable:
       CAP#1 extends Object from capture of ?

Burada derleyici görevini yapmış, içinde hangi türden nesneler olduğunu bilmediğimiz Set<?> içerisine ekleme yapmanıza izin vermeyerek tür güvenliğini korumuştur. Derleyici bunun yanında, bu Set<?> içerisinden okuduğunuz nesnelerin türüyle ilgili bir varsayımda bulunmanıza da izin vermez. Eğer bu kısıtlamalar sizin için kabul edilemezse üreysel metotları (generic methods) veya sınırlandırılmış joker türlerini (bounded wildcard type) kullanabilirsiniz. (Madde 30, Madde 31)

Ham türlerin kullanılması konusunda bir iki tane ufak istisnai durum vardır. Java’da class literal denilen yapıları kullanırken ham türler kullanmak zorundayız çünkü Java burada parametreli türlere izin vermez. (JLS 15.8.2) Örneğin, List.class, String[].class ve int.class geçerli olmasına rağmen List<String>.class ve List<?>.class geçersiz ifadelerdir.

İkinci bir istisna da instanceof işlecini (operator) kullanırken söz konusudur. Üreysel türlerde tür bilgisi çalışma zamanında silindiği için (type erasure), instanceof işlecini sınırlandırılmamış joker tür haricindeki parametreli türlerle kullanamayız. Burada ham tür yerine kullanılabilecek sınırlandırılmamış joker tür instanceof işlecinin davranışını etkilememektedir, bu sebeple burada ham tür kullanılmasında bir sakınca yoktur. Aşağıda, instanceof işlecinin üreysel türlerle nasıl kullanılması gerektiğini görüyorsunuz:

// Ham türlerin instanceof ile geçerli kullanımı
if (o instanceof Set) {      // Ham tür
    Set<?> s = (Set<?>) o;   // Joker tür
    ... 
}

Burada o referansının Set türünde olduğunu öğrendikten sonra, ham tür olan Set‘e değil joker türü Set<?>‘e dönüşüm uyguluyoruz. Bu kontrollü bir dönüşüm olduğu için derleyicinin uyarı vermesine sebep olmaz.

Özetle, ham türler kullanmak çalışma zamanında aykırı durumlara (exception) sebebiyet verebilir, o yüzden kullanmayın. Var olmalarının tek sebebi üreysel türler dile eklenmeden önce yazılmış eski kodlarla uyumluluğu korumaktır. Hızlı biçimde gözden geçirecek olursak, Set<Object> içerisinde bütün türlerden nesneleri tutabilecek bir küme, Set<?> ise içinde hangi türden nesneler olduğunu bilmediğimiz bir küme ifade eder. Tek başına tanımlanan Set ise ham tür olarak üreysel tür sisteminin tamamen dışında kalmıştır. Bunların ilk ikisi güvenli, sonuncusu ise güvensizdir.

Hem referans olması açısından hem de bu madde tanıştığımız bazı terimleri özetlemesi açısından aşağıdaki tabloya bir göz atmakta fayda var.

TerimÖrnekMadde
Parametreli TürList<String>Madde 26
Tür parametesi
(parametreli tür için)
StringMadde 26
Üreysel TürList<E>Madde 26, 29
Tür parametresi
(üreysel tür için)
EMadde 26
Sınırlandırılmamış joker türüList<?>Madde 26
Ham türListMadde 26
Sınırlandırılmış tür parametresi<E extends Number>Madde 29
Özyineli tür sınırı<T extends Comparable<T>>Madde 30
Sınıflandırılmış joker türü List<? extends Number> Madde 31
Üreysel Metot static <E> List<E> asList(E[] a) Madde 30
Tür belirteciString.classMadde 33

Share

3 Replies to “Effective Java Madde 26: Ham (raw) Türleri Kullanmayın”

Bir Cevap Yazın