 |
|
객체 해시하기
이제 너무 많은 코드를 작성하지 않고도 제대로 동작하는 equals 메서드를 정복했다. 하지만 hashCode 메서드도 함께 재정의해야 함을 잊어서는 안 된다. 이 절에서는 hashCode 메서드를 작성하는 법을 보이겠다.
hashCode 메서드 작성하기
이 hashCode 메서드도 작성 규약이 있다. 하지만 equals 메서드만큼 수학적 형식에 입각하지는 않는다. 그래도 규약을 제대로 따르는 것이 중요하다. 먼저 equals처럼 결과가 일관성이 있어야 한다. 그리고 두 객체 foo와 bar가 있을 때 foo.equals(bar)가 true면 foo와 bar의 hashCode는 모두 같은 값을 돌려 줘야 한다. foo와 bar가 같지 않으면 다른 해시 코드를 돌려줄 필요는 없다. 하지만 Javadoc에 따르면 해당 객체들이 다른 결과를 돌려주는 편이 일반적으로 더 잘 동작할 것이다.
이미 눈치 챘을지도 모르지만 hashCode 메서드는 재정의하지 않으면 겉보기에는 정수 난수(random integer) 같이 보이는 값을 돌려준다. 이는 통상 플랫폼이 객체의 메모리 주소를 정수로 변경해서 돌려주기 때문이다. 그럼에도 불구하고 문서에 따르면 이는 필수 사항이 아니라서 바뀔 수 있다고 되어 있다. 하지만 그와 무관하게 equals 메서드를 재정의하면 hashCode 메서드도 재정의해야 이치에 맞다(비록 있는 그대로 동작하는 것처럼 보이지만 Joshua Bloch가 쓴 Effective Java는 hashCode 메서드를 제대로 구현하는 데 대해 6페이지를 할애했다는 점을 상기하자).
Commons Lang 라이브러리는 EqualsBuilder와 거의 비슷한 HashCodeBuilder 클래스를 제공한다. 하지만 두 프로퍼티를 비교하는 대신 위에 언급한 규약을 따르는 정수를 만들어 내려고 한번에 하나의 프로퍼티를 추가한다.
앞선 Account 객체에 Listing 9에 보인 것처럼 hashCode 메서드를 재정의해 보자.
Listing 9. A 기본 hashCode 메서드
public int hashCode() {
return 0;
}
|
해시 코드를 생성할 때는 비교할 것이 없으므로 HashCodeBuilder를 사용하면 한 줄짜리 코드가 나온다. 여기서 중요한 것은 HashCodeBuilder를 제대로 초기화하는 것이다. HashCodeBuilder의 생성자는 두 개의 int를 받아 해시 코드를 계산하는 데 사용한다. 이 int 두 개는 반드시 홀수여야 한다. HashCodeBuilder의 append 메서드는 하나의 프로퍼티를 받는다. 그리고 이전처럼 호출을 사슬 형태로 연결할 수 있다. 이 호출 사슬의 마지막에는 toHashCode 메서드를 호출한다.
이 정도 설명을 했으니 Listing 10에 보인 것처럼 hashCode 메서드를 구현할 수 있을 것이다.
Listing 10. HashCodeBuilder로 hashCode 메서드 구현하기
public int hashCode() {
return new HashCodeBuilder(11, 21).append(this.id)
.append(this.firstName)
.append(this.lastName)
.append(this.emailAddress)
.append(this.creationDate)
.toHashCode();
}
|
여기서 생성자에 11과 21을 넘겼다. 이는 이 객체를 위해 완전히 임의로 선택한 홀수들이다. 앞서 작성했던 AccountTest를 열어(Listing 2를 보라) 두 객체에 대해 equals가 true를 돌려주면 hashCode가 같은 숫자를 돌려줘야 한다는 규약을 확인하기 위한 간단한 테스트를 추가해 보자. Listing 11에 수정한 테스트를 보였다.
Listing 11. 두 개의 값이 같은 객체에 대해 hashCode 규약 점검하기
import org.junit.Test;
import org.junit.Assert;
import com.acme.app.Account;
import java.util.Date;
public class AccountTest {
@Test
public void verifyAccountEquals(){
Date now = new Date();
Account acct1 = new Account(1, "Andrew", "Glover", "ajg@me.com", now);
Account acct2 = new Account(1, "Andrew", "Glover", "ajg@me.com", now);
Assert.assertTrue(acct1.equals(acct2));
Assert.assertEquals(acct1.hashCode(), acct2.hashCode());
}
}
|
Listing 11에서 값이 같은 객체 두 개의 해시 코드 값이 같음을 확인했다. 다음 Listing 12에서는 값이 다른 객체 두 개는 해시 코드가 다른지를 확인해 보겠다.
Listing 12. 값이 다른 객체 두 개에 대해 hashCode 규약을 확인하기
@Test
public void verifyAccountDifferentHashCodes(){
Date now = new Date();
Account acct1 = new Account(1, "John", "Smith", "john@smith.com", now);
Account acct2 = new Account(2, "Andrew", "Glover", "ajg@me.com", now);
Assert.assertFalse(acct1.equals(acct2));
Assert.assertTrue(acct1.hashCode() != acct2.hashCode());
}
|
궁금해서 hashCode 메서드를 직접 구현해 보길 원한다면 어떻게 해야 할까? 먼저 hashCode 규약을 명심하고 작성하면 Listing 13과 같을 것이다.
Listing 13. 직접 hashCode 구현하기
public int hashCode() {
int result;
result = (int) (id ^ (id >>> 32));
result = 31 * result + (firstName != null ? firstName.hashCode() : 0);
result = 31 * result + (lastName != null ? lastName.hashCode() : 0);
result = 31 * result + (emailAddress != null ? emailAddress.hashCode() : 0);
result = 31 * result + (creationDate != null ? creationDate.hashCode() : 0);
return result;
}
|
말할 필요도 없이 이 코드는 유효한 hashCode 메서드 구현이다. 하지만 여러분이라면 이 두 hashCode 메서드 중 어느 쪽을 택하겠는가? 어느 쪽을 더 빨리 이해할 수 있는가? 여기서 한번 더 언급하지만, Listing 13에서는 많은 조건 논리를 피하기 위해 삼항 연산자를 사용했다. 짐작이 가겠지만 Commons Lang의 HashCodeBuilder는 내부적으로 비슷한 일을 한다. 하지만 여기서 중요한 것은 Commons Lang의 개발자들이 유지 보수하고 테스트한다는 점이다.
EqualsBuilder처럼 HashCodeBuilder도 리플렉션을 이용하는 다른 API를 제공한다. 이를 사용하면 객체의 각 프로퍼티를 append 메서드로 직접 추가할 필요가 없어 Listing 14처럼 hashCode 메서드를 구현할 수 있다.
Listing 14. HashCodeBuilder의 리플렉션 API 사용하기
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
|
이전과 마찬가지로 이 메서드는 내부에서 자바 리플렉션을 이용하기 때문에 보안 설정이 바뀌면 제대로 동작하지 않을 수도 있고 성능이 얼마간 느려질 수 있다.
|