Comparable arayüzü, Java 1.2'de Collections Framework ile birlikte tanıtılmıştır ve sınıflar için doğal sıralama tanımlar. Sözleşme, compareTo yönteminin equals ile tutarlı olması gerektiğini belirler: (compareTo(x, y) == 0) == (x.equals(y)). Bu tutarlılık, compareTo'ya dayanan sıralı koleksiyonların, tutarlı bir anlamını korumasını sağlar.
Eğer compareTo, equals'in farklı olarak değerlendirdiği nesneler için sıfır dönerse (veya tersine), TreeMap veya TreeSet gibi sıralı koleksiyonlar tanımsız bir davranış sergiler. Özellikle, içsel Kırmızı-Siyah ağaç yapısı gezinme için karşılaştırma kullanırken, koleksiyonun contains veya remove yöntemleri son doğrulama için equals'e dayanabilir. Bu ikilik, yinelemeyle mevcut görünen ama anahtar aramasıyla elde edilemeyen "hayalet" öğeler veya görünüşte mevcut olan ama kaldırılmayan öğeler oluşturur.
Çözüm, iki yöntem arasında sıkı bir uyum sağlamak gerektirir. Eğer compareTo iki nesneyi eşit görüyorsa (0 döndürüyorsa), equals true döndürmelidir. Uygulamalar, karşılaştırma mantığını ortak bir bileşene devretmeli veya kimlik tutarlılığını gözeten Comparator bileşimini kullanmalıdır. Doğal sıralamanın mantıksal eşitlikten farklı olduğu durumlar için, koleksiyon yapıcısına dışsal Comparator örnekleri sağlanmalıdır; bu, Comparable'ı tutarsız bir şekilde uygulamaktan kaçınır.
public class Employee implements Comparable<Employee> { private final String name; private final int id; public Employee(String name, int id) { this.name = name; this.id = id; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Employee)) return false; Employee other = (Employee) o; return this.id == other.id; // Eşitlik sadece ID'ye dayanmaktadır } @Override public int hashCode() { return Integer.hashCode(id); } @Override public int compareTo(Employee other) { return this.name.compareTo(other.name); // Sıralama sadece isme dayanmaktadır! } }
Bir bordro sistemi, çalışan tazminatını yönetmek için TreeMap<Employee, Salary> kullanır; burada Employee, alfabetik raporlama için soyadı temelinde Comparable olarak uygulanmıştır. Ancak, equals metodu hem çalışan ID'sini hem de ismini dikkate alarak, iki "John Smith" kaydını (ID'leri 101 ve 102) ayrı birer varlık olarak kabul eder. Prim işlemleri sırasında, sistem map.put(new Employee("Smith", 101), new Salary(5000)) kullanarak maaşları güncellemeye çalışır, ancak harita zaten "Smith" (ID 102) için bir kayıt içermektedir; bu durumda compareTo aynı isimlerden dolayı 0 döner.
Bir yaklaşım, equals yöntemini yalnızca isimleri karşılaştıracak şekilde değiştirmekti. Bu, sözleşmeyi yerine getirirdi ancak ismi paylaşan ayrı çalışanların ayrı varlıklar olarak kalması gereken iş gereksinimlerini çiğnerdi. Artısı, sözleşmeye uyum; eksisi, varlık kimliği bütünlüğünün kaybıydı.
Diğer bir yaklaşım, karşılaştırma sonrasında equals doğrulamasını zorlamak için TreeMap alım mantığını üst sınıfla geçersiz kılmaktı. Bu kimliği korusa da kırılgan bir özel koleksiyon uygulaması gerektiriyordu. Artısı, iş mantığının korunması; eksisi ise kod tabanı genelinde karmaşıklık ve bakım yükünün artmasıydı.
Ekip, Employee sınıfının Comparable uygulamasını tamamen kaldırarak, tam sıralama amaçları için TreeMap yapıcısına bir Comparator sağlamayı tercih etti. Bu, nesnenin içsel eşitliğinden sıralama mekanizmasını ayırdı; böylece doğal equals (ID'ye dayalı) kimliği yönetti, dışsal karşılaştırıcı ise sıralamayı gerçekleştirdi. Bu, hem Kırmızı-Siyah ağaç yapısının gereksinimlerini hem de alan modelinin bütünlüğünü korudu.
Bordro sistemi, aynı adı paylaşan ayrı çalışanları anahtar çakışma hataları olmadan başarıyla işledi. Prim hesaplamaları doğru bir şekilde belirli çalışan ID'lerini hedefledi ve alfabetik raporlama doğru kaldı. Mimari daha esnek hale geldi; farklı sıralama düzenleri (işe alma tarihi, departman) sadece karşılaştırıcı örneklerini değiştirmekle sağlandı, Employee sınıfını değiştirmeye gerek kalmadı.
Neden BigDecimal anahtar olarak TreeMap içinde kullanıldığında sözleşme ihlallerini özel olarak tetikler?
BigDecimal, 0.0 ve 0.00 gibi değerler için equals ile tutarsız bir şekilde Comparable'i uygulamaktadır. new BigDecimal("0.0").equals(new BigDecimal("0.00")) ifadesi false döner (farklı ölçekler) ama compareTo 0 döner (aynı sayısal değer). TreeMap anahtarları olarak kullanıldığında, ağaç bunları aynı olarak değerlendirir (aynı düğüm), ancak containsKey kontrolü equals kullanarak başarısız olabilir, bu da ekleme hatalarına veya yanlış değerlerin alınmasına yol açar. Bu tutarsızlık, 0.0 eklerken ve sonra 0.00 ararken, ağacın karşılaştırma ile düğümü bulduğu ama son eşitlik kontrolünün başarısız olduğu, anahtarın mevcut olmasına rağmen null döndürmesi anlamına gelir. Adaylar, BigDecimal'ı tutarlı ölçekle kullanmalı veya özel bir karşılaştırıcı sağlamalıdır.
Manuel compareTo uygulamasına kıyasla Comparator.comparing fabrikası yöntemi sözleşme ihlallerini nasıl önler?
Comparator.comparing API'si, Comparable anahtarlar veya çıkarım fonksiyonlarına bağlı karşılaştırıcılar üretir, ancak bu otomatik olarak üst nesnenin equals yöntemine senkronize olmaz. Adaylar, Comparator.comparing(Employee::getName) kullandıklarında, eğer Employee.equals ek alanları dikkate alıyorsa, sözleşmeyi ihlal ettiğini genellikle gözden kaçırırlar. Temelde, karşılaştırıcının iki nesneyi tam olarak eşit gördüğünde, nesnelerin kendilerini eşit olarak görmesi gerektiği anlamına gelir; bu nedenle her iki yöntemde alan eşitliğini sağlamak gerekmektedir. Çözüm, karşılaştırıcının eşitlik mantığının (0 döndürme) nesnenin eşitliği ile eşleşmesini sağlamak ya da equals mantığını alan bazında yansıtan thenComparing zincirleri kullanmak gerektirir.
Eğer compareTo istisnalar fırlatıyorsa veya yansıtıcı değilse, ne gibi tehlikeli davranışlar ortaya çıkar?
Yansıtıcı olmayan veya istisna fırlatan compareTo uygulamaları, Kırmızı-Siyah ağacın denge tutarlılıklarını bozar ve IllegalArgumentException ("Karşılaştırma yöntemi genel sözleşmeyi ihlal ediyor") veya ekleme/sıralama sırasında sonsuz döngülere neden olur. TreeMap, sıkı zayıf bir sıralama varsayar; ihlaller, düğümlerin geçersiz konumlarda yerleştirilmesine neden olur, BST özelliğini bozarak. HashMap'in hash çarpışmalarını zarif bir şekilde işlediği gibi, TreeMap tamamen karşılaştırma tutarlılığına dayanır; herhangi bir sapma, gezinme köprülerinin döngü şeklinde veya null referanslar oluşturmasına yol açar, bu da JVM'yi düşürebilir veya iş parçacığını askıya alabilir. Adaylar, compareTo'nun null'ları savunmacı bir şekilde işlemesini, geçişkenliğini korumasını ve simetrik olmasını sağlayarak JVM düzeyinde koleksiyon yolsuzluklarını önlemelidir.