Java: neden koleksiyonlar bir Karşılaştırıcıyı kabul ediyor ama kabul etmiyor (varsayımsal) Hasher ve Equator?


25

Bu sorun, bir arabirimin farklı uygulamalarına sahip olduğunuzda en belirgindir ve belirli bir koleksiyonun amaçları için yalnızca nesnelerin arabirim düzeyinde görünümünü önemsersiniz. Örneğin, bunun gibi bir arayüzünüz olduğunu varsayalım:

public interface Person {
    int getId();
}

Uygulamanın hashcode()ve equals()sınıfları uygulamanın genel yolu , equalsyöntemde bu gibi bir koda sahip olacaktır :

if (getClass() != other.getClass()) {
    return false;
}

Bu Person, a HashMap. Eğer HashMapsadece arayüz seviyesi görünümü umurunda Person, o zaman onların uygulanmasında sınıflarında sadece farklı tekrarlar da yeterli olabilir.

equals()Tüm bu uygulamalar için aynı liberal yöntemi kullanarak bu durumu çözebilirsiniz , ancak daha sonra equals()yanlış bir şeyi farklı bir bağlamda yapma riskiyle karşı karşıya kalırsınız (örneğin, Personveritabanı kayıtlarıyla desteklenen iki saniyeyi sürüm numaraları ile karşılaştırmak gibi ).

Sezgim bana sınıf başına değil, koleksiyon başına eşitlik tanımlanması gerektiğini söylüyor. Siparişe dayalı koleksiyonlar kullanırken, Comparatorher bağlamda doğru sıralamayı seçmek için bir özel kullanabilirsiniz . Karma tabanlı koleksiyonlar için analog yoktur. Bu neden?

Sadece açıklığa kavuşturmak için, bu soru, koleksiyonların uygulanmasıyla ilgilendiği için, ".equals () Java'da bir sınıftayken neden bir arayüzde .compareTo ()? " Dan farklıdır . compareTo()ve equals()/ hashcode()her ikisi de koleksiyon kullanırken evrensellik sorunundan muzdarip: farklı koleksiyonlar için farklı karşılaştırma işlevleri seçemezsiniz. Dolayısıyla bu sorunun amaçları için, bir nesnenin kalıtım hiyerarşisi hiç önemli değil; tek önemli olan karşılaştırma fonksiyonunun nesne başına mı yoksa toplama başına mı tanımlandığıdır.


5
PersonBeklenen equalsve hashCodedavranış davranışını uygulamak için her zaman sarmalayıcı nesnelerini tanıtabilirsiniz . O zaman olurdu HashMap<PersonWrapper, V>. Bu, saf OOP yaklaşımının zarif olmadığı bir örnektir: bir cisimdeki her işlem o cismin bir yöntemi olarak anlam kazanmaz. Java'nın tüm Objectonly - türü farklı sorumlulukların kaynaşmasıyla oluşmuş getClass, finalizeve toStringyöntemler bugünün en iyi uygulamalar tarafından uzaktan haklı görünüyor.
amon

1
1) C # ' IEqualityComparer<T>da karma tabanlı bir koleksiyona geçebilirsiniz . Siz belirtmeden yoksa, o dayalı bir varsayılan uygulamayı kullanır Object.Equalsve Object.GetHashCode(). 2) EqualsDeğişken bir referans tipini geçersiz kılan IMO nadiren iyi bir fikirdir. Bu şekilde, varsayılan eşitlik oldukça katıdır, ancak bir gelenek üzerinden ihtiyacınız olduğunda daha rahat bir eşitlik kuralını kullanabilirsiniz IEqualityComparer<T>.
CodesInChaos

Yanıtlar:


23

Bu tasarım bazen "Evrensel Eşitlik" olarak bilinir, iki şeyin eşit olup olmadığının evrensel bir özellik olduğu inancıdır.

