Object 클래스의 명세를 보면 hashCode의 일반 규약은 아래와 같습니다.
- 어플리케이션 실행 중 같은 객체의 hashCode를 여러 번 호출하는 경우, equals가 사용하는 정보들이 변경되지 않았다면, 언제나 동일한 정수가 반환되어야한다. 다만 프로그램이 종료되었다가 다시 실행되어도 같은 값이 나올 필요는 없다.
- equals(Object) 메소드가 같다고 판정한 두 객체의 hashCode 값은 같아야 한다.
- equals(Object) 메소드가 다르다고 판정한 두 객체의 hashCode 값은 꼭 다를 필요는 없다. 그러나 서로 다른 hashCode 값이 나오면 해시 테이블의 성능이 향상될 수 있다는 점은 이해하고 있어야 한다.
저번 포스트에서 equals 메소드를 재수정 했습니다. 내부의 중요 필드를 기준으로 equals 의 리턴 값을 조정하는 과정을 거쳤는데
이것이 두번째 규약에 위배됩니다.
일단 다음 코드를 보죠. 연습 겸 간단하게 만들어본 예제 코드입니다.
public class Point {
public static void main(String[] args){
String a = "a1가";
String b = "a1가";
String sy = new String("b");
String sx = new String("b");
System.out.println(a.hashCode());
System.out.println(b.hashCode());
System.out.println(sy.hashCode());
System.out.println(sx.hashCode());
System.out.println("Pointxy :" + new Pointxy(1,2).hashCode());
System.out.println("Pointxy :" + new Pointxy(1,2).hashCode());
System.out.println("동일성 검사 :" + new Pointxy(1,2).equals(new Pointxy(1,2)));
}
public static class Pointxy {
int x; int y;
public Pointxy(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Pointxy))
return false;
Pointxy p = (Pointxy) o;
return p.x == x && p.y == y;
}
}
}
hashCode를 만들 때 String 객체는 값을 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 이런 일정한 공식을 내어 만들어냅니다.
결과를 보면 알겠지만, String의 값이 같을 경우 해쉬코드의 값은 같습니다. 설령 다른 객체라 하더라도.
하지만 간단하게 만든 Pointxy 클래스의 경우 내부 필드 값이 같더라도, 다른 값이 나오게 됩니다.
저번 포스트에서 point 객체의 xy 값이 같다면, equals 메소드의 반환을 true로 바꿔주는 작업을 한 바 있습니다.
equals 는 true인데 hashCode 값은 위에서 보다시피 다릅니다.
이 경우 문제가 되는 것입니다.
해쉬 테이블에 넣었을 때 다른 값이 나오거나 null이 나오는 것은 말할 것도 없죠.
그래서 우리는 동일 값일 경우 동일 hashCode()를 리턴하게끔 hashCode를 재정의 해줄 필요가 있습니다.
가장 간단한 방법은 이 방식입니다.
@Override
public int hashCode() {
return 42; // 이렇게 하지 말자!
}
해쉬 코드는 같아지지만, 모든 객체의 값이 같아집니다.
이렇게 한다면 모든 Pointxy는 전부 같은 버킷에 해시되므로, 해시테이블은 연결리스트가 되어버립니다.
선형적으로 이루어야 될 프로그램의 복잡도가 증가되면서 리소스를 어마어마하게 잡아먹습니다.
우리는 필드 값을 참조해서, 그것도 효율적인 해시코드를 만들어내서 리소스를 최대한 적게 사용하게끔 만들어줄 필요가 있습니다.
책에서는 완벽하지는 않지만, 완벽에 가까운 방법 하나를 소개하고 있습니다.
1. 17과 같은 0이 아닌 상수를 result라는 이름의 int 변수에 저장한다.
2. 객체 안에 있는 모든 중요 필드 f에 대해서(equals 메소드가 사용하는 필드) 아래의 절차를 시행한다.
A. 해당 필드에 대한 int 해시코드 c를 계산한다.
- 필드가 boolean이면(f ? 1 : 0 )을 계산.
- 필드가 byte, char, short, int 중 하나이면 (int) f를 계산.
- 필드가 long이면 (int)( f ^ (f >>> 32 )) 를 계산.
- 필드가 float이면 Float.floatToIntBits(f) 를 계산
- 필드가 double이면 Double.doubleToLongBits(f)를 계산하고 그 결과로 얻은 long 값을 위의 iii 절차에 따라 해시 코드로 변환
- 필드가 객체 참조이고 equals 메소드가 해당 필드의 equals 메소드를 재귀적으로 호출하는 경우에는 해당 필드의 hashCode 메소드를 재귀적으로 호출하여 해시코드를 계산한다. 좀 더 복잡한 비교가 필요한 경우에는 해당 필드의 "대표 형태"를 계산한 다음, 대표형태에 대해 hashCode를 호출한다. 필드 값이 null인 경우에는 0을 반환
- 필드가 배열인 경우에는 배열의 각 원소가 별도 필드인 것처럼 계산한다. 즉, 각각의 중요 원소에 대해서 방금 설명한 규칙들을 재귀적으로 적용해 해시코드를 계산하고, 그 결과를 절차 2.B와 같이 결합한다. 배열 내의 모든 원소가 중요하다면 JDK 1.5부터 제공되는 Arrays.hashCode 메소드 가운데 하나를 사용할 수도 있다.
B. 위의 절차 A에서 계산된 해시 코드 c를 result에 다음과 같이 결합한다.
3. result를 반환한다.
4. hashCode 구현이 끝났다면, 동치 관계에 있는 객체의 해시 코드 값이 똑같이 계산되는지 점검하라. 단위 테스트를 작성해서 생각대로 되는지 확인하라. 동치 관계의 객체인데 해시 코드 값이 서로 다르다면 원인을 알아내서 고친다.
위의 방법을 토대로 고쳐본 Pointxy 내부 클래스와, 결과값입니다.
public static class Pointxy {
int x; int y;
public Pointxy(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Pointxy))
return false;
Pointxy p = (Pointxy) o;
return p.x == x && p.y == y;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + x;
result = 31 * result + y;
return result;
}
}
객체가 다른데 해쉬코드도 일치하고, 동일성 검사가
잘 나오는 모습을 확인할 수 있습니다.
이 방법을 사용할 때 중요한 점은 성능 향상을 위해서 중요필드를 빼먹는 행동은 하지 말아야 한다는 점입니다.
그러면 해시 속도는 빨라질지 몰라도, 해시 값의 품질이 좋지 않기 때문에 해시 테이블의 성능을 떨어뜨릴 수 있습니다.
'프로그래밍 > Java' 카테고리의 다른 글
디자인 패턴 - 전략 패턴(Strategy pattern) 예시 / 예제 (0) | 2018.02.06 |
---|---|
이펙티브 자바 규칙 8 - equals 재정의 / 오버라이드 방법 (0) | 2018.02.03 |
이펙티브 자바 규칙 7 - 종료자 사용을 피하라 (0) | 2018.02.02 |