Java: ทำไมคอลเลกชันถึงรับตัวเปรียบเทียบ แต่ไม่ใช่ (สมมุติ) Hasher and Equator?


25

ปัญหานี้ชัดเจนที่สุดเมื่อคุณมีการใช้อินเทอร์เฟซที่แตกต่างกันและสำหรับวัตถุประสงค์ของคอลเลกชันเฉพาะคุณสนใจเฉพาะมุมมองระดับอินเทอร์เฟซของวัตถุ ตัวอย่างเช่นสมมติว่าคุณมีอินเทอร์เฟซเช่นนี้:

public interface Person {
    int getId();
}

วิธีปกติในการใช้งานhashcode()และequals()ในชั้นเรียนการใช้งานจะมีรหัสดังนี้ในequalsวิธี

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

นี้ทำให้เกิดปัญหาเมื่อคุณผสมการใช้งานในPerson HashMapหากมีHashMapเพียงคนเดียวที่ใส่ใจเกี่ยวกับมุมมองระดับอินเตอร์เฟสของPersonมันก็อาจจบลงด้วยการซ้ำที่แตกต่างกันเฉพาะในชั้นเรียนของพวกเขาใช้งาน

คุณสามารถทำให้กรณีนี้ทำงานโดยใช้วิธีการแบบเดียวกันequals()สำหรับการใช้งานทั้งหมด แต่คุณเสี่ยงต่อการequals()ทำสิ่งที่ผิดในบริบทที่แตกต่างกัน (เช่นการเปรียบเทียบสองPersons ที่สำรองข้อมูลโดยบันทึกฐานข้อมูลที่มีหมายเลขรุ่น)

สัญชาตญาณของฉันบอกฉันว่าควรกำหนดความเสมอภาคต่อการรวบรวมแทนที่จะเป็นแบบต่อชั้นเรียน เมื่อใช้คอลเลกชันที่พึ่งพาการสั่งซื้อคุณสามารถใช้แบบกำหนดเองComparatorเพื่อเลือกลำดับที่ถูกต้องในแต่ละบริบท ไม่มีอะนาล็อกสำหรับคอลเล็กชันที่ใช้แฮช ทำไมนี้

คำถามนี้แตกต่างจาก " Why is .compareTo () ในส่วนติดต่อในขณะที่. equals () อยู่ในคลาสใน Java? " เพราะเกี่ยวข้องกับการใช้งานคอลเลกชัน compareTo()และequals()/ hashcode()ทั้งสองประสบปัญหาสากลเมื่อใช้คอลเลกชัน: คุณไม่สามารถเลือกฟังก์ชั่นการเปรียบเทียบที่แตกต่างกันสำหรับคอลเลกชันที่แตกต่างกัน ดังนั้นสำหรับวัตถุประสงค์ของคำถามนี้ลำดับชั้นการสืบทอดของวัตถุไม่สำคัญเลย สิ่งที่สำคัญคือฟังก์ชันการเปรียบเทียบถูกกำหนดต่อวัตถุหรือต่อการรวบรวม


5
คุณสามารถแนะนำวัตถุห่อหุ้มสำหรับPersonการใช้งานที่คาดหวังequalsและhashCodeพฤติกรรม HashMap<PersonWrapper, V>แล้วคุณต้องการมี นี่คือตัวอย่างหนึ่งที่วิธีการบริสุทธิ์แบบ OOP ไม่สง่างาม: ไม่ใช่การดำเนินการทุกอย่างบนวัตถุที่ทำให้รู้สึกว่าเป็นวิธีการของวัตถุนั้น ทั้งของ Java Objectประเภทคือการรวมกันของความรับผิดชอบที่แตกต่างกัน - เพียงgetClass, finalizeและtoStringวิธีดูเหมือนสมเหตุสมผลจากระยะไกลโดยปฏิบัติที่ดีที่สุดของวันนี้
amon

1
1) ใน C # คุณสามารถส่งผ่านIEqualityComparer<T>ไปยังคอลเลกชันตามแฮ ถ้าคุณไม่ได้ระบุอย่างใดอย่างหนึ่งจะใช้เริ่มต้นใช้งานอยู่บนพื้นฐานและObject.Equals Object.GetHashCode()2) การแทนที่ IMO Equalsบนประเภทการอ้างอิงที่ไม่แน่นอนไม่ค่อยเป็นความคิดที่ดี วิธีการที่เท่าเทียมกันเริ่มต้นเป็นที่เข้มงวดสวย IEqualityComparer<T>แต่คุณสามารถใช้กฎความเท่าเทียมกันที่ผ่อนคลายมากขึ้นเมื่อคุณต้องการมันผ่านทางที่กำหนดเอง
CodesInChaos

2
คำถามเมตาที่เกี่ยวข้อง: คำถามเหล่านี้ซ้ำซ้อนกันหรือไม่

คำตอบ:


23

การออกแบบนี้บางครั้งเรียกว่า "Universal Equality" เป็นความเชื่อที่ว่าสองสิ่งเท่ากันหรือไม่นั้นเป็นสมบัติสากล

ยิ่งไปกว่านั้นความเสมอภาคเป็นคุณสมบัติของวัตถุสองชิ้น แต่ใน OO คุณจะเรียกวิธีการในวัตถุหนึ่งเดียวเสมอและวัตถุนั้นจะต้องตัดสินใจว่าจะจัดการกับวิธีการเรียกวิธีนั้นเพียงอย่างเดียวได้หรือไม่ ดังนั้นในการออกแบบเช่น Java ของที่ความเท่าเทียมกันเป็นคุณสมบัติของหนึ่งในสองวัตถุที่ถูกเปรียบเทียบมันเป็นไปไม่ได้ที่จะรับประกันคุณสมบัติพื้นฐานของความเท่าเทียมกันเช่นสมมาตร ( a == bb == a) เพราะในกรณีแรกวิธีการ กำลังถูกเรียกบนaและในกรณีที่สองมันถูกเรียกใช้bและเนื่องจากหลักการพื้นฐานของ OO มันเป็นการaตัดสินใจของแต่เพียงผู้เดียว(ในกรณีแรก) หรือbการตัดสินใจของ (ในกรณีที่สอง) ไม่ว่าจะพิจารณาตัวเองหรือไม่เท่ากับอีก วิธีเดียวที่จะได้รับความสมมาตรคือการให้วัตถุสองชิ้นร่วมมือกัน แต่ถ้าพวกเขาไม่…โชคร้าย

ทางออกหนึ่งคือการสร้างความเท่าเทียมกันไม่ใช่ทรัพย์สินของวัตถุหนึ่ง แต่เป็นสมบัติของวัตถุสองชิ้นหรือทรัพย์สินของวัตถุที่สาม ตัวเลือกหลังนั้นยังแก้ปัญหาความเสมอภาคสากลเพราะถ้าคุณสร้างความเท่าเทียมกันเป็นสมบัติของวัตถุ "บริบท" ตัวที่สามคุณสามารถจินตนาการได้ว่ามีEqualityComparerวัตถุต่างๆ สำหรับบริบทที่แตกต่างกัน

นี่คือการออกแบบที่เลือกสำหรับ Haskell ตัวอย่างเช่นกับประเภทของงานEqพิมพ์ นอกจากนี้ยังเป็นการออกแบบที่ได้รับการคัดเลือกจากห้องสมุด Scala ของบุคคลที่สาม (ตัวอย่างเช่น ScalaZ) แต่ไม่ใช่ Scala core หรือไลบรารีมาตรฐานซึ่งใช้ความเท่าเทียมกันแบบสากลสำหรับความเข้ากันได้กับแพลตฟอร์มโฮสต์ต้นแบบ

มันเป็นสิ่งที่น่าสนใจและการออกแบบที่เลือกด้วย Java's Comparable/ Comparatorinterfaces ผู้ออกแบบ Java ได้ทราบปัญหาอย่างชัดเจน แต่ด้วยเหตุผลบางอย่างเพียงแก้ไขเพื่อการสั่งซื้อ แต่ไม่ใช่เพื่อความเท่าเทียมกัน (หรือการแปลงแป้นพิมพ์)

ดังนั้นตามคำถาม

ทำไมมีComparatorอินเตอร์เฟซ แต่ไม่มีHasherและEquator?

คำตอบคือ "ฉันไม่รู้" เห็นได้ชัดว่านักออกแบบของ Java ได้ตระหนักถึงปัญหาดังที่เห็นได้จากการมีอยู่จริงComparatorแต่พวกเขาไม่ได้คิดว่ามันเป็นปัญหาสำหรับความเท่าเทียมกันและการคร่ำครวญ ภาษาและห้องสมุดอื่น ๆ มีตัวเลือกต่างกัน


7
+1 แต่โปรดทราบว่ามีภาษา OO ที่มีการแจกจ่ายหลายรายการ (Smalltalk, Common Lisp) ดังนั้นมักจะแข็งแกร่งเกินไปในประโยคต่อไปนี้: "ใน OO คุณมักจะเรียกวิธีการในวัตถุหนึ่งเดียว"
coredump

ฉันพบข้อความที่ฉันต้องการแล้ว ตาม JLS 1.0 The methods equals and hashCode are declared for the benefit of hashtables such as java.util.Hashtableคือทั้งสองอย่างequalsและhashCodeถูกนำมาใช้เป็นObjectวิธีการโดย Java devs แต่เพียงผู้เดียวเพื่อประโยชน์Hashtable- ไม่มีความคิดของ UE หรืออะไรก็ตามที่เป็น silimar ที่ใดก็ได้ในสเป็คและคำพูดนั้นชัดเจนเพียงพอสำหรับฉัน ถ้าไม่ได้สำหรับHashtable, จะได้รับอาจจะอยู่ในอินเตอร์เฟซเหมือนequals Comparableในขณะที่ก่อนหน้านี้ฉันเชื่อว่าคำตอบของคุณถูกต้อง แต่ตอนนี้ฉันคิดว่ามันไม่มีเงื่อนไข
vaxquis

@ JörgWMittagมันเป็นตัวพิมพ์ผิด IFTFY BTW พูดถึงclone- แต่เดิมเป็นผู้ดำเนินการไม่ใช่วิธี (ดูข้อกำหนดภาษาของ Oak) อ้างถึง: 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)- ตัวดำเนินการเหมือนคำหลักทั้งสามคือinstanceof new clone(ส่วน 8.1 ตัวดำเนินการ) ฉันคิดว่านั่นเป็นเหตุผลที่แท้จริง (ในอดีต) ของclone/ Cloneableระเบียบ - Cloneableเป็นเพียงการประดิษฐ์ในภายหลังและcloneรหัสที่มีอยู่ถูกดัดแปลงเพิ่มเติม
vaxquis

2
"นี่คือการออกแบบที่เลือกสำหรับ Haskell ตัวอย่างเช่นกับ Eq typeclass" นี่เป็นความจริง แต่มันก็คุ้มค่าที่จะสังเกตเห็นว่า Haskell ระบุไว้อย่างชัดเจนว่าวัตถุสองชนิดที่แตกต่างกันนั้นไม่เคยเท่าเทียมกัน การดำเนินการที่เท่าเทียมกันจึงเป็นส่วนหนึ่งของประเภท (ดังนั้น "typeclass") ไม่ได้เป็นส่วนหนึ่งของค่าบริบทที่สาม
แจ็ค

19

คำตอบที่แท้จริง

ทำไมมีComparatorอินเตอร์เฟซ แต่ไม่มีHasherและEquator?

คืออ้างความอนุเคราะห์จาก Josh Bloch :

Java API ดั้งเดิมนั้นทำเสร็จเร็วมากภายใต้กำหนดเวลาที่ จำกัด เพื่อพบกับหน้าต่างตลาดปิด ทีม Java ดั้งเดิมทำงานได้ดีมาก แต่ API ทั้งหมดนั้นไม่ได้สมบูรณ์แบบ

ปัญหาอยู่ แต่เพียงผู้เดียวในประวัติศาสตร์ของ Java เช่นเดียวกับเรื่องอื่นที่คล้ายคลึงกันเช่นVS.clone()Cloneable

TL; DR

สำหรับเหตุผลทางประวัติศาสตร์เป็นหลัก; พฤติกรรมปัจจุบัน / สิ่งที่เป็นนามธรรมถูกนำมาใช้ใน JDK 1.0 และไม่ได้รับการแก้ไขในภายหลังเพราะมันแทบเป็นไปไม่ได้ที่จะทำเช่นนั้นกับการรักษาความเข้ากันได้ของรหัสย้อนหลัง


ก่อนอื่นมาสรุปข้อเท็จจริงของ Java ที่รู้จักกันดี:

  1. Java ตั้งแต่เริ่มต้นจนถึงปัจจุบันมีความเข้ากันได้กับระบบย้อนหลังอย่างภาคภูมิใจซึ่งต้องใช้ API ดั้งเดิมที่ยังคงรองรับในเวอร์ชันที่ใหม่กว่า
  2. ด้วยเหตุนี้ภาษาเกือบทุกภาษาที่ใช้กับ JDK 1.0 จะมีชีวิตอยู่จนถึงปัจจุบัน
  3. Hashtable, .hashCode()และ.equals()ถูกนำมาใช้ใน JDK 1.0 ( Hashtable )
  4. Comparable/ Comparatorเป็นที่รู้จักใน JDK 1.2 ( เทียบเคียง )

ตอนนี้มันเป็นดังนี้:

  1. มันเป็นไปไม่ได้จริงและหมดสติที่จะติดตั้งเพิ่มเติม.hashCode()และ.equals()อินเตอร์เฟซที่แตกต่างกันในขณะที่ยังคงรักษาความเข้ากันได้หลังจากที่ผู้คนได้ตระหนักถึงมีแนวคิดที่ดีกว่าวางไว้ใน superobject เพราะเช่นทุกคนโปรแกรม Java 1.2 รู้ว่าทุกคนObjectมีพวกเขาและพวกเขามี จะอยู่ที่นั่นร่างกายเพื่อให้รหัสเรียบเรียง (JVM) เข้ากันได้ยัง - และการเพิ่มอินเตอร์เฟซที่ชัดเจนในทุกObjectระดับชั้นย่อยที่ดำเนินการจริงๆพวกเขาจะทำให้ระเบียบนี้เท่ากับ (sic!) ไปClonableหนึ่ง ( กล่าวถึง Bloch ทำไม Cloneable ครับนอกจากนี้ยังกล่าวถึงในเช่น EJ 2 และสถานที่อื่น ๆ อีกมากมายรวมถึง SO)
  2. พวกเขาทิ้งไว้ที่นั่นเพื่อคนรุ่นต่อไปที่จะมีแหล่งที่มาของ WTF อย่างต่อเนื่อง

ตอนนี้คุณอาจถาม "สิ่งที่Hashtableมีทั้งหมดนี้"?

คำตอบคือ: hashCode()/ equals()contractและทักษะการออกแบบภาษาที่ไม่ดีนักของนักพัฒนา Java core ในปี 1995/1996

อ้างอิงจากJava 1.0 Language Spec, ลงวันที่ 1996 - 4.3.2 The Class Object, p.41:

วิธีการequalsและhashCodeมีการประกาศเพื่อประโยชน์ของjava.util.Hashtableแฮชเทเบิลเช่น(§21.7) วิธีการที่กำหนดเท่ากับความคิดของความเท่าเทียมกันของวัตถุซึ่งขึ้นอยู่กับมูลค่าไม่อ้างอิงการเปรียบเทียบ

(โปรดสังเกตว่าข้อความที่แน่นอนนี้มีการเปลี่ยนแปลงในรุ่นที่ใหม่กว่าเพื่อพูดอ้างว่า: The method hashCode is very useful, together with the method equals, in hashtables such as java.util.HashMap.ทำให้เป็นไปไม่ได้ที่จะทำให้การเชื่อมต่อโดยตรงHashtable- hashCode- equalsการเชื่อมต่อโดยไม่ต้องอ่าน JLS ประวัติศาสตร์!)

ทีม Java ตัดสินใจว่าพวกเขาต้องการคอลเลกชันสไตล์พจนานุกรมที่ดีและพวกเขาสร้างHashtable(ความคิดที่ดีจนถึงตอนนี้) แต่พวกเขาต้องการให้โปรแกรมเมอร์สามารถใช้มันด้วยโค้ด / การเรียนรู้น้อยที่สุดเท่าที่จะทำได้ (อุ๊ปส์! และเนื่องจากยังไม่มีข้อมูลทั่วไป [มันคือ JDK 1.0 หลังจากทั้งหมด] นั่นหมายความว่าทุกสิ่งที่ Objectใส่เข้าไปHashtableจะต้องใช้ส่วนต่อประสานอย่างชัดเจน (และส่วนต่อประสานก็ยังอยู่ในช่วงเริ่มต้น ... Comparableยังไม่ได้!) ทำให้นี้เป็นอุปสรรคที่จะใช้สำหรับจำนวนมาก - หรือObjectจะต้องปริยายใช้วิธีการบางอย่างคร่ำเครียด

เห็นได้ชัดว่าพวกเขาใช้โซลูชัน 2 ด้วยเหตุผลที่กล่าวไว้ข้างต้น ใช่ตอนนี้เรารู้ว่าพวกเขาผิด ... มันง่ายที่จะฉลาดในการเข้าใจถึงปัญหาหลังเหตุการณ์ ซิกซี้

ตอนนี้hashCode() ต้องการให้ทุกวัตถุที่มีมันต้องมีequals()วิธีการที่แตกต่างกัน - ดังนั้นจึงค่อนข้างชัดเจนว่าequals()จะต้องใส่Objectเช่นกัน

ตั้งแต่เริ่มต้นการใช้งานวิธีการเหล่านั้นในที่ถูกต้องaและb Objects เป็นหลักไร้ประโยชน์โดยการซ้ำซ้อน (ทำให้a.equals(b) เท่าเทียมกันไปa==bและa.hashCode() == b.hashCode() เท่ากับไปa==bยังเว้นแต่hashCodeและ / หรือequalsถูกแทนที่หรือคุณ GC หลายร้อยหลายพันObjects ในช่วงวงจรชีวิตของแอพลิเคชันของคุณ1 ) มันปลอดภัยที่จะบอกว่าพวกเขามีให้เป็นมาตรการสำรองและเพื่อความสะดวกในการใช้งาน นี่คือวิธีที่เราได้รับความจริงที่รู้จักกันดีซึ่งจะแทนที่ทั้งสอง.equals()& .hashCode()ถ้าคุณตั้งใจจะเปรียบเทียบวัตถุจริง ๆ หรือเก็บไว้แฮช. การเอาชนะเพียงคนเดียวโดยที่ไม่มีคนอื่นเป็นวิธีที่ดีในการไขรหัสของคุณ (โดยการเปรียบเทียบผลลัพธ์ที่ชั่วร้ายหรือค่าการชนกันของถังสูงอย่างไม่น่าเชื่อ) - การเอาหัวไปรอบ ๆ มันเป็นแหล่งที่มาของความสับสน สำหรับตัวคุณเอง) และสร้างความรำคาญให้กับคนที่มีประสบการณ์มากขึ้น

