Press "Enter" to skip to content

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

Last updated on April 5, 2020

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

2 Comments

  1. […] Ç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): […]

Leave a Reply

%d bloggers like this: