우테코 프리코스를 하는 도중, hashCode()를 오버라이딩하여 사용하는 분들이 많았습니다. 아직 부족한 실력을 가져 왜 hashCode()를 오버라이딩을 하는지 전혀 알 수가 없었습니다. 그래서 궁금해서 hashCode()를 오버라이딩 하는 이유를 찾아 이해하고 정리해보도록 하겠습니다.
hashCode는 객체를 식별하는 하나의 정수값을 말합니다. 일반적으로 각 객체의 주소값을 해시코드로 변환하여, 생성한 객체의 고유한 정수값을 반환합니다. 그렇기 때문에, 동일한 객체에 대해서는 항상 동일한 해시 코드를 반환해야합니다. hashCode() 메서드의 기본 구현은 객체의 내부 주소를 기반으로 한 해시 코드를 반환하도록 되어 있습니다.
equals는 2개의 객체가 동일한지 알려주는 메소드 입니다. 기본적으로 객체의 주소값을 비교하여 같은 메모리 주소를 가진 경우에는 동일하다고 판단하여 true를 반환합니다.
우선, hashCode()를 오버라이딩 하는 이유는 주로 객체를 해시 기반의 자료구조에 저장할 때 사용됩니다. HashMap, HashSet, Hashtable 등의 컬렉션 클래스들은 해시 코드를 사용하여 객체를 저장하고 검색합니다. 객체의 해시 코드를 사용하면 빠른 검색이 가능하므로 성능 향상에 기여할 수 있습니다.
💡 **[ 동일성 vs 동등성 ]** 동일성 : 주소 값을 비교 (==) 동등성 : 객체 내부의 값을 비교 (equals와 hashCode 재정의를 통한 비교)객체의 동등성 비교시 hashCode()와 equals()를 오버라이딩할 필요성이 있다고 합니다. hashCode()를 오버라이딩할 때는 equals() 메서드와 관련하여 일정한 규칙을 지켜야 합니다. equals()는 두 객체를 비교할 때 이 hashCode인 참조값을 사용하여 비교하기 때문입니다. 두 객체가 equals()에서 동등하다면, 두 객체의 hashCode() 값도 같아야 합니다. 이것을 "일관성"이라고 합니다.
class Book {
String title;
int price;
public Book(String title, int price){
this.title = title;
this.price = price;
}
}
public class Main {
public static void main(String[] args) throws IOException {
Book book1 = new Book("자바의 정석", 17_000);
Book book2 = new Book("자바의 정석", 17_000);
System.out.println(book1.equals(book2));
}
}
위와 같이 객체를 생성했다고 했을때, boo1과 boo2는 같은 객체임에도 불구하고 서로 다른 Hash 값(=참조값)을 갖기 때문에 , book1.equals(book2) 결과가 false 로 나올 것 입니다. 만약 우리가 인스턴스 필드가 같을 경우, 같은 객체로 인식하겠다! 라는 규칙을 갖고 있다고 하면, equals()를 재정의 해줘야 합니다.
@Override
public boolean equals(Object o) {
Book book = (Book) o;
return Objects.equals(title, book.title) &&
Objects.equals(price, book.price);
}
하지만, 여기서 단순히 equals()만 오버라이딩하여 객체 내부 값이 같을시 같은 객체라고 반환해주면 문제점시 생깁니다. 바로 equals()에서는 같은 객체라고 true를 반환해도, hashCode()를 확인하면 서로 다른 값이 나오기 때문에, 이는 같은 객체라고 할 수 없습니다. 즉 일관성을 가지지 못합니다.
위와 같은 이유로, 객체의 동등성을 비교하기 위해서 hashCode()와 equals()를 오버라이딩해야하는 것 입니다.
class Book {
String title;
int price;
public Food(String title, int price){
this.title = title;
this.price = price;
@Override
public int hashCode() {
return Objects.hash(title, price);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(title, book.title) &&
Objects.equals(price, book.price);
}
}
위 Book 클래스에 hashCode()과 equals() 메소드를 오버라이딩 하여 사용 해봤습니다. hashCode()에서 인스턴스 필드값으로 해시를 생성해주도록 hashCode()를 오버라이딩줬습니다. 또한 equals() 메소드에서, 자기자신이면 true가 반환 되도록, null 값이거나 같은 클래스 객체가 아니면 false가 반환 되도록 했으며 같은 인스턴스 필드를 가질시 true를 반환하게 equals()를 재정의 해주었습니다.
해시코드를 생성하는 방법은 크게 2가지가 존재합니다. 두 방법 중 더 편한 방법으로 사용하시면 됩니다.
@Override
public int hashCode() {
return Objects.hash(title, price);
}
31을 곱해주는 이유는 31이 홀수이면서 소수이기 때문입니다. 만약 이 숫자가 짝수이고 오버플로가 발생한다면 정보를 잃게 됩니다. 2를 곱하는 것은 시프트 연산과 같은 결과를 내기 때문입니다. 31 숫자는 곱셈을 시프트 연산과 뺄셈으로 대체해 최적화 할 수 있습니다.
@Override
public int hashCode() {
int result = 17; // 초기값 설정
result = 31 * result + price; // 31은 소수로 선택된 상수, 필드들을 곱해줌
result = 31 * result + (title == null ? 0 : title.hashCode());
return result;
}
이제 오버라이딩 한 hashCode()와 equals()를 확인해보도록 합시다.
public class Main {
public static void main(String[] args) throws IOException {
Book book1 = new Book("자바의 정석", 17_000);
Book book2 = new Book("자바의 정석", 17_000);
Book book3 = new Book("이것이 자바다", 18_000);
System.out.println("book1 : " + book1.hashCode());
System.out.println("book2 : " + book2.hashCode());
System.out.println("book3 : " + book3.hashCode());
System.out.println("book1 <-> book2 : " + book1.equals(book2));
System.out.println("book2 <-> book3 : " + book2.equals(book3));
}
}
book1 : -1493617995
book2 : -1493617995
book3 : -1758353748
book1 <-> book2 : true
book2 <-> book3 : false
확인해보니 객체의 내부 값이 같은 book1과 book2는 같은 해시코드를 반환하고, 같은 객체라고 true를 반환하고 있습니다. 또한 book3는 다른 해시코드를 반환하며 다른 객체라고 인식하고 있습니다.
hashCode()가 무엇인지, 왜 필요한지, 어떻게 사용하는지 이야기 해봤습니다. 더불어 hashCode() 의 짝꿍인 equals()도 함께 봤습니다.
public int hashCode() {
// 동일한 hashCode를 반환하는 간단한 예제
return 42;
}