Java equals () 및 hashCode () 계약

1. 개요

이 튜토리얼에서는 서로 밀접하게 관련된 두 가지 메소드 (equals ()hashCode ())를 소개 합니다. 우리는 서로 간의 관계, 올바르게 재정의하는 방법 및 둘 다 재정의해야하는 이유에 초점을 맞출 것입니다.

2. equals ()

개체 클래스를 정의 모두 등호 ()해시 코드 () 방법 - 이러한 두 가지 방법이 암시 적으로 모든 자바 클래스에 정의되어 있음을 의미, 우리가 만든 것을 포함 :

class Money { int amount; String currencyCode; }
Money income = new Money(55, "USD"); Money expenses = new Money(55, "USD"); boolean balanced = income.equals(expenses)

우리는 earn.equals (expenses)true 를 반환 할 것으로 기대 합니다 . 그러나 현재 형식 의 Money 클래스에서는 그렇지 않습니다.

Object 클래스에서 equals () 의 기본 구현은 동등성이 객체 ID와 동일하다고 말합니다. 그리고 수입지출 은 두 가지 별개의 사례입니다.

2.1. equals () 재정의

객체 ID 만 고려하지 않고 두 관련 속성의 값도 고려하도록 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. equals () 계약

Java SE는 equals () 메소드 구현이 이행해야하는 계약을 정의 합니다. 대부분의 기준은 상식입니다. 등호 () 메소드는해야합니다 :

  • 반사적 : 객체는 자신과 같아야합니다.
  • symmetric : x.equals (y)y.equals (x) 와 동일한 결과를 반환해야합니다.
  • 전이 : x.equals (y)y.equals (z) 이면 x.equals (z)
  • 일관성 :의 값 등호 () 에 포함되어있는 속성 경우에만 변경해야합니다 (등호) 변경 (더 임의성은 허용되지 않습니다)

Object 클래스 에 대한 Java SE 문서에서 정확한 기준을 찾을 수 있습니다 .

2.3. 상속 이있는 equals () 대칭 위반

equals () 의 기준 이 그렇게 상식이라면 어떻게 위반할 수 있을까요? 글쎄요, 우리가 equals ()를 재정의 한 클래스를 확장하면 위반이 가장 자주 발생 합니다. Money 클래스 를 확장 하는 Voucher 클래스를 고려해 보겠습니다 .

class WrongVoucher extends Money { private String store; @Override public boolean equals(Object o)  // other methods }

언뜻보기에 Voucher 클래스와 equals ()에 대한 재정의 가 올바른 것 같습니다. 그리고 우리가 Money to Money 또는 Voucher to 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.

그것은 equals () 계약 의 대칭 기준을 위반합니다 .

2.4. 컴포지션으로 equals () 대칭 고정

이러한 함정을 피하려면 상속보다 구성을 선호 해야합니다 .

Money 를 서브 클래 싱하는 대신 Money 속성이 있는 Voucher 클래스를 만들어 보겠습니다 .

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 () 는 클래스의 현재 인스턴스를 나타내는 정수를 반환합니다. 이 값은 클래스의 동등성 정의에 따라 계산해야합니다. 따라서 우리가 무시할 경우 ) (등호를 방법을, 우리는 또한 오버라이드 (override) 할 필요 해시 코드를 () .

자세한 내용은 hashCode () 가이드를 확인하세요 .

3.1. hashCode () 계약

Java SE는 hashCode () 메서드에 대한 계약도 정의합니다 . 자세히 살펴보면 hashCode ()equals () 가 얼마나 밀접하게 관련되어 있는지 알 수 있습니다.

hashCode () 계약의 세 가지 기준은 모두 equals () 메서드 를 몇 가지 방식으로 언급합니다 .

  • 내부 일관성 : 값 해시 코드 () 만 변경 될 경우에 속성 등호 () 변화
  • 같음 일관성 : 서로 동일한 객체는 동일한 hashCode를 반환해야합니다.
  • 충돌 : 동일하지 않은 객체는 동일한 hashCode를 가질 수 있습니다.

3.2. hashCode ()equals () 의 일관성 위반

hashCode 메서드 계약의 두 번째 기준은 중요한 결과를 가져옵니다. equals ()를 재정의하면 hashCode ()도 재정의해야합니다. 그리고 이것은 equals ()hashCode () 메서드 의 계약과 관련하여 가장 널리 퍼진 위반 입니다.

이러한 예를 살펴 보겠습니다.

class Team { String city; String department; @Override public final boolean equals(Object o) { // implementation } }

클래스 우선은 ()와 동일 하지만, 여전히 암시의 기본 구현 사용하는 해시 코드 () 에 정의로 개체 클래스를. 그리고 이것은 클래스의 모든 인스턴스에 대해 다른 hashCode () 를 반환합니다 . 이것은 두 번째 규칙을 위반합니다.

이제 도시 "뉴욕"및 부서 "마케팅"이있는 두 개의 Team 개체를 만들면 동일 하지만 서로 다른 hashCode를 반환합니다.

3.3. 일치하지 않는 hashCode ()가 있는 HashMap

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()
  • 값 객체에 대해 equals ()hashCode () 재정의
  • equals ()hashCode ()를 재정의 한 클래스 확장의 함정에 유의하십시오.
  • equals ()hashCode () 메소드 생성을 위해 IDE 또는 타사 라이브러리 사용을 고려하십시오.
  • EqualsVerifier를 사용하여 구현을 테스트 해보십시오.

마지막으로 모든 코드 예제는 GitHub에서 찾을 수 있습니다.