Dahası, eşitlik iki nesnenin bir özelliğidir , ancak OO'da her zaman tek bir nesnede bir yöntem çağırırsınız ve bu nesne yalnızca bu yöntem çağrısının nasıl ele alınacağına karar verir. Dolayısıyla, eşitlik karşılaştırılan iki nesneden birinin bir özelliği olan Java gibi bir tasarımda, simetri ( a == bb == a) gibi eşitlik gibi bazı temel özellikleri garanti etmek bile mümkün değildir , çünkü ilk durumda yöntem çağrılıyor ave ikinci durumda çağrılıyor bve OO'nun temel ilkeleri nedeniyle, yalnızca bir akarar (ilk durumda) veyab(ikinci durumda), kendisini diğerine eşit olarak kabul edip etmediği kararı. Simetri elde etmenin tek yolu, iki nesnenin birlikte çalışmasını sağlamaktır, ancak eğer yapmazlarsa ... sert şans.

Bir çözüm, eşitliği bir nesnenin özelliği değil, iki nesnenin özelliği veya üçüncü bir nesnenin özelliği yapmak olacaktır. Bu ikinci seçenek aynı zamanda evrensel eşitlik sorununu da çözer, çünkü eşitliği üçüncü bir "bağlam" nesnesinin özelliği yaparsanız, EqualityComparerfarklı bağlamlar için farklı nesneler olduğunu hayal edebilirsiniz .

Bu , Haskell için seçilen tasarımdır, örneğin, Eqtypeclass ile. Aynı zamanda bazı üçüncü taraf Scala kütüphaneleri (örneğin ScalaZ) tarafından seçilen tasarımdır, ancak temel ana bilgisayar platformuyla uyumluluk için evrensel eşitliği kullanan Scala çekirdeği veya standart kütüphanesi değildir.

İlginçtir, ayrıca Java'nın Comparable/ Comparatorarayüzleriyle seçilen tasarımdır . Java tasarımcıları sorunun açıkça farkındaydı, ancak bazı nedenlerden dolayı sadece sipariş için çözdüm, eşitlik (veya karmaşa) için çözemedi.

Yani, soruya gelince

neden bir Comparatorarayüz var ama hayır Hasherve Equator?

Cevap "bilmiyorum" dir. Açıkçası, Java tasarımcıları, sorunun varlığından da anlaşılacağı gibi farkındaydılar Comparator, ancak açıkça eşitlik ve karmaşa için bir sorun olmadığını düşünüyorlardı. Diğer diller ve kütüphaneler farklı seçimler yapar.


7
+1, ancak birden fazla gönderimin olduğu OO dilleri olduğunu unutmayın (Smalltalk, Common Lisp). Dolayısıyla , aşağıdaki cümle için her zaman çok güçlü: "OO'da, her zaman tek bir nesne üzerinde bir yöntem çağırırsınız".
coredump

Aradığım teklifi buldum; JLS 1.0'a göre The methods equals and hashCode are declared for the benefit of hashtables such as java.util.Hashtable, yani her ikisi de equalsve Java devs tarafından yalnızca kendi iyilikleri için kullanılan yöntemler hashCodeolarak sunuldu - UE kavramı ya da şartnamenin herhangi bir yerinde silimar diye bir şey yok ve alıntı benim için yeterince açık; olmasaydı , muhtemelen bir arayüzde olurdu . Bu nedenle, cevabınızın eskiden doğru olduğuna inanırken, şimdi bunun asılsız olduğunu düşünüyorum. ObjectHashtableHashtableequalsComparable
vaxquis

@ JörgWMittag bir yazım hatası oldu, IFTFY. BTW, konuşma clone- aslen bir operatördü , bir yöntem değildi (bakınız Meşe Dili Belirtimi), alıntı: The unary operator clone is applied to an object. (...) The clone operator is normally used inside new to clone the prototype of some class, before applying the initializers (constructors)- anahtar kelime benzeri üç operatör instanceof new clone(bölüm 8.1, operatörler). Sanırım clone/ Cloneablekarışıklığın gerçek (tarihi) sebebi - Cloneabledaha sonraki bir icattı ve mevcut clonekod onunla güçlendirildi.
vaxquis

2
"Bu, Haskell için seçilen tasarımdır, örneğin Eq tip sınıfıyla" Bu doğru, ancak Haskell'in Java'nın yaklaşımı olmasa da , farklı türdeki iki nesnenin hiçbir zaman eşit olmadığını açıkça belirtti . Eşitlik işlemi bu şekilde bir parçası olan türü , (dolayısıyla "typeclass") üçüncü bir bağlam değerine parçası değil.
Jack,

19

Gerçek cevap

