Home > Java > Effective Java Madde 12: Comparable Arayüzünü Gerektiğinde Uygulayın

Effective Java Madde 12: 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 ve 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 8) 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. Farklı türden nesnelerin karşılaştırılmasını engelleyen bir madde sözleşmede yer almasa da, Java 1.6 itibariyle Java platformu içerisinde bunu yapan bir sınıf yoktur.

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 8’de 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 8’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 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.

Basit ilkel alanları karşılaştırıken < ve > operatörlerini, kayan noktalı (floating point) değerleri karşılaştırırken ise Float.compare ve Double.compare metotlarını kullanın. Diziler söz konusu olduğunda bu kuralı her bir elemana ayrı ayrı uygulayın.

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 9’da kullandığımız PhoneNumber sınıfı için bu yöntem aşağıdaki gibi kodlanabilir:

public int compareTo(PhoneNumber pn) {

    // areaCode değerlerini karşılaştır
    if (areaCode < pn.areaCode)
        return -1;
    if (areaCode > pn.areaCode)
        return 1;

    // areaCode değerleri eşit, prefix karşılaştır
    if (prefix < pn.prefix)
        return -1;
    if (prefix > pn.prefix)
        return 1;

    // areaCode ve prefix değerleri eşit, line number karşılaştır
    if (lineNumber < pn.lineNumber)
        return -1;
    if (lineNumber > pn.lineNumber)
        return 1;
    
    return 0; // bütün alanlar eşit

Yukarıdaki metod doğru çalışacaktır ancak yine de geliştirilebilir. compareTo() sözleşmesi döndürülen int değerinin kaç olduğuyla ilgilenmez, sadece sayının negatif veya pozitif olmasıyla ilgilenir. Bu avantajı kullanarak aynı metodu aşağıdaki gibi de yazabiliriz:

public int compareTo(PhoneNumber pn) {

    // areaCode değerlerini karşılaştır
    int areaCodeDiff = areaCode - pn.areaCode;
    if (areaCodeDiff != 0)
        return areaCodeDiff;

    // areaCode değerleri eşit, prefix karşılaştır
    int prefixDiff = prefix - pn.prefix;
    if (prefixDiff != 0)
        return prefixDiff;
    
    // areaCode ve prefix değerleri eşit, line number karşılaştır
    return lineNumber - pn.lineNumber;
}

Bu yöntem burada çalışacaktır ancak yine de çok dikkatli olmalısınız. Eğer bir alan için mümkün olan en büyük değerle mümkün olan en küçük değerin farkı Integer.MAX_VALUE (2^31-1) değerinden büyükse bu yöntem yanlış sonuç üretecektir. Eğer i çok büyük bir pozitif sayı ve j çok küçük bir negatif sayı ise i – j taşacak ve negatif bir değer üretecektir. Bu da compareTo() metodunun yanlış sonuçlar döndürmesine sebep olacak, sözleşmenin birinci ve ikinci maddelerini ihlal edecektir. Bu teorik bir problem değildir, gerçek hayatta hatalara yol açmıştır ve bu tür hatalar compareTo() metodu istisna fırlatmayacağı için farkedilmesi oldukça zordur.

  1. No comments yet.
  1. No trackbacks yet.

Please leave these two fields as-is:

Protected by Invisible Defender. Showed 403 to 641.622 bad guys.

%d blogcu bunu beğendi: