1. ภาพรวม
ในการกวดวิชานี้เราจะแนะนำสองวิธีที่ใกล้ชิดอยู่ด้วยกัน: เท่ากับ ()และhashCode () เราจะมุ่งเน้นไปที่ความสัมพันธ์ของพวกเขาซึ่งกันและกันวิธีการลบล้างอย่างถูกต้องและทำไมเราจึงควรลบล้างทั้งสองอย่างหรือไม่
2. เท่ากับ ()
วัตถุกำหนดระดับทั้งเท่ากับ ()และhashCode ()วิธีการ - ซึ่งหมายความว่าทั้งสองวิธีจะถูกกำหนดโดยปริยายในทุกระดับ Java รวมทั้งคนที่เราสร้าง:
class Money { int amount; String currencyCode; }
Money income = new Money(55, "USD"); Money expenses = new Money(55, "USD"); boolean balanced = income.equals(expenses)
เราคาดหวังว่าจะincome.equals (ค่าใช้จ่าย)จะกลับมาจริง แต่ด้วยระดับMoneyในรูปแบบปัจจุบันจะไม่
การปรับใช้ค่าเริ่มต้นของequals ()ในคลาสObjectกล่าวว่าความเท่าเทียมกันนั้นเหมือนกับเอกลักษณ์ของวัตถุ และรายได้และค่าใช้จ่ายเป็นสองกรณีที่แตกต่างกัน
2.1. การลบล้างเท่ากับ ()
มาแทนที่วิธีการequals ()เพื่อที่จะไม่พิจารณาเฉพาะเอกลักษณ์ของวัตถุ แต่ยังรวมถึงค่าของคุณสมบัติที่เกี่ยวข้องทั้งสอง:
@Override public boolean equals(Object o) if (o == this) return true; if (!(o instanceof Money)) return false; Money other = (Money)o; boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
2.2. เท่ากับ ()สัญญา
Java SE กำหนดสัญญาที่การดำเนินการตามวิธีequals () ของเราจะต้องเป็นไปตาม ส่วนใหญ่เป็นเกณฑ์สามัญสำนึก วิธีการเท่ากับ ()ต้องเป็น:
- สะท้อนกลับ : วัตถุต้องเท่ากับตัวเอง
- สมมาตร : x.equals (y)ต้องส่งคืนผลลัพธ์เดียวกันกับy.equals (x)
- สกรรมกริยา : ถ้าx.equals (y)และy.equals (z)แล้วก็x.equals (z)
- สอดคล้องกัน : ค่าของequals ()ควรเปลี่ยนก็ต่อเมื่อคุณสมบัติที่มีอยู่ในเท่ากับ ()เปลี่ยนแปลง (ไม่อนุญาตให้สุ่ม)
เราสามารถค้นหาเกณฑ์ที่แน่นอนใน Java SE Docs สำหรับคลาสObject
2.3. ละเมิดเท่ากับ ()สมมาตรกับมรดก
หากเกณฑ์สำหรับequals ()เป็นสามัญสำนึกเช่นนั้นเราจะละเมิดได้อย่างไร? ดีการละเมิดเกิดขึ้นบ่อยที่สุดถ้าเราขยายชั้นเรียนที่มีแทนที่เท่ากับ () ลองพิจารณาคลาสVoucherที่ขยายระดับMoneyของเรา:
class WrongVoucher extends Money { private String store; @Override public boolean equals(Object o) // other methods }
เมื่อมองแวบแรกคลาสVoucherและการแทนที่สำหรับequals ()ดูเหมือนจะถูกต้อง และทั้งสองเท่ากับ ()วิธีการปฏิบัติตนอย่างถูกต้องตราบใดที่เราเปรียบเทียบเงินเพื่อเงินหรือบัตรกำนัลเพื่อคูปอง แต่จะเกิดอะไรขึ้นถ้าเราเปรียบเทียบวัตถุทั้งสองนี้?
Money cash = new Money(42, "USD"); WrongVoucher voucher = new WrongVoucher(42, "USD", "Amazon"); voucher.equals(cash) => false // As expected. cash.equals(voucher) => true // That's wrong.
ที่ละเมิดเกณฑ์สมมาตรของเท่ากับ ()สัญญา
2.4. การแก้ไขเท่ากับ ()สมมาตรด้วยองค์ประกอบ
เพื่อหลีกเลี่ยงข้อผิดพลาดนี้เราควรให้ความสำคัญกับการถ่ายทอดทางพันธุกรรม
แทนที่จะเป็นคลาสย่อยของMoneyให้สร้างคลาสVoucherด้วยคุณสมบัติMoney :
class Voucher { private Money value; private String store; Voucher(int amount, String currencyCode, String store) { this.value = new Money(amount, currencyCode); this.store = store; } @Override public boolean equals(Object o) // other methods }
และตอนนี้การเท่ากับจะทำงานแบบสมมาตรตามที่สัญญาต้องการ
3. hashCode ()
hashCode ()ส่งคืนจำนวนเต็มแทนอินสแตนซ์ปัจจุบันของคลาส เราควรคำนวณค่านี้ให้สอดคล้องกับนิยามของความเท่าเทียมกันสำหรับคลาส ดังนั้นหากเราลบล้างเมธอดequals ()เราจะต้องแทนที่hashCode ()ด้วย
สำหรับรายละเอียดเพิ่มเติมโปรดดูคู่มือhashCode () ของเรา
3.1. hashCode ()สัญญา
Java SE ยังกำหนดสัญญาสำหรับเมธอดhashCode () การดูอย่างละเอียดจะแสดงให้เห็นว่าhashCode ()และequals ()มีความสัมพันธ์กันมากเพียงใด
เกณฑ์ทั้งสามในสัญญาของhashCode ()กล่าวถึงวิธีการequals ()ในบางวิธี:
- ความสอดคล้องภายใน : ค่าของhashCode ()อาจเปลี่ยนแปลงได้ก็ต่อเมื่อคุณสมบัติที่อยู่ในเท่ากับ ()เปลี่ยนแปลง
- ความสอดคล้องเท่ากัน : ออบเจ็กต์ที่เท่ากันจะต้องส่งคืน hashCode เดียวกัน
- การชนกัน : วัตถุที่ไม่เท่ากันอาจมี hashCode เหมือนกัน
3.2. การละเมิดความสอดคล้องของhashCode ()และเท่ากับ ()
เกณฑ์ที่ 2 ของสัญญาเมธอด hashCode มีผลลัพธ์ที่สำคัญ: ถ้าเราแทนที่ equals () เราจะต้องแทนที่ hashCode () ด้วย และนี่คือไกลโดยการละเมิดที่แพร่หลายมากที่สุดเกี่ยวกับสัญญาของเท่ากับ ()และhashCode ()วิธีการ
ลองดูตัวอย่างดังกล่าว:
class Team { String city; String department; @Override public final boolean equals(Object o) { // implementation } }
ทีมแทนที่ชั้นเดียวเท่ากับ ()แต่ก็ยังปริยายใช้เริ่มต้นใช้งานhashCode ()ตามที่กำหนดไว้ในวัตถุชั้น และส่งคืนhashCode () ที่แตกต่างกันสำหรับทุกอินสแตนซ์ของคลาส สิ่งนี้ละเมิดกฎข้อที่สอง
ตอนนี้ถ้าเราสร้างออบเจ็กต์Team 2 ชิ้นทั้งที่มีเมือง“ นิวยอร์ก” และแผนก“ การตลาด” พวกมันจะเท่ากันแต่จะส่งคืน hashCodes ที่แตกต่างกัน
3.3. คีย์HashMap ที่มีhashCodeไม่สอดคล้องกัน()
But why is the contract violation in our Team class a problem? Well, the trouble starts when some hash-based collections are involved. Let's try to use our Team class as a key of a HashMap:
Map leaders = new HashMap(); leaders.put(new Team("New York", "development"), "Anne"); leaders.put(new Team("Boston", "development"), "Brian"); leaders.put(new Team("Boston", "marketing"), "Charlie"); Team myTeam = new Team("New York", "development"); String myTeamLeader = leaders.get(myTeam);
We would expect myTeamLeader to return “Anne”. But with the current code, it doesn't.
If we want to use instances of the Team class as HashMap keys, we have to override the hashCode() method so that it adheres to the contract: Equal objects return the same hashCode.
Let's see an example implementation:
@Override public final int hashCode() { int result = 17; if (city != null) { result = 31 * result + city.hashCode(); } if (department != null) { result = 31 * result + department.hashCode(); } return result; }
After this change, leaders.get(myTeam) returns “Anne” as expected.
4. When Do We Override equals() and hashCode()?
Generally, we want to override either both of them or neither of them. We've just seen in Section 3 the undesired consequences if we ignore this rule.
Domain-Driven Design can help us decide circumstances when we should leave them be. For entity classes – for objects having an intrinsic identity – the default implementation often makes sense.
However, for value objects, we usually prefer equality based on their properties. Thus want to override equals() and hashCode(). Remember our Money class from Section 2: 55 USD equals 55 USD – even if they're two separate instances.
5. Implementation Helpers
We typically don't write the implementation of these methods by hand. As can be seen, there are quite a few pitfalls.
One common way is to let our IDE generate the equals() and hashCode() methods.
Apache Commons Lang and Google Guava have helper classes in order to simplify writing both methods.
Project Lombok also provides an @EqualsAndHashCode annotation. Note again how equals() and hashCode() “go together” and even have a common annotation.
6. Verifying the Contracts
If we want to check whether our implementations adhere to the Java SE contracts and also to some best practices, we can use the EqualsVerifier library.
Let's add the EqualsVerifier Maven test dependency:
nl.jqno.equalsverifier equalsverifier 3.0.3 test
Let's verify that our Team class follows the equals() and hashCode() contracts:
@Test public void equalsHashCodeContracts() { EqualsVerifier.forClass(Team.class).verify(); }
It's worth noting that EqualsVerifier tests both the equals() and hashCode() methods.
EqualsVerifier is much stricter than the Java SE contract. For example, it makes sure that our methods can't throw a NullPointerException. Also, it enforces that both methods, or the class itself, is final.
It's important to realize that the default configuration of EqualsVerifier allows only immutable fields. This is a stricter check than what the Java SE contract allows. This adheres to a recommendation of Domain-Driven Design to make value objects immutable.
If we find some of the built-in constraints unnecessary, we can add a suppress(Warning.SPECIFIC_WARNING) to our EqualsVerifier call.
7. Conclusion
In this article, we've discussed the equals() and hashCode() contracts. We should remember to:
- Always override hashCode() if we override equals()
- แทนที่เท่ากับ ()และhashCode ()สำหรับออบเจ็กต์ค่า
- ระวังกับดักของการขยายคลาสที่มีการลบล้างเท่ากับ ()และhashCode ()
- พิจารณาใช้ IDE หรือไลบรารีของ บริษัท อื่นในการสร้างเมธอดequals ()และhashCode ()
- พิจารณาใช้ EqualsVerifier เพื่อทดสอบการใช้งานของเรา
ในที่สุดตัวอย่างโค้ดทั้งหมดสามารถพบได้บน GitHub