Effective Java Madde 33: Tür Güvenlikli Heterojen Taşıyıcıları Göz Önünde Bulundurun

Üreysel türlerin yaygın olarak bilinenleri Set<E>, Map<K,V> gibi koleksiyonlar ve ThreadLocal<T> ve AtomicReference<T> gibi tek elemanlı taşıyıcılardır. Bütün bu kullanımlarda taşıyıcı olan türün kendisi parametrelerle ifade edilmektedir. Bu bizi her taşıyıcı için sabit bir sayıda tür parametresi kullanmaya zorlamaktadır. Örneğin Set eleman türünü belirleyen tek bir tür parametresine sahiptir, Map ise içindeki girdilerin anahtar ve değerlerini (key-value) tutmak üzere iki tane tür parametresine sahiptir.

Ancak bazen bundan daha fazla esneklik isteyebilirsiniz. Mesela bir veritabanı kaydı çok sayıda sütunda farklı değerlere sahip olabilir ve biz bunlara tür güvenliğini de koruyarak erişmek isteyebiliriz. Bunu yapabilmek için taşıyıcı türü parametrelerle ifade etmek yerine (parameterize) taşıyıcının içinde tür anahtarı kullanabiliriz. Bu tür anahtarı taşıyıcıya farklı türlerde değer yazmak ve okumak için kullanılacaktır.

Bu yaklaşıma örnek olarak Favorites isimli bir sınıfımız olsun. Bu sınıf istemcilerine farklı türlerde favori nesneleri kaydedip daha sonra bunları okuma imkanı vermektedir. (örneğin Integer türünde ve değeri 42 olan bir favori nesnesi, String türünde ve değeri "Java" olan bir favori nesnesi gibi) Bu sınıfın API’ı aşağıdaki gibi olacaktır (gerçekleştirimi yazının devamında göreceğiz):

// Tür güvenlikli heterojen taşıyıcı deseni - API
public class Favorites {
    public <T> void putFavorite(Class<T> type, T instance);
    public <T> T getFavorite(Class<T> type);
}

Burada Class nesnesi anahtar tür görevini görmektedir ve üreysel olduğu için farklı türleri temsil etme yeteneğine sahiptir.

Şimdi de bu Favorites sınıfını kullanarak Integer, String ve Class türlerinde favori nesneleri kaydedip sonra da okuyan bir istemci yazalım:

// Tür güvenlikli heterojen taşıyıcı deseni - istemci
public static void main(String[] args) {
    Favorites f = new Favorites();
    f.putFavorite(String.class, "Java");
    f.putFavorite(Integer.class, 0xcafebabe);
    f.putFavorite(Class.class, Favorites.class);

    String favoriteString = f.getFavorite(String.class);
    int favoriteInteger = f.getFavorite(Integer.class);
    Class<?> favoriteClass = f.getFavorite(Class.class);
    System.out.printf("%s %x %s%n", favoriteString,
             favoriteInteger, favoriteClass.getName());
}

Bu istemci tahmin edeceğiniz üzere Java cafebabe Favorites yazdıracaktır. Favorites nesneleri tür güvenliği sağlamaktadır. Bir String istediğiniz zaman size asla bir Integer döndürmeyecektir. Aynı zamanda heterojendir çünkü bir Map nesnesinin aksine bütün anahtarlar farklı türdedir. Bu sebeple Favorites sınıfını tür güvenlikli heterojen taşıyıcı (typesafe heterogeneous container) olarak adlandırırız.

Favorites sınıfının gerçekleştirimi sürpriz bir biçimde çok kısadır ama üzerinde konuşulması gerekir:

// Tür güvenlikli heterojen taşıyıcı deseni - gerçekleştirim
public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();
    
    public <T> void putFavorite(Class<T> type, T instance) {
        favorites.put(Objects.requireNonNull(type), instance);
    }

    public <T> T getFavorite(Class<T> type) { 
        return type.cast(favorites.get(type));
    } 
}

Gördüğünüz gibi her Favorites nesnesi arkada favorites isminde bir private Map<Class<?>, Object> nesnesi tutmaktadır. Burada Map referansını tanımlarken kullanılan sınırlandırılmamış joker türü (Class<?>), Map‘in anahtarlarının farklı parametreli türler olabileceğini göstermektedir. Örneğin bu Map nesnesinin bir girdisinin (entry) anahtarı Class<String> başka bir girdisinini anahtarı Class<Integer> olabilir. Favorites nesnelerinin heterojen olmalarını sağlayan şey budur.

Dikkat çeken başka bir unsur da kullanılan Map nesnesinin değer türünün Object olmasıdır. Başka bir deyişle, bu Map nesnesi bize anahtar ile değer arasındaki tür uyumunu sağlamıyor. Örneğin Class<Integer> anahtarı kullandığımız bir girdinin değer türü Integer değil Object olacaktır. Java’nın tür sistemi bunu doğru biçimde ifade edebilecek kadar güçlü değildir ancak biz anahtar ile değer türü arasında bir uyum olduğunu biliyoruz ve istemci okuma yaparken bunun avantajını kullanacağız.

Favori nesneleri eklemek için kullanılan putFavorite metodunu anlamak aslında kolaydır. Tek yaptığı iş verilen Class nesnesi ve T türündeki favori nesnesi arasında bir eşleşmeyi temsil etmek üzere Map içerisine bir girdi eklemektir. Girdinin anahtarını temsil eden type parametresi Class<T>, değeri temsil eden instance parametresi de T türünde olduğu için putFavorite metodu anahtar ile değer arasındaki tür uyumunu korumaktadır. Başka bir deyişle istemci putFavorite metodunu çağırırken type yerine Class<Integer> geçiyorsa instance yerine de bir Integer nesnesi geçmek zorundadır.

Her ne kadar putFavorite metodunda tür uyumu olduğunu bilsek de Map içerisine ekleme yapılırken girdilerin değer türü Object olduğu için anahtar ve değer arasında tür uyumu olduğu bilgisi kaybolmaktadır. Ancak bu bizim için bir problem teşkil etmez çünkü getFavorites ile okuma yapılırken bu bağlantı yeniden kurulmaktadır.

getFavorite metodunun gerçekleştirimi putFavorite‘dan daha kafa karıştırıcıdır. Birincisi, bu metot parametre olarak aldığı Class nesnesine denk gelen girdiyi bulup değerini okumaktadır ancak bu değer Object türündedir. Metodun T türünde bir değer döndürmesi beklenmektedir. Bu yüzden metot Object türünde gelen nesneyi Class sınıfının cast metoduyla dinamik olarak T türüne dönüştürmektedir. Şimdi cast metodunun imzasına bir göz atalım:

public class Class<T> {
    T cast(Object obj);
}

Görüldüğü gibi cast metodu Class sınıfının üreysel olmasından faydalanmaktadır. Object türündeki obj parametresini sınıfın tür parametresi olan T türüne dönüştürmeye çalışmaktadır. Tür dönüşümü başarılı olursa bunu döndürmektedir, başarısız olursa da ClassCastException fırlatmaktadır. Ancak biz biliyoruz ki buradaki tür dönüşümü güvenlidir çünkü girdileri yazarken kullandığımız putFavorite bunu sağlamaktadır.

Favorites sınıfı için bahsedebileceğimiz iki tane kısıtlama mevcuttur. Birincisi kötü niyetli bir istemci bir Favorites nesnesinin tür güvenliğini ham(raw) bir Class türü kullanarak bozabilir. Bu durumda istemci kodu derlenirken derleyici bir uyarı verecektir. Bu aslında HashSet ve HashMap gibi gerçekleştirimlerin maruz kaldığı problemle aynıdır. Bir HashSet<Integer> nesnesine String eklemek referans olarak ham HashSet türü kullanıldığında mümkündür. (Madde 26) Bununla beraber, tür güvenliğinin bozulup bozulmadığını çalışma zamanında kontrol etmek de mümkündür. Bunun için putFavorite metodunda instance nesnesinin gerçekten de type türünde olup olmadığını kontrol edebiliriz. Bunu da dinamik bir şekilde yapabileceğimizi zaten öğrendik:

// dinamik tür dönüşümü ile çalışma zamanında tür güvenliği sağlamak
public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(type, type.cast(instance));
}

java.util.Collections içerisindeki checkedSet, checkedList, checkedMap gibi üreysel metotlar da bu yöntemi kullanarak çalışma zamanında tür uyuşmazlığına izin vermeyen koleksiyonlar üretirler. Parametre olarak bir veya iki tane Class nesnesi alırlar ve koleksiyona eklenen elemanların bu tür veya türlerle uyumlu olmasını şart koşarlar. Örneğin bir Collection<Stamp> nesnesine Coin eklemeye çalışırsanız ClassCastException fırlatırlar. Bunun bize faydası hata problemli kod parçası işletildiğinde üretildiği için çalışma zamanında gerçekleşecek bu hatanın kaynağını bulmak çok kolay olacaktır. Aksi taktirde tür uyumunu bozan eleman koleksiyona yazılırken değil okunurken hata ile karşılaşırdık ve bunu da bulmak çok zor olurdu.

Favorites sınıfının kısıtlı olduğu bir başka nokta da anahtar tür olarak parametreli türleri kullanamıyor olmamızdır. Mesela String veya String[] türleri için birer favori nesnesi oluşturabilirsiniz ama List<String> için oluşturamazsınız. Bunun sebebi de List<String> için özel bir Class nesnesinin olmayışıdır. Örneğin List<String> ve List<Integer> parametreli türleri için ortak bir tane Class nesnesi vardır o da List.class‘dır. List<String>.class gibi bir kullanım Java’da geçersizdir ve derleyici hata üretecektir, ki bu çok iyi bir şeydir. List<String>.class ve List<Integer>.class ifadelerini gören derleyici hata üretmeyip her ikisi için de aynı List.class nesnesini döndürseydi Favorites sınıfı için çok büyük problem yaratırdı. Dolayısıyla bu kısıtlamadan kurtulmanın tatmin edici bir çözümü yoktur.

getFavorite ve putFavorite metotları Class türündeki type parametresine herhangi bir kısıt koymamıştır. İstemci buraya istediği Class nesnesini geçebilir. Bu noktada istemcilerin geçebileceği Class nesnelerine bir sınır koymak isterseniz sınırlandırılmış tür belirteci (bounded type token) kullanabilirsiniz. Bu aslında tür belirtecinin (String.class tür belirtecine bir örnektir) hangi türleri temsil edebileceğini sınırlandırılmış tür parametresi (Madde 30) veya sınırlandırılmış joker (Madde 31) kullanılarak kısıtlamaktan ibarettir.

Madde 39’da bahsedilen notasyon API (annotations API) sınırlandırılmış tür belirteçlerini sıklıkla kullanmaktadır. Aşağıda notasyon okuyan bir metodu görüyorsunuz. Bu metot AnnotatedElement arayüzünden gelmektedir ve Method, Class gibi program elemanlarını temsil eden türler tarafından gerçekleştirilmektedir.

public <T extends Annotation>
             T getAnnotation(Class<T> annotationType);

annotationType parametresi notasyon türünü temsil eden bir sınırlandırılmış tür belirtecidir. Bu metot üyesi olduğu program elemanı (Method sınıfı gibi) annotationType türünde bir notasyona sahipse onu döndürmektedir, sahip değilse de null döndürmektedir. Başka bir ifadeyle bir AnnotatedElement anahtar tür olarak notasyon türlerini kullanan tür güvenlikli heterojen bir taşıyıcıdır.

Elimizde Class<?> türünden bir nesne olduğunu düşünelim ve bu nesneyi getAnnotation gibi sınırlandırılmış tür belirteci bekleyen bir metoda geçmemiz bekleniyor olsun. Bu durumda nesneyi Class<? extends Annotation> türüne çevirebilirsiniz ancak bu kontrolsüz bir dönüşüm olduğu için derleyici uyarı verecektir. (Madde 27) Bunun yerine Class sınıfının bize sağladığı asSubclass nesne metodunu kullanarak daha güvenli ve dinamik bir biçimde tür dönüşümü yapabiliriz. Bu metot, üzerinden çağrıldığı Class nesnesini argümanında belirtilen türe dönüştürmeye çalışır. Başarılı olursa argümanını geri döndürür, olamazsa da ClassCastException fırlatır. Aşağıda, derleme zamanında türü bilinmeyen bir notasyonu okumak için asSubclass metodunun nasıl kullanılacağını görüyoruz. Bu metot hata ve uyarı vermeden derlenir:

