Tıpkı sınıflar gibi metotlar da üreysel (generic) olarak yazılabilir. Parametreli türlerle çalışan statik yardımcı metotlar (utility method) genellikle üreyseldir. Örneğin, Collections
içerisindeki binarySearch
ve sort
gibi algoritma metotlarının hepsi üreyseldir.
Üreysel metot yazmak üreysel tür yazmakla çok benzerdir. (Madde 29) İki kümenin birleşimini döndüren aşağıdaki kusurlu metoda bakalım:
// Ham tür kullanmaktadır - kabul edilemez! (Madde 26)
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
Bu metot derlenir ama derleyici iki tane uyarı verir (Madde 27):
Union.java:5: warning: [unchecked] unchecked call to HashSet(Collection<? extends E>) as a member of raw type HashSet
Set result = new HashSet(s1);
^
Union.java:6: warning: [unchecked] unchecked call to
addAll(Collection<? extends E>) as a member of raw type Set
result.addAll(s2);
^
Bu hataları giderip tür güvenliğini sağlamak için, metodun imzasını değiştirerek bir tür parametresi tanımlayabiliriz. Bu tür parametresi hem parametre olan Set
nesneleri hem de geri döndürdüğümüz Set
için eleman türünü belirleyecektir. Böyle metotlarda tür parametreleri metodun dönüş türünün önünde tanımlanır:
// Üreysel metot
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
Basit üreysel metotlar söz konusu olduğunda aslında anlatılabilecekler bundan ibaret. Yukarıdaki metot hatasız ve uyarısız derlenir, tür güvenliği sağlar ve kullanımı kolaydır. Aşağıda bu metodu kullanan bir istemci görüyoruz:
// Üreysel metodu kullanan istemci
public static void main(String[] args) {
Set<String> guys = Set.of("Tom", "Dick", "Harry");
Set<String> stooges = Set.of("Larry", "Moe", "Curly");
Set<String> aflCio = union(guys, stooges);
System.out.println(aflCio);
}
Bu programı çalıştırdığınızda şöyle bir çıktı alırsınız:
[Moe, Tom, Harry, Larry, Curly, Dick]
Üreysel union
metodunda hem metot parametreleri hem de dönüş türü aynı türden olmak zorundadır. Bunu biraz esnekleştirmek için sınırlandırılmış joker tür (bounded wildcard type – Madde 31) kullanabilirsiniz.
Bazı durumlarda, değiştirilemeyen (immutable) ama birçok türle birlikte kullanılabilen nesneler yaratmak isteyebilirsiniz. Java’da üreysellik mekanizması çalışma zamanında tür bilgisini silecek şekilde tasarlandığı için (type erasure), birçok parametreli tür için tek bir nesneyi kullanabilirsiniz. Ancak bunun için bir de kullanacağınız parametreli türleri yaratacak static fabrika metodu (static factory method) yazmanız gerekir. Bu tasarım üreysel singleton fabrikası (generic singleton factory) olarak bilinir ve Collections.reverseOrder
gibi Madde 42’de anlatılan fonksiyon nesneleri, bazen de Collections.emtpySet
gibi koleksiyonlar için kullanılır.
Şimdi farzedelim ki birim fonksiyonu üreten bir kod yazmak istiyoruz. Java kütüphaneleri bunu zaten sağlamaktadır (Function.identity
) o yüzden yazmanıza gerek yok ama öğretici olduğu için biz burda yazacağız. Birim fonksiyonların tek yaptığı iş verilen parametreyi geri döndürmektir. Herhangi bir durum (state) tutmadığı için her ihtiyaç olduğunda yenisini üretmek israf olacaktır. Eğer Java’da üreysel türlerin ve metotların tür parametreleri çalışma zamanında kaybolmasaydı, her bir tür için farklı fonksiyonlar gerekirdi. Ancak tür bilgisi kaybolduğu için aşağıdaki kod yeterli olacaktır:
// Üreysel singleton fabrika tasarım kalıbı
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FN;
}
IDENTITY_FN
fonksiyonu UnaryFunction<T>
‘ye dönüştürülürken derleyici kontrolsüz tür dönüşümü (unchecked cast) uyarısı verecektir. Ancak burada IDENTITY_FN
fonksiyonu kendisine verilen değişkeni aynen geri döndürdüğü için tür güvenliğini tehlikeye sokmaz. Bu sebeple de @SuppressWarnings("unchecked")
notasyonu ile bu derleyici uyarısı gizlenmiştir.
Bu fabrika metodunu kullanarak UnaryOperator<String>
ve UnaryOperator<Number>
üretip kullanan kod aşağıdaki gibi olacaktır:
// Üreysel singleton fabrikasının kullanımı
public static void main(String[] args) {
String[] strings = { "jute", "hemp", "nylon" };
UnaryOperator<String> sameString = identityFunction();
for (String s : strings) {
System.out.println(sameString.apply(s));
}
Number[] numbers = { 1, 2.0, 3L };
UnaryOperator<Number> sameNumber = identityFunction();
for (Number n : numbers) {
System.out.println(sameNumber.apply(n));
}
}
Çok yaygın olmasa da bazen tür parametresinin, kendisini de içeren bir tür sınırlama ifadesiyle sınırlandırıldığını görebilirsiniz. Buna özyineli tür sınırlaması (recursive type bound) denir. Bunun çok bilinen bir örneği Comparable
arayüzü ile beraber kullanılır (Madde 14):
public interface Comparable<T> {
int compareTo(T o);
}
Burada T
tür parametresi, arayüzü uygulayan sınıfın kendi nesnelerini hangi tür nesnelerle karşılaştırmak istediğini belirler. Çoğu durumda her sınıf kendi türünden nesnelerle karşılaştırılır. Bu sebeple de örneğin String
sınıfı Comparable<String>
, Integer
ise Comparable<Integer>
olarak bu arayüzü uygular.
Birçok statik yardımcı metot Comparable
arayüzünü uygulayan bir Collection
parametresi alarak sıralama, arama, en küçük ve en büyük değerleri hesaplama gibi işlemler yapar. Bunu yapabilmek için, Collection
içinde yer alan elemanların birbirleriyle karşılaştırılabilir olması gerekir. Bu koşulu aşağıdaki gibi ifade edebiliriz:
// özyineli tür sınırlaması örnek kullanımı
public static <E extends Comparable<E>> E max(Collection<E> c);
Burada sınırlandırılmış olan tür parametresi <E extends Comparable<E>>
, metoda geçilebilecek E
türünün kendi türünden nesnelerle karşılaştırılabilmesi gerektiğini ifade etmektedir.
Şimdi yukarıdaki metot imzasını kullanarak bir metot gövdesi yazalım. Bu metot Collection<E>
içindeki en büyük değeri bularak döndürmektedir:
// Özyineli tür sınırlaması kullanarak
// koleksiyondaki en büyük değeri bulur
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty()) {
throw new IllegalArgumentException("Empty collection");
}
E result = null;
for (E e : c) {
if (result == null || e.compareTo(result) > 0) {
result = Objects.requireNonNull(e);
}
}
return result;
}
Bu metot boş listeler için IllegalArgumentException
fırlatmaktadır. Daha iyi bir çözüm olarak Optional<E>
döndürülebilir. (Madde 55)
Özyineli tür sınırlamaları bundan çok daha karmaşık olabilmektedir ancak böyle kullanımlar çok azdır. Bu kitapta gösterilen kullanımları anladığınız taktirde pratikte karşılaşabileceğiniz çoğu durumla baş edebilirsiniz.
Özetle, üreysel metotlar da üreysel türler gibi hem daha güvenli hem de istemcilerin tür dönüşümü yapmalarına gerek kalmadığı için kullanımları daha kolaydır. Yazdığınız metotları kullanırken istemciler tür dönüşümü yapmak zorunda kalıyorsa, üreysele çevirerek bunu düzeltin. Yine türlerde olduğu gibi, kullanımları tür dönüşümü gerektiren metotları da güvenli bir biçimde üreysele çevirebilirsiniz çünkü mevcut istemciler etkilenmeyecektir. (Madde 26)