Effective Java Madde 31: API Esnekliğini Artırmak İçin Sınırlandırılmış Joker (bounded wildcard) Kullanın

Madde 28’de anlatıldığı üzere parametreli türler arasında hiçbir koşulda alt tür/üst tür ilişkisi bulunmaz. Örneğin String türü Object‘in bir alt türü olmasına rağmen, List<String> ile List<Object> arasında böyle bir ilişki bulunmaz. List<Object> içerisine istediğiniz türden nesneleri koyabilirsiniz ama List<String> sadece String türünden nesneler içerebilir. Bu durumda List<String> türü, List<Object> türünün yaptığı her işi yapamadığına göre, alt türü de değildir.

Bu her ne kadar mantıklı olsa da bazı durumlarda bizi kısıtlayabilir. Madde 29’daki üreysel yığıt olarak tasarladığımız Stack sınıfını düşünelim. Hafızanızı tazelemek için API hatırlatması yapalım:

public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

Farzedelim ki bu sınıfa bir dizi nesne alıp hepsini yığıta ekleyen yeni bir metot yazmak istiyoruz. İlk denememiz aşağıdaki gibi olsun:

// joker tür kullanmayan pushAll metodu - kusurlu!
public void pushAll(Iterable<E> src) {
    for (E e : src) {
        push(e);
    }
}

Bu metot tertemiz derlenir ama tam tatmin edici değildir. src içerisindeki elemanların türü ile yığıtın eleman türü tıpatıp aynı ise sorunsuz çalışır. Elinizde bir Stack<Number> varsa ve siz yığıta tek bir eleman eklemek için push metodunu bir Integer nesnesi ile çağırırsanız sorunsuz çalışacaktır çünkü Integer sınıfı Number‘ın bir alt türüdür. Mantık olarak aşağıdakinin de çalışmasını beklersiniz:

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers);

Ancak denediğinizde aşağıdaki gibi bir hata alırsınız:

StackTest.java:7: error: incompatible types: Iterable<Integer>
   cannot be converted to Iterable<Number>
           numberStack.pushAll(integers);
                               ^

Burada hata almamızın sebebi, yazının başında belirtildiği gibi Integer ile Number arasında bulunan alt/üst tür ilişkisinin Iterable<Integer> ile Iterable<Number> arasında bulunmamasıdır.

Şanslıyız ki bunun bir çözümü var. Java bizlere sınırlandırılmış joker tür (bounded wildcard type) adında özel bir tür parametresi belirleme imkanı sunmaktadır. Bizim pushAll metodundan istediğimiz aslında sadece E türünden değil, E‘nin alt türlerinden nesneler içeren bir Iterator geçtiğimizde de doğru çalışarak bunları yığıta eklemesi. İşte joker tür tam burada devreye giriyor. pushAll metodunun parametresini Iterable<? extends E> olarak değiştirdiğimizde amacımıza ulaşmış oluyoruz. <? extends E> ifadesi E‘nin kendisi ve onu kalıtan türler anlamına gelmektedir. (Burada extends ifadesi hafif bir kafa karışıklığına sebep olabilir. Madde 29’dan hatırlayın, Java’da her tür kendisinin alt türü sayıldığı için bu ifade E türünü de kapsamaktadır.) Şimdi pushAll metodunu buna göre güncelleyelim:

// sınırlandırılmış joker tür ile esnekleştirilmiş pushAll metodu
public void pushAll(Iterable<? extends E> src) {
    for (E e : src) {
        push(e);
    }
}

Metodu yukarıdaki gibi yazdığımızda hem Stack sınıfı hem de daha önce hata veren istemci sınıfı sorunsuz derlenecek ve çalışacaktır. Derleyiciden uyarı almadığımız için tür güvenliğini de sağladığımızdan emin olabiliriz.

Şimdi de pushAll metoduna eşlik etmek üzere bir de popAll metodu yazmaya çalışalım. Bu metot yine bir koleksiyon parametresi alacak ama bu sefer yığıttaki elemanları çıkartıp bu koleksiyona ekleyecektir. Bu şekildeki bir popAll metodunu ilk denemede şöyle yazabiliriz:

// joker tür kullanılmayan popAll metodu, kusurlu!
public void popAll(Collection<E> dst) {
    while (!isEmpty()) {
        dst.add(pop());
    }
}

Önceki durumda olduğu gibi, eğer istemcinin geçtiği dst koleksiyonunun tür parametresi E ile yığıttaki elemanların türü tıpatıp aynı ise kod sorunsuz çalışacaktır. Ancak bu yine tatmin edici olmaz. Elimizde Stack<Number> türünde bir Stack varsa ve biz pop metodunu çağırıp sonucu bir Object içinde saklamak istesek hiçbir sorunla karşılaşmayız. O zaman aşağıdakini de yapabilmemiz gerekmez mi?

Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ... ;
numberStack.popAll(objects);

Bu istemci kodunu popAll metodunu da içeren Stack ile beraber derlersek, pushAll yazmaya çalışırken ilk başta aldığımıza çok benzer bir hatayla karşılaşırız. Bunun sebebi yine Collection<Object> ile Collection<Number> arasında bir alt/üst tür ilişkisi olmamasıdır. Bu sorunu da yine sınırlandırılmış joker tür ile çözebiliriz ama arada ufak bir fark var. Bu sefer yığıta yazma degil de okuma yaptığımız için, geçtiğimiz koleksiyon türünün E‘nin alt türlerini değil üst türlerini ifade etmesi gerekir. Bunu ifade etmenin yolu da tür parametresini Collection<? super E> olarak değiştirmektir. Dikkat ederseniz burada extends ifadesi üst türleri ifade edebilmek için super olarak değişti. Java’da her tür kendisinin üst türü olarak kabul edildiği için bu ifade hem E türünün kendisini de kapsamaktadır. Şimdi popAll metodunu yeniden yazalım:

// joker türünün üst türler için kullanımı  
public void popAll(Collection<? super E> dst) {
    while (!isEmpty()) {
        dst.add(pop());
    }
}

Bu kod kullanıldığı zaman hem Stack sınıfı hem de istemci uyarısız ve hatasız derlenip çalışacaktır.

Buradaki ders bellidir. Esnekliği artırmak için üretici (producer) ve tüketicileri (consumer) temsil eden girdi parametrelerini (input parameter) joker tür kullanarak tanımlayın. Farklı bir biçimde ifade edecek olursak, metoda geçilen parametreli tür T nesneleri üretmek için kullanılıyorsa <? extends T>, T nesnelerini tüketmek için kullanılıyorsa <? super T> kullanın.

Bizim pushAll örneğinde parametreli koleksiyon türü yığıt için yeni eleman üretmek için kullanıldığı için (üretici) extends, popAll metodunda ise benzer parametre yığıttan eleman çıkarmak için kullanıldığı için (tüketici) super kullanılmıştır. Şunu da belirmek gerekir ki, bir parametre hem üretici hem de tüketici görevi görüyorsa o zaman joker türü kullanmanın bir anlamı yoktur. Joker tür kullanmadan normal tür parametresi geçmek daha doğru olacaktır.

Bu kuralı unutmamak için PECS olarak aklımızda tutabiliriz. Bunun açılımı yukarıdaki anlatıma uygun olarak producer-extends, consumer-super olarak düşünülebilir. (producer=üretici, consumer=tüketici)

Bu ipucunu aklımızda tutarak, kitabın Üreyseller bölümünün daha önceki maddelerinde gördüğümüz örneklere tekrar bakalım. Madde 28‘deki Chooser sınıfının yapıcı metodundan başlayalım:

public Chooser(Collection<T> choices)

Bu yapıcı metot choices koleksiyonunu kullanarak T türünde nesneler üretmekte ve bunları sonradan kullanmak için saklamaktadır. Bu durumda üretici olduğundan dolayı joker türü extends ile kullanılmalıdır. Şimdi bu şekilde tekrar yazalım:

// T üreticisi görevi gören parametre için joker tür kullanımı
public Chooser(Collection<? extends T> choices)

Peki bu yeni tanım pratikte ne işimize yarayacak? Diyelim ki elimizde Chooser<Number> var ve biz yapıcı metoda List<Integer> geçmek istiyoruz. Orijinal tanımda bu mümkün olmazdı ama yukarıdaki gibi joker tür kullandığımızda sorunsuz çalışacaktır.

Şimdi de Madde 30’daki union metoduna bakalım. Bu metot parametre aldığı iki kümeyi birleştirip tek bir küme olarak döndürüyor:

public static <E> Set<E> union(Set<E> s1, Set<E> s2)

Hem s1 hem de s2 parametreleri üreticidir. PECS kuralına göre ikisini de extends ifadesi ile joker türe dönüştürebiliriz:

public static <E> Set<E> union(Set<? extends E> s1, 
                               Set<? extends E> s2)

Dikkat ederseniz, parametre olan Set türlerini joker türe çevirdik ama dönüş türünü (return type) ellemedik. Bu esneklik sağlamanın tersine, istemcileri joker türü kullanmaya zorlardı, bu istenen bir durum değildir. Yukarıdaki gibi güncellenen bir union metodunu kullanan aşağıdaki gibi bir istemci yazabiliriz (Java 8 ve sonrası):

Set<Integer> integers = Set.of(1, 3, 5);
Set<Double>  doubles  = Set.of(2.0, 4.0, 6.0);
Set<Number>  numbers  = union(integers, doubles);

Doğru kullanıldıklarında joker türler istemciler için görünmez olurlar, yani istemciler bunları fark etmeden dahi kodlarını yazabilirler. Kabul edilmesi gereken metot parametrelerini kabul eder, reddedilmesi gerekenleri reddelerler. Eğer bir sınıfın kullanıcısı kod yazarken sınıfta kullanılan joker türleri düşünüyorsa o sınıfın API’ında bir sorun var demektir.

Yine Madde 30’daki max metoduna bakalım. Tanımı şu şekildeydi:

public static <T extends Comparable<T>> T max(List<T> list)

Bunu PECS kuralına göre yeniden düzenlersek aşağıdaki gibi bir sonuç çıkar:

public static <T extends Comparable<? super T>> T max( 
                                List<? extends T> list)

Bu sonuca ulaşmak için iki kere PECS kuralını uyguladık. İlk olarak list parametresini T nesneleri ürettiği için List<T> iken List<? extends T> olarak değiştirdik. Kafa karıştırıcı kısım ise T extends Comparable<T> ifadesinin T extends Comparable<? super T> olarak değiştirilmiş olması. Bunun sebebi de Comparable<T> arayüzünün karşılaştırma yaparken T türündeki nesneleri tüketmesidir, bu sebeple de extends yerine super kullanılmıştır. (Burada tüketmek derken illa ki bir koleksiyondan çıkartılması, yok edilmesi manası çıkartılmamalıdır. Bir nesnenin parametre alınıp okunarak bir iş için kullanılması ”tüketilmesi” demektir)

Comparable ve Comparator her zaman tüketicidir. Bu sebeple Comparable<T> yerine Comparable<? super T>, Comparator<T> yerine de Comparator<? super T> kullanmanız önerilir.

Yukarıda yeniden yazdığımız max metodu bu kitapta göreceğiniz en karmaşık metot tanımıdır diyebiliriz. Peki bu karmaşıklık bize ne kazandırıyor? Örneğin aşağıdaki liste orijinal max metoduna parametre geçilemez, ama joker türlerle geliştirdiğimiz max metoduna bu listeyi geçebiliriz:

List<ScheduledFuture<?>> scheduledFutures = ... ;

Hatırlayacağınız üzere orijinal tanımda T extends Comparable<T> şunu söylemektedir: T türü sadece kendisiyle direk olarak karşılaştırılabilen türlerden seçilebilir. Bu listede kullanılan ScheduledFuture türü Comparable<ScheduledFuture> arayüzünü uygulamadığı için orijinal max metoduna geçilemez. Ancak ScheduledFuture arayüzü Delayed arayüzünü ve Delayed arayüzü de Comparable<Delayed> arayüzünü kalıtmaktadır. Bunun anlamı şudur: ScheduledFuture kendi türünden nesneler ile karşılaştırılamasa da, üst türü olan Delayed nesneleri ile karşılaştırılabilir. Bu durumda super kullanılarak eklenen joker tür parametresi problemi çözmektedir. Comparable (veya Comparator) arayüzünü direk uygulamayan ama uygulayan bir üst türü kalıtan türleri desteklemek istiyorsak yukarıdaki gibi bir joker tür parametresi kullanmamız gerekir

Joker türlerle alakalı tartışmaya değer bir konu daha var. Aşağıdaki gibi bir swap metodunu üreysel mekanizmalar kullanarak tanımlamanın iki yolu vardır. Birincisi sınırlandırılmamış tür parametresi (Madde 30) ikincisi ise sınırlandırılmamış joker türü kullanmaktadır:

// swap metodunun iki farklı tanımı
public static <E> void swap(List<E> list, int i, int j); 
public static void swap(List<?> list, int i, int j);

Bunlardan hangisini kullanmak daha mantıklıdır ve neden? Açık (public) bir API için ikinci kullanım daha mantıklıdır çünkü daha basittir. İstediğiniz bir listeyi geçebilirsiniz ve metot i ve j indislerindeki (index) elemanları yer değiştirecektir. Tür parametresi ile ilgilenmeye gerek yoktur. Bir kural olarak, eğer tür parametresi metot tanımında sadece bir kere görülüyorsa bunu joker türüne çevirebilirsiniz. Tür parametresi sınırlandırılmamış ise sınırlandırılmamış joker, sınırlandırılmış ise de sınırlandırılmış jokere çevirebilirsiniz.

Ancak ikinci swap tanımıyla ilgili bir problem vardır, aşağıdaki gibi yazıldığında derlenmeyecektir:

public static void swap(List<?> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

Bu kod aşağıdaki gibi çok da anlaşılmayan bir hata vermektedir:

Swap.java:5: error: incompatible types: Object cannot be
   converted to CAP#1
           list.set(i, list.set(j, list.get(i)));
                                           ^
     where CAP#1 is a fresh type-variable:
       CAP#1 extends Object from capture of ?

Bir elemanı listeden okuyup sonra aynı listede başka bir indise yazarken hata almak pek de mantıklı değil. Buradaki problem şudur: List<?> türünden listelere null haricinde bir değer yazmak yasaktır. Ancak bunu tür güvenliğini tehlikeye atmadan çözmek mümkündür. Yardımcı bir private metot yazarak ? ile temsil edilen türü yakalayabilir, sonra da elemanları yer değiştirebiliriz:

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}

// joker türünü yakalamak için yazılmış yardımcı metot
private static <E> void swapHelper(List<E> list, int i, int j) { 
    list.set(i, list.set(j, list.get(i)));
}

swap metodunda ? ile temsil edilen tür bilgisi swapHelper içerisinde artık E ile ifade edilmektedir. İçinde E türünden nesneler olan bir listenin elemanlarının yerlerini değiştirmek tür güvenliğini tehlikeye atmayacağı için bu kod tertemiz derlenecektir. Biraz dolambaçlı bir yöntem gibi görünse de, bu sayede istemciler daha karmaşık görünen swapHelper metodunun imzasını görmeyecek ama bundan faydalanabilecektir.

Özetle, API tasarlarken joker türler kullanmak biraz alengirli olsa da esnekliği çok artıracaktır. Eğer çok kullanılan bir kütüphane yazacak olursanız, joker tür kullanımı çok daha önemli hale gelmektedir. Temel PECS (producer-extends consumer-super) kuralımızı hatırlayın. Comparable ve Comparator her zaman tüketicidir, bunu da aklınızdan çıkarmayın.

Share

Effective Java Madde 14: Comparable Arayüzünü Gerektiğinde Uygulayın

Bu bölümde gördüğümüz diğer metotların aksine, compareTo metodu Object içerisinde tanımlanmış değildir. Bunun yerine Comparable arayüzünün tek metodu olarak karşımıza çıkmaktadır. Karakter olarak Object sınıfındaki equals metoduna benzer ancak eşitlik karşılaştırması yanında sıralama da yapabilir ve üreyseldir (generic). Comparable arayüzünü uygulayarak, sınıfınızın nesneleri arasında sıralama yapılabileceğini belirtmiş olursunuz. Bu arayüzü uygulayan nesnelerden oluşan bir diziyi (array) sıralamak aşağıdaki gibi son derece kolaydır:

Arrays.sort(a); 

Benzer şekilde, Comparable arayüzü uygulandığında arama yapma, uç değerleri bulma, kendiliğinden sıralı veri yapılarını yönetme gibi problemler de çok kolaylaşır. Örneğin, aşağıdaki program komut satırından girilen parametreleri alfabetik sırada yazdırmakta ve tekrar edilen değerleri elemektedir. Bu kodun doğru çalışması String sınıfının Comparable arayüzünü uygulamasıyla mümkün olmaktadır.

public class WordList { 
    public static void main(String[] args) { 
        Set<String>; s = new TreeSet<String>(); 
        Collections.addAll(s, args); 
        System.out.println(s); 
    } 
} 

Comparable arayüzünü uygulayarak, sınıfınızın birçok üreysel (generic) algoritma ve veri yapılarıyla uyumlu çalışmasını sağlamış olursunuz. Az çaba harcayarak çok büyük fayda sağlarsınız. Java platformundaki hemen hemen bütün değer sınıfları Comparable arayüzünü uygularlar. Siz de bir değer sınıfı yazıyorsanız ve bu sınıfın nesneleri arasında sıralama yapılabiliyorsa (alfabetik, sayısal, kronolojik vs.) aşağıda imzası verilen Comparable arayüzünü uygulamayı çok ciddi olarak düşünmelisiniz.

public interface Comparable<T> { 
    int compareTo(T t); 
}

compareTo metodunun sözleşmesi equals sözleşmesine benzer ve aşağıdaki şekildedir:

Bu nesneyi (this) parametre olarak geçilen nesne ile karşılaştırır ve daha küçükse negatif bir sayı, eşitse 0 ve daha büyükse pozitif bir sayı döndürür. Eğer parametre geçilen nesne karşılaştırılamıyorsa ClassCastException fırlatılır.

Aşağıdaki açıklamada sgn(a) fonksiyonu, a değeri negatifse -1, sıfırsa 0 ve pozitifse 1 döndürecek şekilde tanımlanmıştır.

  • compareTo metodu, bütün x ve y değerleri için sgn(x.compareTo(y) == -sgn(y.compareTo(x) koşulunu sağlamalıdır. Bu aynı zamanda şu anlama da gelmektedir: x.compareTo(y) ifadesinin ClassCastException fırlatabilmesi için y.compareTo(x) ifadesinin de aynı istisnayı fırlatması gerekmektedir.
  • Geçişilik prensibinin sağlanması gereklidir. Yani (x.compareTo(y) > 0 && y.compareTo(z) > 0) ise, x.compareTo(z) > 0 olmalıdır.
  • x.compareTo(y) == 0 ise, bütün z değerleri için sgn(x.compareTo(z)) == sgn(y.compareTo(z)) doğru olmalıdır.
  • Şart olmamakla birlikte şu ifadenin doğru olacak biçimde compareTo metodu yazılması önerilmektedir: (x.compareTo(y) == 0) == (x.equals(y)). Herhangi bir sınıf Comparable arayüzünü gerçekleştirdiği halde yukarıdaki kuralı sağlamıyorsa, bunu açık bir şekilde ifade etmelidir.

Yukarıdaki sözleşmenin matematiksel içeriği gözünüzü korkutmasın. equals sözleşmesi (Madde 10) gibi bu sözleşme de göründüğü kadar karmaşık değildir ve yukarıdaki kuralları sağlayan compareTo metotları yazmak çok da zor değildir. equals‘ın tersine, compareTo metodu farklı sınıflar üzerinde çalışmak zorunda değildir. Farklı türden nesneler karşılaştırılmaya çalışıldığında ClassCastException fırlatabilir, hatta yapması gereken de budur.

Nasıl hashCode
sözleşmesine uymayan bir sınıf hash tabanlı çalışan diğer sınıfları bozuyorsa, compareTo sözleşmesine uymayan bir sınıf da karşılaştırmaya dayalı çalışan diğer sınıfları bozar. Bu tür sınıflara örnek olarak TreeSet, TreeMap gibi sıralı veri yapıları ve sıralama, arama fonksiyonları içeren Collections, Arrays gibi yardımcı sınıfları verebiliriz.

Şimdi yukarıdaki sözleşmenin maddeleri üzerinden kabaca gidelim. Birinci maddeye göre, iki nesne karşılaştırıldığında eğer birinci ikinciden küçükse, ikinci birinciden büyük olmalı, birinci ikinciye eşitse ikinci de birinciye eşit olmalı, birinci ikinciden büyükse ikinci de birinciden küçük olmalıdır. İkinci maddeye göre, eğer birinci nesne ikinciden büyükse ve ikinci nesne de üçüncü başka bir nesneden büyükse, birinci de üçüncüden büyük olmalıdır. Son maddeye göre ise birbirine eşit olan nesneler başka nesnelerle karşılaştırıldığında aynı sonucu üretmelidirler.

Bu üç maddeye bakıldığında, compareTo tarafından tanımlanan eşitlik testi ile equals ile tanımlanan eşitlik testi aynı koşullara dayanmaktadır: yansıma, simetri ve geçişlilik.
Bu yüzden de Madde 10’da yaptığımız uyarıyı burada da yapabiliriz: nesnesi yaratılabilen bir sınıfı hem kalıtarak yeni bir anlamlı değer eklemek, hem de compareTo sözleşmesini korumak nesne tabanlı soyutlamanın faydalarından vazgeçmek istemediğiniz sürece mümkün değildir. Bu sorun için aynı çözüm yolunu burada da uygulayabiliriz: eğer Comparable arayüzünü uygulayan bir sınıfı kalıtarak yeni bir anlamlı değer eklemek istiyorsanız, bu sınıfı kalıtmak yerine başka bir sınıf yaratıp içerisine bu sınıfın bir referansını koyun.

compareTo sözleşmesinin son paragrafı, bir kuraldan ziyade çok güçlü bir öneriden bahsetmektedir. Bu öneriye göre compareTo tarafından tanımlanan eşitlik testi equals ile uyumlu olmalı ve aynı sonuçları döndürmelidir. Bu kuralı ihlal etmek bir felakete yol açmasa da, sıralı veri yapılarıyla birlikte kullanıldığında hatalara neden olabilir.

Örneğin, compareTo metodu equals ile uyumsuz çalışan BigDecimal sınıfını ele alalım. Eğer bir HashSet nesnesi yaratır ve BigDecimal("1.0") ve BigDecimal("1.00") şeklinde iki tane farklı nesneyi bu kümeye eklerseniz sonuçta iki elemanlı bir kümeniz olur, çünkü bu iki nesne equals karşılaştırmasına göre eşit değildir. Ancak aynı iki nesneyi HashSet yerine TreeSet veri yapısına eklerseniz tek elemanlı bir kümeniz olur çünkü bu iki nesne compareTo ile karşılaştırıldığında eşittir ve TreeSet bu karşılaştırmaya göre çalışmaktadır. (Detaylı bilgi için BigDecimal dokümantasyonuna bakabilirsiniz)

compareTo metodu yazmak equals yazmaya çok benzer ama arada birkaç önemli fark vardır. Comparable arayüzü üreysel (generic, parameterized) olduğu için compareTo içerisinde karşılaştırma yaparken tür dönüşümü yapmaya gerek yoktur. Eğer parametre farklı türde geçilirse derleme hatası olacaktır, eğer null geçilirse de NullPointerException fırlatılacaktır.

compareTo içerisindeki alan karşılaştırmaları eşitlikten ziyade sıralama odaklıdır. Eğer nesne içerisinde başka nesnelere referanslar varsa da bu referanslar üzerinden özyineli olarak compareTo metodu çağrılabilir. Eğer bu alan Comparable arayüzünü gerçekleştirmiyorsa bir Comparator kullanarak karşılaştırma yapabilirsiniz. Bunu isterseniz kendiniz yazabilir, isterseniz de aşağıdaki örnekte olduğu gibi önceden var olan bir tane kullanabilirsiniz. (CaseInsensitiveString sınıfını Madde 10’de bulabilirsiniz)

public final class CaseInsensitiveString 
    implements Comparable<CaseInsensitiveString> { 

    public int compareTo(CaseInsensitiveString cis) { 
        return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s); 
    } 
    ... // Sınıfın geri kalanı ihmal edilmiştir. }
}

Burada CaseInsensitiveString sınıfının üreysel Comparable arayüzünü gerçekleştirmiş olması önemlidir. Bu sayede, CaseInsensitiveString nesnelerinin sadece başka CaseInsensitiveString nesneleriyle karşılaştırılabileceğini belirtmiş oluyoruz. Dikkat edersiniz ki compareTo metodunun parametresi de Object değil, CaseInsensitiveString türündedir, bunu sağlayan da üreysel olarak (generic) gerçekleştirilen Comparable arayüzüdür.

Karşılaştırmanız gereken ilkel alanlar varsa < ve > operatörleri yerine, Java 7 ile boxed-primitive sınıflara (Integer, Short gibi) eklenen static compare metotlarını kullanmalısınız.

Eğer bir sınıfın birden fazla anlamlı alanı varsa, bu alanları hangi sırayla karşılaştırdığınız önemlidir. Her zaman en önemli alandan başlayıp önemsize doğru gitmeniz gerekmektedir. Eğer herhangi bir noktada karşılaştırma 0’dan başka bir sonuç üretirse (eşitsizlik durumu) başka alanlara bakmadan o sonucu direk döndürmelisiniz. Eğer alanlar eşit çıkarsa, bir sonraki alanı karşılaştırarak devam etmelisiniz. Bütün alanlar eşit çıkarsa da eşitliği bildiren 0 değerini döndürmelisiniz.
Madde 11’de kullandığımız PhoneNumber sınıfı için bu yöntem aşağıdaki gibi kodlanabilir:

public int compareTo(PhoneNumber pn) {
    int result = Short.compare(areaCode, pn.areaCode);
    if (result == 0)  {
        result = Short.compare(prefix, pn.prefix);
        if (result == 0) { 
            result = Short.compare(lineNum, pn.lineNum);
        }
    }
    return result;
}

Java 8’de Comparator arayüzü, daha akıcı bir biçimde comparator nesneleri yaratabileceğimiz bir takım yapıcı metotlar eklenerek geliştirilmiştir. Bu yapıcı metotlar compareTo metodunda karşılaştırma yapmak için kullanılabilir. Birçok yazılımcı bu yöntemi daha kısa ve öz olduğu için bir miktar performans kaybı olmasına rağmen tercih etmektedir. PhoneNumber nesnelerini bir dizi içerisinde sıralamak bu yöntemle benim makinamda %10 daha yavaş çalışmaktadır. Bu yöntemi kullanırken, Java’nın sağladığı static import özelliğinden faydalanarak yapıcı metotları sınıf ismi belirtmeden sadece isimleriyle kullanmak mümkün. Bu şekilde yazılmış bir compareTo metodu aşağıdaki gibi olacaktır:

// Comparator yapıcı metotları kullanılarak yazılan compareTo metodu 
private static final Comparator<PhoneNumber> COMPARATOR =
       comparingInt((PhoneNumber pn) -> pn.areaCode)
         .thenComparingInt(pn -> pn.prefix)
         .thenComparingInt(pn -> pn.lineNum);
    
public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}

Bu gerçekleştirimde, sınıf başlatıldığında (initialization) iki farklı comparator yapıcı metodu kullanılarak bir comparator nesnesi oluşturuluyor. Bunların ilki comparingInt. Bu yapıcı metot parametre olarak int değeri üreten bir lambda fonksiyonu kabul ediyor ve geriye döndürdüğü Comparator<PhoneNumber> nesnesi, compare metodu çağrılırken parametre geçilen PhoneNumber nesnelerinin areaCode değerlerine göre sıralama yapabiliyor.

Eğer iki nesne de aynı areaCode değerine sahipse, o zaman thenComparingInt kullanarak belirtilen kriterler devreye girerek karşılaştırmaya devam ediliyor. Bu şekilde birden çok sayıda thenComparingInt ekleyerek comparator nesneleri oluşturabilirsiniz.

Comparator sınıfı bu şekilde çok sayıda yapıcı metotlar barındırır. Yukarıda int değerler için gösterilen kullanımın benzerleri long ve double türleri için de mevcuttur. İlkel türler dışında, nesne referansları ile kullanılabilecek comparing yapıcı metotları da bulunmaktadır. (Detaylar için: https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html)

Özetle, yazdığınız bir sınıfın nesnelerinin birbirleriyle mantıklı bir karşılaştırması yapılabiliyorsa, bu durumlarda Comparable arayüzünü uygulayın ki bu sınıftan üretilen nesneler kolayca sıralanabilsin, aranabilsin ve karşılaştırma esasına göre çalışan veri yapılarıyla beraber kullanılabilsin. compareTo metodunu yazarken de ilkel türler için < ve > operatörleri yerine static compare metotlarını veya Comparator arayüzüyle gelen yapıcı metotları kullanın.

Share