Ü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.
[…] gerçekleşmektedir. Bu kullanım sınırlandırılmış tür belirtecine bir örnektir. (Madde 33) Metottaki opEnumType parametresinin <T extends Enum<T> & Operation> Class<T> […]
[…] olarak kullanabilirler. Bu kullanım sınırlandırılmış tür belirtecine bir örnektir. (Madde 33) Şimdi bu notasyon pratikte nasıl kullanılabilir ona […]