neden bir Comparatorarayüz var ama hayır Hasherve Equator?

, Josh Bloch'un izniyle :

Orijinal Java API'leri, kapanış pazar penceresini karşılamak için sıkı bir süre içerisinde çok hızlı bir şekilde yapıldı. Orijinal Java ekibi inanılmaz bir iş çıkardı, ancak API'lerin tümü mükemmel değil.

Sorun diğer benzer konularda, örneğin olduğu gibi, Java'nın tarihinin sadece yatıyor .clone()vs Cloneable.

tl; Dr.

temelde tarihsel nedenlerden dolayı; Geçerli davranış / soyutlama JDK 1.0'da tanıtıldı ve daha sonra sabit değildi çünkü geri kod uyumluluğunu korumakla bunu yapmak neredeyse imkansızdı.


İlk olarak, birkaç tanınmış Java bilgisini özetleyelim:

  1. Java, başlangıçtan günümüze kadar gururla geriye dönük olarak uyumluydu ve eski API'lerin daha yeni sürümlerde hala desteklenmesini gerektiriyordu.
  2. Bu nedenle, JDK 1.0 ile tanıtılan hemen hemen her dil kurgusu günümüze gelmiştir.
  3. Hashtable, .hashCode()& .equals()JDK 1.0’da uygulandı ( Hashtable )
  4. Comparable/ ComparatorJDK 1.2'de ( Karşılaştırılabilir ) tanıtıldı ,

Şimdi, izler:

  1. neredeyse imkansız & PMS için anlamsız olduğunu .hashCode()ve .equals()hala insanlar sonra geriye uyumluluk muhafaza örneğin çünkü superobject'e koymadan daha iyi soyutlama vardır fark ise ayrı arayüzler için her biri 1,2 oranında Java programcısı her biliyordu Objectbunları vardır ve onlar vardı Ayrıca derlenmiş kod (JVM) uyumluluğu sağlamak için fiziksel olarak orada kalmak - ve Objectonları gerçekten uygulayan her alt sınıfa açık bir arabirim eklemek bu karışıklığı bire eşit (sic!) Clonablebir hale getirecektir ( Bloch, Cloneable'in neden örneğin EJ 2'de tartışıldığı gibi. ve SO dahil birçok başka yer,)
  2. Sadece gelecek nesiller için sabit bir WTF kaynağına sahip olmaları için onları orada bıraktılar.

Şimdi, "bunlarla neyin Hashtablevar " diye sorabilirsiniz.

Cevap şudur: hashCode()/equals() 1995 / 1995'te çekirdek Java geliştiricilerinin sözleşmeli ve o kadar da iyi olmayan dil tasarım becerileri.

1996 - 4.3.2 tarihli Java 1.0 Dil Özelinden Alıntı, Sınıf Object, s.41:

Yöntemler equalsve (§21.7) hashCodegibi java.util.Hashtablefinansal tabloların yararı için beyan edilmiştir . Yöntem, eşittir referans, karşılaştırmaya değil, değere dayalı bir nesne eşitliği kavramını tanımlar.

(bu tam ifadenin daha sonraki sürümlerde değiştirildiğini not ediniz , alıntı The method hashCode is very useful, together with the method equals, in hashtables such as java.util.HashMap.yapmak : tarihsel JLS'yi okumadan doğrudan Hashtable- hashCode- equalsbağlantı yapmayı imkansız kılmak !)

Java ekibi iyi bir sözlük tarzı koleksiyon istediklerine karar verdiler ve yarattılar Hashtable(şimdiye kadar iyi fikir), ancak programcının mümkün olduğu kadar az kod / öğrenme eğrisi ile kullanabilmelerini istediler (hata! Geliyor!) - henüz [sonuçta 's JDK 1.0] hiçbir Generics'i yoktu çünkü ve, bu da demek olur her Object içine koymak Hashtableaçıkça bazı arabirimini uygulamak zorunda kalacak (hayır ... ve arayüzleri o zamanlar sadece kendi kuruluşundan hâlâ Comparablebile henüz!) Bunu birçok kişi için caydırıcı hale getirmek - ya Objectda dolaylı olarak bazı karma yöntemlerini uygulamak zorunda kalacaktı .

Açıkçası, yukarıda belirtilen nedenlerden dolayı çözüm 2 ile birlikte gittiler. Evet, şimdi yanlış olduklarını biliyoruz. ... akıllıca olmak çok kolay. kıkırdama

Şimdi, hashCode() ona sahip olan her nesnenin kendine özgü bir equals()metoda sahip olması gerekiyor - bu yüzden equals()de konulması gerektiği çok açıktı Object.

Yana varsayılan geçerli üzerinde bu yöntemlerin uygulamaları ave b Objects gereksiz kalarak esasen faydasız (yapım a.equals(b) eşit etmek a==bve a.hashCode() == b.hashCode() kabaca eşit için a==b, ayrıca sürece hashCodeve / veya equalsyüz binlerce değiştirileceğini veya GC Objectuygulamanızın ömrü boyunca s 1 ) , temel olarak bir yedekleme önlemi olarak ve kullanım kolaylığı için sağlandığını söylemek güvenlidir. Bu, o iyi bilinen gerçeğine olsun tam olarak nasıl olduğunu hep hem geçersiz .equals()& .hashCode()aslında nesneleri karşılaştırarak veya bunları karma-depolamak niyetinde olmadığını. Bunlardan sadece birini diğeri olmadan geçersiz kılmak, kodunuzu bozmanın iyi bir yoludur (kötü karşılaştırma sonuçları veya delice yüksek kova çarpışma değerleri ile) - ve kafanızı etrafa sokmak, yeni başlayanlar için sürekli bir karışıklık ve hata kaynağıdır (görmek için SO kelimesini arayın). Kendiniz için) ve daha deneyimli olanlar için sürekli sıkıntı.

Ayrıca, eşitler & karma kodu içeren C # fırsat daha iyi bir yol bit rağmen notu o, Eric Lippert'ın kendisi onlar Sun Java yıllık yaptığını C # ile hemen hemen aynı hatayı yaptığını belirten C # 'ın kuruluşundan önce :

Fakat neden her nesnenin bir karma tablosuna yerleştirilmek üzere kendi kendine hash yapması gerekiyor? Her nesnenin yapabilmesini gerektiren garip bir şey gibi görünüyor. Bence bugün tip sistemini sıfırdan yeniden tasarlarsak, karmaşanın belki de bir IHashablearayüzle farklı şekilde yapılabileceğini düşünüyorum . Ancak CLR tipi sistem tasarlandığında, genel tipler yoktu ve bu nedenle herhangi bir nesneyi depolayabilmek için genel amaçlı bir karma tablo gerekiyordu.

Tabii ki, 1Object#hashCode hala çarpışabilir, ancak bunu yapmak için biraz çaba harcar , bkz. Http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6809470 ve ayrıntılar için bağlantılı hata raporları; /programming/1381060/hashcode-uniqueness/1381114#1381114 bu konuyu daha derinlemesine ele alıyor.


Yine de sadece Java değil. Çağdaşlarının çoğu (Ruby, Python,…) ve seleflerinin (Smalltalk,…) ve haleflerinin bazılarının Evrensel Eşitlik ve Evrensel Hasa Sahipliği de var (bu bir kelime mi?).
Jörg W Mittag

@ JörgWMittag bkz. Programmers.stackexchange.com/questions/283194/… - Java’da "UE" hakkında katılmıyorum; UE tarihsel olarak hiçbir zaman Objecttasarımının gerçek bir endişesi değildi ; yıpratma oldu.
vaxquis

@vaxquis Buna engel olmak istemem ama önceki yorumum eşzamanlı olarak erişilebilen iki nesnenin aynı (varsayılan) hash koduna sahip olabileceğini gösteriyor.
Monica’ya

1
@vaxquis Tamam. Onu satın alıyorum. Benim endişem, öğrenen birinin bunu görmesi ve eşit vb. Yerine Sistem kodunu kullanarak zeki olduğunu düşünmesidir. Bunu yaparlarsa, nadiren yapılmadığı ve olacağı durumlar dışında yeterince iyi çalışacaktır Sorunu güvenilir bir şekilde çoğaltmanın bir yolu yok.
JimmyJames,

1
Bu kabul edilen cevap olmalıdır, çünkü kabul edilen cevap "bilmiyorum" sonucudur
Phoenix
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.