Ch11 기본 API클래스 - 해시코드
해시코드는 무엇이고, 왜 재정의해서 사용할까?
equals는 논리적으로 '값'을 비교한다.다른 객체라 하더라도 같은 값을 가진다면 equals()는 true를 반환한다.
그리고 해시코드가 있다. Map컬렉션을 구현하는 HashTable과 HashMap은 Key-Value 쌍으로 객체를 보관하는 도구이다.
Key가 가진 hashcode를 통해 value를 더욱 빠르게 찾아낸다.
그렇다면 해시코드란 무엇일까?
최초의 해시코드는, Object클래스의 hashCode()의 리턴값으로 객체의 참조(16진수)를 10진수로 변환시킨 값이다.
각 객체에 대응되는 고유한 정수값을 리턴한다. 따라서 원래의, 오버라이딩 되지 않은 hashcode는 주소값과 밀접한 연관이 있다.
그러나 실제로 많은 클래스에서 hashcode 메소드를 오버라이딩한다.
예시1. String a = "자바", String b = new String("자바")
a.equals(b)의 리턴값은 true이지만, a와 b는 서로 다른 객체를 참조하며(다른 주소값을 가짐) hashcode 또한 다르게 나올 것으로 예상할 수 있다.
그런데 둘은 같은 문자열을 가지기 때문에 사실상 구별이 의미 없으며 같은 객체로 취급되어야 한다.
해시코드가 객체가 가진 해당 값을 빠르게 찾는 역할을 한다는 점을 고려할 때, 같은 value를 가지는 문자열은 해시코드 또한 같은 값을 가져야 할 것이다.
자바에서는 이렇게 같은 문자열을 가지는 객체들을 같은 객체로 취급하기 위해 해시코드를 재정의하고 있다.
또 다른 예시를 살펴본다.
이전 글에서 equals()를 수정해야 하는 경우들을 보았다면, 지금은 hashcode()를 재정의해야 하는 경우이다.
Person p1 = new Person("한소희", 9401012111111)
Person p2 = new Person("한소희", 9401012111111)
두 Person 객체는 같은 이름과 주민번호를 가지고 있다.
같은 이름과 주민번호를 가지고 있는 사람이 둘일 수는 없기 때문에, 두 객체는 동일한 객체로 취급되어야 할 것이다.
(실제로 new로 새로운 객체를 생성하지만, 같은 객체로 취급하여야 하는 경우가 존재한다)
두 객체를 equals 메소드로 비교하면 리턴값은 당연하게도 true다.
하지만, 두 객체의 해시코드는 다르다.
같은 값을 가지는 객체가 같은 해시코드를 가져야 자료 활용이 원활하다는 점에서, hashcode()를 오버라이딩해야 한다.
자바 규약
equals(Object)메소드가 true이면 두 객체의 hashCode 값은 같아야 한다.
equals(Object)메소드가 false이면 두 객체의 hashCode가 꼭 다를 필요는 없다.
하지만 서로 다른 hashCode 값이 나오면 해시 테이블(hash table)의 성능이 향상될 수 있다는 점은 이해하고 있어야 한다.
실행클래스에서 실행 순서를 자세히 살펴본다.
1. Key라는 식별키로 String 타입의 value를 저장하는 HashMap 생성
HashMap<Key, String> hashMap = new HashMap<Key, String>();
2. HashMap의 참조를 통해 put 메소드 실행 (과정에서 Key 클래스의 hashCode( ) 실행)하여 해쉬맵 식별키 new Key(1)에 "홍길동" 값을 저장한다.
hashMap.put(new Key(1), "홍길동"); //아래 예시에서는 Key클래스가 hashCode()를 재정의하여 필드 number을 해시코드로 돌려주고 있다.
즉, 반환되는 해시코드는 Key객체의 고유주소와는 무관한 1이다.
3. HashMap의 참조를 통해 get 메소드 실행 (Key객체 안 재정의된 hashCode()와 equals()가 차례로 실행된다)
String value = hashMap.get(new Key(1)); //get 메소드가 실행될 때 hashCode메소드가 실행되면서 해시코드는 필드 number, 즉 1이된다.
System.out.println(value); //해시코드 1을 통해 HashMap안의 객체를 식별해서 value를 반환한 결과 "홍길동"이 출력된다.
put()
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true); //key를 매개값으로 hash() 메소드를 실행하고
} 그 리턴값, key, value 등의 값을 가진 집합 putVal을 반환한다.
hash()
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //key가 null이면 0, 아니면 h에 key의 해시코드를 저장해서 반환
}
실행클래스에서 hashMap.get(new key(1))한 결과,
get()
public V get(Object key) { //key(1)객체를 가지고 get 메소드 호출
Node<K,V> e; //Node<K,V> e; 추측건대, 식별키와 Value값을 가진 집합을 변수 e에 저장하기로 약속
return (e = getNode(hash(key), key)) == null ? null : e.value; //hash(key)와 key를 가지고 Node를 구하고, 그 value를 리턴
}
그렇다면 hash(key)를 살펴봐야한다. 앞서 본 hash()와 같이 Key객체에서 오버라이드된 메소드이다.
즉, get()이든 put()이든 Key객체에서 오버라이드된 hashCode()를 사용하여 만든 해시코드를 반환하기 때문에, 처음엔 다른 해시코드를 가지고 있다고 하더라도 get, put메소드가 실행되는 과정에서 같은 해시코드를 가지게 된다.
hash()
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //매개값 객체에서 hasCode()를 실행한 결과를 리턴하고 있다.
} //여기서 key.hashCode() : 오버라이딩 된 해시코드 메소드를 쓰고있다.
package p5;
public class Key {
public int number;
public Key(int number) {
this.number = number;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Key) {
Key compareKey = (Key) obj;
System.out.println("이곳은 Key의 equals 부분임");
if(this.number == compareKey.number) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
System.out.println("이곳은 Key의 hashCode 부분임");
//원래는 주소가 리턴되는데, new key(1)일때 key값 1을 리턴
return number;
}
}
package p5;
import java.util.HashMap;
public class KeyExample {
public static void main(String[] args) {
//Key 객체를 식별키로 사용해서 String 값을 저장하는 HashMap 객체 생성
HashMap<Key, String> hashMap = new HashMap<Key, String>();
//식별키 "new Key(1)"로 "홍길동" 저장함
//객체를 생성하면 힙메모리의 주소값이 반환되면서 해쉬코드가 생성된다.
hashMap.put(new Key(1), "홍길동"); //put 값을 넣어줌 : Key의 주소가 해쉬코드맵객체에 들어감
System.out.println("---------------------");
//식별키 "new Key(1)"로 "홍길동"을 읽어옴
String value = hashMap.get(new Key(1)); //get 꺼내와라. key값이 같으면 equals를 수행함.
System.out.println("---------------------");
System.out.println(value);
}
}