นอกจากนี้โปรดทราบว่าถึงแม้ว่า C # จะเกี่ยวข้องกับค่าเท่ากับ & hashcode ในทางที่ดีขึ้นเล็กน้อยEric Lippert ระบุว่าพวกเขาทำผิดพลาดเกือบเหมือนกันกับ C # ที่ Sun ทำกับ Java เมื่อหลายปีก่อนที่ C # จะเริ่มต้น :

แต่ทำไมมันเป็นกรณีที่วัตถุทุกชิ้นควรจะสามารถแฮชตัวเองเพื่อแทรกเข้าไปในตารางแฮช? ดูเหมือนว่าเป็นเรื่องแปลกที่ต้องให้ทุกวัตถุสามารถทำได้ ฉันคิดว่าถ้าเราออกแบบระบบพิมพ์ใหม่ตั้งแต่เริ่มต้นวันนี้การแปลงแป้นพิมพ์อาจทำแตกต่างกันอาจมีIHashableส่วนต่อประสาน แต่เมื่อระบบประเภท CLR ได้รับการออกแบบไม่มีประเภททั่วไปดังนั้นตารางแฮชวัตถุประสงค์ทั่วไปจึงจำเป็นต้องเก็บวัตถุใด ๆ

1แน่นอนObject#hashCodeยังสามารถชนกันได้ แต่ต้องใช้ความพยายามเล็กน้อยในการดูที่: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6809470และเชื่อมโยงรายงานข้อผิดพลาดเพื่อดูรายละเอียด; /programming/1381060/hashcode-uniqueness/1381114#1381114ครอบคลุมหัวข้อนี้ในเชิงลึกมากขึ้น


มันไม่ใช่แค่ Java เท่านั้น ผู้ร่วมสมัยจำนวนมาก (Ruby, Python, …) และรุ่นก่อนหน้า (Smalltalk, …) และผู้สืบทอดบางคนก็มี Universal Equality และ Universal Hashability (นั่นคือคำ?)
Jörg W Mittag

@ JörgWMittagดูprogrammers.stackexchange.com/questions/283194/… - ฉันไม่เห็นด้วยกับ "UE" ใน Java; อดีตนักประวัติศาสตร์ไม่เคยกังวลเรื่องObjectการออกแบบมาก่อนเลย ความยุ่งเหยิงคือ
vaxquis

@vaxquis ฉันไม่ต้องการฮาร์ปกับเรื่องนี้ แต่ความคิดเห็นก่อนหน้าของฉันแสดงว่าวัตถุที่สามารถเข้าถึงได้พร้อมกันสองตัวสามารถมีรหัสแฮช (ค่าเริ่มต้น) เดียวกันได้
Reinstate Monica

1
@vaxquis ตกลง ฉันซื้อมัน ความกังวลของฉันคือคนที่เรียนรู้จะเห็นสิ่งนี้และคิดว่าพวกเขาฉลาดโดยใช้ hashcode ของระบบแทนเท่ากับ ฯลฯ หากพวกเขาทำมันก็น่าจะทำงานได้ดีพอยกเว้นเวลาที่หายากมันจะไม่เกิดขึ้นและจะมี ไม่มีวิธีที่จะทำให้เกิดปัญหาได้อย่างน่าเชื่อถือ
JimmyJames

1
นี่ควรเป็นคำตอบที่ได้รับการยอมรับเนื่องจากข้อสรุปที่ได้รับการยอมรับคือ "ฉันไม่รู้"
ฟินิกซ์
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.