자바에서 hashCode를 사용하는 이유
Collection을 사용하는 단순한 연산이 특정 상황에서 비효율적일 수 있기 때문에 hashCode를 사용한다.
예를 들자면
List<String> words = Arrays.asList("Welcome", "to", "junwoo");
if (words.contains("junwoo")) {
System.out.println("junwoo is in the list");
}
"junwoo"가 있는지 확인하는 위의 코드는 List의 사이즈가 커질수록 비효율적인 선형 탐색을 과정을 거친다.
hashCode의 이해
- hashCode는 hashing 알고리즘에 의해 만들어진 Integer 값을 리턴한다.
- 똑같은 Object는 반드시 똑같은 hashCode를 리턴해야 하지만 반대는 필수적이지 않다.
두 번째 경우에 개발자들이 유의해야할 점은 동일하지 않은 Object에 대해 다른 hashCode를 리턴하는 것이 필수적이진 않을 지라도 다른 hashCode를 만드는 것이 hashTable의 성능 향상에 도움이 된다.
hashCode의 구현
구현 1.
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
}
User 클래스는 위에서 명시한 기준을 고수하여 equals()와 hashCode()를 재정의하였다.
다만 hashCode()는 고정된 값을 리턴하도록 설계하였을 뿐이다.
그러나 이러한 방법은 hashTable의 성능을 퇴화시킨다.
왜냐면 모든 Object들이 단일 버킷에 저장되기 때문이다.
구현 2.
현재 hashCode를 개선해보자
User 클래스에 존재하는 모든 필드들을 이용해여 다른 Object에 대해서는 다른 hashCode를 리턴하도록 설계해보자
@Override
public int hashCode() {
return (int) id * name.hashCode() * email.hashCode();
}
일반적으로 현재 개선한 hashCode는 equals()을 일관성을 유지하는 한 합리적인 hashCode 구현이라고 할 수 있다.
구현 3.
해시 코드를 계산하는 데 사용하는 해시 알고리즘이 좋을수록 해시 테이블의 성능이 향상됩니다.
@Override
public int hashCode() {
int hash = 7;
hash = 31 * hash + (int) id;
hash = 31 * hash + (name == null ? 0 : name.hashCode());
hash = 31 * hash + (email == null ? 0 : email.hashCode());
return hash;
}
위의 코드는 두 개의 소수를 이용하여 hashCode를 계산하였다.
그러나 우리는 equals를 구현할 때마다 이러한 복잡한 방식으로 hashCode를 구현해야 할까?
답은 그럴 필요는 없다는 것이다.
왜냐면 우리가 사용하고 있는 IDE에서 커스텀한 hashCode를 정의해주기 때문이다. 또한 7 버전 이후
자바는 안전한 해싱을 위한 hash() 메서드를 제공해 준다.
Objects.hash(name, email)
인텔리제이에서 hashCode 정의
@Override
public int hashCode() {
int result = (int) (id ^ (id >>> 32));
result = 31 * result + name.hashCode();
result = 31 * result + email.hashCode();
return result;
}
이클립스에서 hashCode 정의
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((email == null) ? 0 : email.hashCode());
result = prime * result + (int) (id ^ (id >>> 32));
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
우리가 주목해야 할 점은 hashCode를 구현하는 점에서 31이라는 숫자를 사용한다는 것이다.
이유는 곱셈 보다 bit shift 가 훨씬 빠르기 때문이다.
31 * i == (i << 5) - i
위와 같이 31을 곱하는 것은 정수를 왼쪽으로 5번 shift 해서 빼는 결과랑 같다.
조금 더 자세하게 말하자면
31 이란 32 -1 이다
32는 2의 5 승(2^5)이며 결국 31 = 2^5 -1 이 된다.
따라서 2^5은 왼쪽으로 5번 shift 한 결과와 같다.
결론적으로 32 = (<<< 5) -1 이된다.
쉽게 말하자면 곱셈보다 비트연산의 성능이 훨씬 빠르기에 31을 곱하는 것은 비트연산을 사용해서 계산할 수 있기 때문에 hashCode 는 31값을 선택한 것이다.
References
https://www.baeldung.com/java-hashcode
'Java > Java' 카테고리의 다른 글
(JAVA) 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2020.02.20 |
---|---|
(JAVA) The final keyword in Java (0) | 2020.02.11 |
(JAVA) Immutable Class (0) | 2020.02.09 |
(JAVA) equals에 대해서(4) (0) | 2020.02.09 |
(JAVA) 비트연산(2) (0) | 2020.02.08 |