// sınırlandırılmış tür belirtecine güvenli dönüşüm için 
// asSubclass metodunun kullanımı 
static Annotation getAnnotation(AnnotatedElement element,
                                   String annotationTypeName) {

    Class<?> annotationType = null; // sınırlandırılmış tür belirteci 
    try {
        annotationType = Class.forName(annotationTypeName);
    } catch (Exception ex) {
        throw new IllegalArgumentException(ex);
    }
    return element.getAnnotation(
             annotationType.asSubclass(Annotation.class));
    }

Özetle, üreysel türlerin Collections arayüzündeki gibi kullanılması bizi belirli bir sayıda tür parametresi kullanmak zorunda bırakır. Bu kısıtlamayı aşmak için taşıyıcı sınıfı parametrelerle ifade etmek yerine tür anahtarı kullanabiliriz. Tür anahtarı olarak Class nesnelerini kullanmak suretiyle, tür güvenlikli heterojen taşıyıcılar yazabilirsiniz. Bu niyetle kullanılan Class nesneleri tür belirteci olarak anılır. Ayrıca, kendi isteğinize göre tür anahtarları da kullanabilirsiniz. Örneğin veritabanı kayıtlarını temsil eden bir DatabaseRow sınıfında (taşıyıcı), tür anahtarı olarak Column<T> kullanabilirsiniz.

Share

Effective Java Madde 27: Kontrolsüz Uyarılardan (unchecked warning) Kurtulun

Yazıya başlamadan önce Java derleyicisinin verdiği kontrolsüz uyarı (unchecked warning) ne anlama geliyor ondan bahsedelim. Kısaca, derleyici burada bize tür güvenliğini (type safety) garanti edemediğini söylemektedir. Burada ”kontrolsüz” ifadesi biraz yanıltıcıdır çünkü uyarının kontrolsüz olması söz konusu değildir. İfade etmek istediği şey, derleyicinin tür güvenliğini sağlamak için yeterli tür bilgisine sahip olmadığıdır. Bu yüzden de bazı işlemler ”kontrolsüz” gerçekleşecektir.

Üreysel türlerle programlama yaptığınız zaman çeşit çeşit derleyici uyarıları görürsünüz: kontrolsüz tür dönüşümü uyarısı (unchecked cast warning) ve kontrolsüz metot çağrısı uyarısı (unchecked method invocation warning) bunlardan sadece bazılarıdır. Üreysel türlerle olan tecrübeniz arttıkça bu uyarılar azalacaktır.

Bu uyarıların çoğundan kurtulmak kolaydır. Örneğin, yanlışlıkla aşağıdaki gibi bir tanım yaptığınızı düşünün:

Set<Lark> exaltation = new HashSet();

Burada derleyici nazikçe size nerede yanlış yaptığınızı hatırlatacaktır:

Venery.java:4: warning: [unchecked] unchecked conversion
           Set<Lark> exaltation = new HashSet();
                                  ^
     required: Set<Lark>
     found:    HashSet

Bunu görünce, bildirilen hatayı düzeltip uyarıdan kurtulabilirsiniz. Burada eksik olan tür parametresi belirtmek için kullanılan baklava işlecidir (diamond operator). Dikkat ederseniz exaltation zaten parametreli bir tür olarak tanımlandığı için nesne yaratırken tekrar tür parametresi geçmenize gerek yoktur, işlecin kendisini kullanmanız yeterlidir. Java 7’den sonra buradaki tür parametresi otomatik olarak algılanır (bu örnekte Lark).

Set<Lark> exaltation = new HashSet<>();

Bazı uyarıları gidermek ise oldukça zor olabilir. İleriki maddelerde bunların örneklerini göreceğiz. Ancak, derleyiciden böyle bir uyarı aldığınız her durumu değerlendirin ve uyarıdan kurtulmak mümkünse bunu yapın! Eğer bütün uyarıları giderebilirseniz, çalışma zamanında tür güvenliği sağladığınızdan emin olabilirsiniz. Programınız ClassCastException üretmeyecektir ve kötü sürprizlerle karşılaşma olasılığı azalacaktır.

