Press "Enter" to skip to content

Effective Java Madde 30: Üreysel Metotları Tercih Edin

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)

Share

Leave a Reply

%d bloggers like this: