Yeni mezun meslektaşlarımızın List, Map, Set türevi veri yapılarında nesneleri konumlandırdıkları zaman nesnelerin equals ve hashCode metotlarını hazırlamamaları yaygın bir unutkanlıktır. List bileşenine koydukları nesne nedense
contains() kontrolünde kaybolur. Sorunun çözümünü de benzer olayları önceden yaşamış ve kaynağını az çok tahmin eden tecrübeli yazılım geliştiricen gelir.
equals ve
hashCode metotlarının sınıf için gerçekleştirimi unutulmuştur. List ve Set sınıfları barındırdıkları nesnelerin eşitliğini bu iki metodu kullanarak kontrol ederler
Ne var ki
equals ve hashCode metotları tecrübeli geliştiriciler tarafından bile olması gerektiği gibi hazırlanmıyor. Bu yazımda "Effective Java" kitabında yer alan "Item 8: Obey the general contract when overriding equals" bölümüne referans göstererek ideal bir
equals metodunun nasıl olması gerektiğine değineceğim.
İlk uyulması gereken kural basit. "En iyi kod yazılmamış kodtur" ilkesini benimsemiş gibi: Eğer sınıfın her bir nesnesi
sadece kendisiyle eşit olacak ise, yani
sadece aynı bellek alanına referans gösteren değişkenler birbirleriyle eşit olacak ise sınıfta
equals metodu yazmayın. Her Java sınıfının türediği
Object sınıfının
equals metodu bu bellek kontrolünü üstlenmiş durumda zaten. Böyle bir durumu oluşturabilecek senaryolar ise şu şekilde listeleniyor:
- Sınıfın değerleri üzerinden işlevsel bir eşitlik gerektirmiyorsa: Örneğin java.util.Random sınıfının görevi rasgele sayılar üretmektir. İki Random sınıfının birbirine eşit olup olmaması işlevsel bir eşitlik kontrolü gerektirmez.
- Atasınıfın (superclass) ihtiyacı karşılayacak şekilde gerçekleştirilmiş halihazırda bir equals metodu varsa: Çoğu Set gerçekleştirimi AbstractSet, List gerçekleştirimleri ise AbstractList atasınıfının equals metodunu kullanır.
- Eğer sınıf private ve package-private ise ve sınıflar üzerinde eşitlik kontrolü yapılmayacağı kesinse
- Sınıfın sadece yaratılmış tek bir olgusu olacak ise: Singleton sınıflar
Özetle sınıfımızın eşitliği, barındırdığı değişkenlerin değerleri üzerinden anlam ifade ediyorsa ve atasınıfta bu değerler üzerinden eşitlik kontrolünü sağlayacak metot yoksa sınıfımız için bir equals metodu yazmalıyız.
Peki ideal bir equals nasıl olmalı? Matematik derslerinden hatırlayacağımız aşağıdaki 5 kriteri de karşılamalı:
- Dönüşlü: null olmayan her x değeri için x.equals(x)'in sonucu true olmalı.
- Simetrik: null olmayan her x, y değerleri için x.equals(y) sadece y.equals(x) sonucu true ise true olmalı.
- Geçişli: null olmayan her x, y, z değerleri için x.equals(y) ve y.equals(z) sonuçları true ise x.equals(z) sonucu da true olmalı.
- Tutarlı: eşitlik kontrolünde kullanılan değişkenlerin değerleri değişmediği sürece x.equals(y) sonucu her zaman ya hep true ya da hep false olmalı.
- null olmayan her x için x.equals(null) sonucu hep false olmalı
Bu eğlenceli matematiksel kriterleri sağlayamayan her equals metodu uygulamamızda sıkıntı yaratacak ve hatanın kaynağı bulmak da belki uzunca bir süre mümkün olmayacaktır. Bu kriterlere biraz yakından bakalım:
Dönüşlü
Basitçe, her bir nesne kendisine eşit olmalı. Bu kriteri bozacak bir gerçekleştirimde, List'e eklediğimiz nesnemizi contains metoduyla sorguladığımızda karşılaşacağımız sonuç olumsuz olacaktır.
Simetrik
Simetri kriterini aşağıdaki örnek kod kırıyor. (Örnek kodlar Effective Java kitabından alınmıştır):
// Broken - violates symmetry!
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
if (s == null)
throw new NullPointerException();
this.s = s;
}
// Broken - violates symmetry!
@Override public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(
((CaseInsensitiveString) o).s);
if (o instanceof String) // One-way interoperability!
return s.equalsIgnoreCase((String) o);
return false;
}
... // Remainder omitted
}
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");String s = "polish";
Burada cis.equals(s) kontrolü true dönerken s.equals(cis) kontrolü ise false değerini dönecektir. Problemin kaynağı CaseInsensitiveString sınıfının equals metodu, karşılaştırmayı büyük/küçük harf ayırtmaksızın yaparken, String sınıfının equals metodunun bu ayrımı yapmamasından kaynaklanmaktadır.
List list = new ArrayList();
list.add(cis);
Yukarıdaki gibi bir senaryoda yapılan list.contains(s) kontrolü kodun üzerinde çalıştığı Java gerçekleştirimine göre true ya da false dönebilir. "Benim bilgisayarımda çalışıyordu" dememek için hastalıklı kodu düzeltmeliyiz. Karşılaştırmada String sınıfı aradan çıkartılmalı:
@Override public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
Geçişli
Bu kriteri kırmak da çok zor değil. Point sınıfından türeyen bir ColorPoint sınıfında koordinatlara ek olarak getirilen renk değişkeni equals metoduna eklenince olanlar oluyor:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point p = (Point)o;
return p.x == x && p.y == y;
}
... // Remainder omitted
}
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
... // Remainder omitted
}
// Broken - violates transitivity!
@Override public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
// If o is a normal Point, do a color-blind comparison
if (!(o instanceof ColorPoint))
return o.equals(this);
// o is a ColorPoint; do a full comparison
return super.equals(o) && ((ColorPoint)o).color == color;
}
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
p1 ile p2, p2 ile p3 birbirlerine eşit iken p1 ile p3 birbirine eşit olmalıyken maalesef eşitlik kontrolü bize false değerini dönecektir. Java kütüphanesinde bu kriteri kıran sınıflar bulmak mümkün. Örneğin Date ve Timestamp sınıfları. Ayrıntılı değerlendirmeyi "Effective Java" kitabından edinebilirsiniz.
Bu sorun için çözüm ise gene bir Object Oriented tasarım ilkesini adres gösteriyor. "is-a ilişkisi yerine has-a" ilişkisi kullanmak. Point sınıfından ColorPoint sınıfı türetmek yerine ColorPoint sınıfı içerisinde Point sınıfının referansını barındırsın.
Tutarlılık
Eşitlik kontrolü yapılacak değişkenler her zaman tutarlı değerlere sahip olmalıdır. Örneğin java.net.URL sınıfının eşitlik kontrolü IP adresi ile ilişkilendirilen host name değişkeni üzerinden yapılıyor. Bu yüzden ağ erişiminin yanı sıra host name ile ilişkilendirilmiş IP adresi de değişebilir. Sağlıklı bir karşılaştırma her zaman yapılamayacaktır.
Equals metodu için bu olmazsa olmazların yanında sorguyu hızlandıracak ve kesirli değerler için equals yerine Float.compare() ya da Double.compare() kullanmanız gerektiğini söyleyen bir çok minik ipucunu "Effective Java" kitabında bulabilirsiniz. Kitabın Java ile geliştirim yaparken dikkat edilmesini önerdiği diğer konular da her Java geliştiricisi tarafından sindirilip kod yazarken uygulanmalı. Kitaptan bir tane edinin derim :)