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çinsgn(x.compareTo(y) == -sgn(y.compareTo(x)
koşulunu sağlamalıdır. Bu aynı zamanda şu anlama da gelmektedir:x.compareTo(y)
ifadesininClassCastException
fırlatabilmesi içiny.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çinsgn(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ıfComparable
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.
[…] Ç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): […]
[…] ifadesidir. Buradaki comparing metodu bir Comparator nesnesi üretmektedir (Madde 14). Argüman olarak geçilen freq::get metot referansı ise tabloda anahtarlara karşılık gelen […]