equals()와 hashCode()
https://github.com/irerin07/interview_prep/blob/master/Java/Java_interview.md#equals-hashcode
equals, hashcode
- Java.lang.object에는 굉장히 중요한 두 메소드가 정의되어 있다. public boolean equals(Object obj)와 public int hashCode().
- equals()는 두 Object가 같은 내용인지 비교한다.
class Money { int amount; String currencyCode; }
Money income = new Money(55, "USD");
Money expenses = new Money(55, "USD");
boolean balanced = income.equals(expenses);
```
-
위의 코드는 true를 반환할 것 같지만 실제로는 false를 반환한다.
- 그 이유는 실제로 구현되어 있는 equals의 코드를 보면 알 수 있다.
-
public boolean equals(Object obj) { return (this == obj); }
- 단순히 ==를 사용해서 비교하고 있다.
- == 연산자는 두개의 비교 대상들이 원시타입(primitive type)인 경우에는 값이 같은지 비교를 하지만 그 외 객체, reference type인 경우엔 주소가 같은지 비교한다.
- 그러니 false가 나올수 밖에...
-
그럼 어떤 방식으로 비교를 해야 할까?
- equals()를 @Override하면 문제를 해결할 수 있다.
-
@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) || (this.currencyCode != null && this.currencyCode.equals(other.currencyCode)); return this.amount == other.amount && currencyCodeEquals; }
- 위와 같이 equals () 메소드를 @Override하면 객체의 정체성만 고려하는것이 아니라 두 가지 관련 속성의 값도 고려하도록 할 수 있다.
-
equals() Contract
- reflexive: 모든 객체 x에 대해 x.equals(x)는 true를 반환해야합니다.
- symmetric: 두 개의 객체 x와 y에 대해 x.equals(y)가 true를 반환한다면 y.equals(x)역시 true를 반환해야합니다.
- transitive: 여러 객체 x, y 및 z의 경우 x.equals(y)가 true를 반환하고 y.equals(z)가 true를 반환하면 x.equals(z)는 true를 반환해야합니다.
- consistent: equals() 메소드 구현에 사용되는 오브젝트 특성이 수정되지 않는 한 x.equals(y)를 여러 번 호출하더라도 항상 동일한 결과가 리턴되어야합니다.
- 객체 클래스 equals() 메소드 구현은 두 레퍼런스가 동일한 객체를 가리키는 경우에만 true를 반환합니다.
-
상속으로 인한 symmetric 위반
-
class Voucher extends Money { private String store; @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Voucher)) return false; Voucher other = (Voucher)o; boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null) || (this.currencyCode != null && this.currencyCode.equals(other.currencyCode)); boolean storeEquals = (this.store == null && other.store == null) || (this.store != null && this.store.equals(other.store)); return this.amount == other.amount && currencyCodeEquals && storeEquals; } // other methods }
위의 코드는 Money를 상속받는 Voucher클래스이다.
단순히 보기에는 아무 문제가 없어보인다. 실제로 Money와 Money를 비교하고, Voucher와 Voucher를 비교하면 아무 문제가 없다
하지만 Money와 Voucher를 비교하면 어떨까?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.
보이는대로 symmetric(두 개의 객체 x와 y에 대해 x.equals(y)가 true를 반환한다면 y.equals(x)역시 true를 반환해야합니다.)을 위반하고 있다.
그럼 어떻게 해야 좋을까? 이 문제를 고치는 방법으로 다음과 같이 코드를 작성할 수 있다.
-
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) { if (o == this) return true; if (!(o instanceof Voucher)) return false; Voucher other = (Voucher) o; boolean valueEquals = (this.value == null && other.value == null) || (this.value != null && this.value.equals(other.value)); boolean storeEquals = (this.store == null && other.store == null) || (this.store != null && this.store.equals(other.store)); return valueEquals && storeEquals; } // other methods }
위의 코드는 Money 클래스를 상속받는 대신 Moeny 클래스의 속성을 사용해서 Voucher클래스를 만든것이다.
이제는 symmetric을 위배하지 않고 잘 작동할것이다.
-
-
hashCode()
-
두 Object가 같은 객체인지 비교한다.
-
해싱 알고리즘을 사용하여 정수값을 반환한다.
-
equals() 메서드를 통해 같은 객체임이 확인 된 두 객체는 반드시 똑같은 hash code를 반환해야 한다.
-
hashCode() contract
- 자바 어플리케이션이 동작할때, 동일한 객체가 호출 될 때마다 hashCode()는 해당 코드에 어떤 변경이 있지 않은 이상 항상 동일한 값을 일관되게 반환해야합니다. 이 값은 다른 자바 어플리케이션에서 실행한 값과 반드시 동일할 필요는 없습니다.
- euqals()메소드를 통해 두 객체가 같은 객체임이 확인된 경우 hashCode()역시 동일한 값을 생성해야 합니다.
- euquals()를 통해 다른 객체임이 확인되었지만 hashCode()에서 반드시 서로 다른 정수값을 반환할 필요는 없습니다. 하지만 서로 다른 객체에 대해 고유한 정수값을 생성하는 것이 해시테이블의 성능향상에 도움이 됩니다.
-
단순하고 실용적이라 할 수 없는 hashCode() 구현체
-
public class User { private long id; private String name; private String email; // standard getters/setters/constructors @Override public int hashCode() { return 1; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null) return false; if (this.getClass() != o.getClass()) return false; User user = (User) o; return id == user.id && (name.equals(user.name) && email.equals(user.email)); } // getters and setters here }
-
위의 코드는 모든 객체를 같은 bucket에 저장하기 때문에 hash Table의 활용도를 0에 가깝게 낮춰버린다.
-
다음은 intellij에서 제공하는 hashCode()이다
-
@Override public int hashCode() { int result = (int) (id ^ (id >>> 32)); result = 31 * result + name.hashCode(); result = 31 * result + email.hashCode(); return result; }
-
더 자세한 내용은 이펙티브 자바에서 확인할 수 있다.
-
https://es.slideshare.net/MukkamalaKamal/joshua-bloch-effect-java-chapter-3
-
-
해시 충돌
- 아무리 효율적인 해싱 알고리즘을 사용한다 하더라도 동일하지 않은 두 객체에 대해 같은 해시코드를 가질수도 있다.
- 그렇기에 두 객체는 해시 테이블 키가 다르더라도 동일한 해시 코드로 인해 같은 bucket을 가리킬것이다.
- 두 개 이상의 객체가 동일한 버킷을 가리키는 경우 linked list에 저장이 된다.
- 이런 경우 해시 테이블은 linked list의 배열이 되며 동일한 해시를 가진 각 객체는 배열의 버킷 인덱스번째(array[bucket_index]) linked list에 추가된다.
- 즉, hashCode()구현은 굉장히 신중하고 효율적으로 이루어져야 한다.
- Java 8은 HashMap 구현에 흥미로운 개선 방법을 내놓았는데, 버킷 크기가 특정 임계 값을 초과하면 연결된 목록이 트리 맵으로 바뀐다. 이를 통해 비관적 O(n) 대신 O(logn)의 시간복잡도(검색)를 달성 할 수 있습니다.
- 아무리 효율적인 해싱 알고리즘을 사용한다 하더라도 동일하지 않은 두 객체에 대해 같은 해시코드를 가질수도 있다.