Eğer bir uyarıdan kurtulamıyorsanız ancak bu uyarıya sebep olan kodun tür güvenliğini tehlikeye atmadığından eminseniz o zaman @SuppressWarnings("unchecked") notasyonunu (annotation) kullanarak bu uyarıyı gizleyebilirsiniz. Ancak tür güvenliğinden emin olmadan bunu yaparsanız kendinizi kandırmış olursunuz. Kod uyarı vermeden derlenmiş olur ama çalışma zamanında hala ClassCastException fırlatabilir. Güvenli olduğunu bildiğiniz durumlarda kontrolsüz uyarıları gizlemek yerine sürekli görmezden gelirseniz de bu sefer gerçekten problem çıkaracak yeni bir uyarı verildiğinde bunu farketmezsiniz. O yüzden tür güvenliğini garanti edebildiğiniz durumlarda bu uyarıları gizlemek en doğrusudur.

SuppressWarnings notasyonu bir yerel değişkenden sınıflara kadar birçok yerde tanımlanabilir. Bu notasyonu mümkün olan en dar alanda kullanmak en doğrusudur. Bu da genelde bir değişken tanımlarken veya bir iki satırlık kısa metotlar/yapıcı metotlar için kullanılması demektir. Sınıflar için SuppressWarnings notasyonunu kullanmayın çünkü kritik uyarıları istemeden gizleyebilirsiniz.

Eğer SuppressWarnings notasyonunu uzun bir metot veya yapıcı metot için kullanıyorsanız, bunu yerel bir değişkene taşımanız mümkün olabilir. Bunun için yeni bir yerel değişken tanımlamanız gerekebilir ama bunu sorun etmeyin. Örneğin, ArrayList‘ten gelen toArray metoduna bakalım:

public <T> T[] toArray(T[] a) {
    if (a.length < size) {
        return (T[]) Arrays.copyOf(elements, size, a.getClass()); 
    }
    System.arraycopy(elements, 0, a, 0, size);
    if (a.length > size) {
        a[size] = null;
    } 
    return a;
}

Bu kodu derleyince aşağıdaki gibi bir uyarı alırsınız:

ArrayList.java:305: warning: [unchecked] unchecked cast
return (T[]) Arrays.copyOf(elements, size, a.getClass());
                           ^
     required: T[]
     found:    Object[]

Uyarı return satırından gelmektedir ancak buraya SuppressWarnings notasyonu koymak yasaktır. Bu sebeple notasyonu metoda eklemeyi düşünebilirsiniz ama böyle yapmayın. Bunun yerine bir yerel bir değişken tanımlayıp döndürmek istediğiniz değeri atayın ve bu değişkene notasyonu ekleyin:

// SuppressWarnings etki alanını daraltmak 
// için yerel değişken tanımladık
public <T> T[] toArray(T[] a) {
    if (a.length < size) {
        // Burada tür güvenliği vardır çünkü yarattığımız dizinin türü
        // parametre geçilen tür ile aynıdır: T[]
        @SuppressWarnings("unchecked") 
        T[] result=(T[]) Arrays.copyOf(elements, size, a.getClass());
        return result;
    }
    
    System.arraycopy(elements, 0, a, 0, size);
    if (a.length > size) {
        a[size] = null;
    }
    return a;
}

Bu metot tertemiz derlenecektir ve SuppressWarnings notasyonunun etki alanı sadece result yerel değişkeniyle kısıtlı bırakılmıştır.

@SuppressWarnings("unchecked") notasyonunu kullandığınız her durum için bir yorum ekleyip bunun neden güvenli olduğunu açıklayın. Bu şekilde hem başkalarının kodu anlamasına yardımcı olursunuz hem de ileride kodu değiştiren kişilerin tür güvenliğini bozmaması için bir kılavuz bırakmış olursunuz. Yorum yazarken zorlandığınız hissediyorsanız da iyi düşünün, belki de güvenli zannettiğiniz kod parçası güvenli değildir.

Özetle, kontrolsüz uyarılar (unchecked warnings) önemlidir, bunları görmezden gelmeyin. Bu uyarıların her biri potansiyel bir ClassCastException aykırı durumunu temsil eder. Bunlardan kurtulmak için elinizden geleni yapın. Kurtulamıyorsanız da, güvenli olduğundan emin olduğunuz yerlerde @SuppressWarnings("unchecked") notastonu ile bu uyarıları gizleyin. Bunu yaparken de notasyonun etki alanını minimize edecek şekilde yapın ve mutlaka yorum ekleyerek neden güvenli olduğunu açıklayın.

Share