본문 바로가기

JAVA/Effective Java

item 11) equals를 재정의하려거든 hashCode도 재정의하라

728x90

 

 

1. hashCode 재정의의 필요성

 

다음 코드를 살펴보자

 

package item11;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class PersonTest {

   static class Person {

        String name;
        int age;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public boolean equals(Object o) { //멤버변수 비교하는 equals만 재정의
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Person person = (Person) o;
            return age == person.age && Objects.equals(name, person.name);
        }


    }

    public static void main(String[] args) {
        Person p1 = new Person("부",30);
        Person p2 = new Person("부",30);

  
        Set<Person> set = new HashSet<>();

        set.add(p1);
        set.add(p2);
        
	    System.out.println(p1.equals(p2)); // true
        System.out.println(set.size()); // 2
    }
}

 

나이와 이름을 가지고 있는 사람 객체를 하나 만들고 똑같은 이름과 나이를 가진 두객체를 만들었다.

equals를 재정의했으니 두 객체는 논리적으로 동치 관계이다. 하지만 두 객체를 Set에 넣으면 원소가 1개가 나올거라고 예상하지만 Set의 원소가 2개라는 결과가 나온다. 이는 Person 객체에서 hashCode를 오버라이딩 하지 않아 Object의 기본 hashCode를 사용했고 이로 인해 다른 hashCode 값이 나오기 때문이다. 

다음은 Object API에 나온 hashCode관련 규약이다.

 

1) equals 비교에 사용되는 정보가 변경되지 않ㅇ았다면, hashCode는 항상 같은 값을 변환해야 한다.(일관성) 

2) equals가 두 객체를 같다고 판단했다면(논리적 동치성), 그 두객체의 hashCode값은 같아야 한다.

3) 두 객체가 다르더라도, 그 두객체의 hashCode가 반드시 달라야할 필요는 없다.(다를 수록 HashTable의 성능은 좋아진다. hashCollision)

 

위의 Person객체는 hashCode를 재정의하지 않아 2번을 위배했고 이로 인해 치명적인 오류가 발생했다. 논리적으로 동치 관계에 있는 객체는 hashCode 값도 반드시 같아야한다. 그러기 위해서는 equals에서 비교했던 멤버변수들을 바탕으로 hashCode를 만들어야한다.

 

2. hashCode 작성 규칙

 

- equals가 true 면 , hashCode 값은 서로 같아야한다.

- equals가 false면,  hashCode는 꼭 다를 필요는 없다.(다르면 성능이 좋아지긴한다.)

 

  좋은 해시 함수는 다른 인스턴스에 대해서 다른 해시코드를 같은 인스턴스에 대해서는 같은 해시코드를 반환한다. 중복되는 해시값이 많을수록 LinkedList와 같은 성능이 나올거고 도저히 쓸수 없을 정도로 되기도 한다. 전형적인 hashCode 메소드는 다음과 같다.

 

public static int hashCode(Object a[]) {
        if (a == null)
            return 0;

        int result = 1;

        for (Object element : a)
            result = 31 * result + (element == null ? 0 : element.hashCode());

        return result;
    }

위 hashCode는 Object의 해시코드로 31 이라는 값을 가중치로 곱해서 계속 더해준다. 굳이 31를 쓰는 이유는 

 

다음과 같다고 한다.

 

  • 나쁜 이유는, 소수에 대한 미신이다.
    • 알고리즘 작성에 소수가 도움이 될 거라는 맹신이 있다.
    • 아무 생각 없이 소수를 가져다 쓰는 사람들이 있다.
  • 좋은 이유는, MULT 값으로 짝수를 피해야 하기 때문이다.
    • 짝수를 사용하면 해시코드를 계산할 때 MULT를 곱해가므로 비트의 오른쪽이 0으로 가득찬 결과가 나온다.
    • 가령, 100글자 문자열의 해시코드를 구하게 되면 오른쪽에 0이 99개 붙은 결과가 나오는 것이다.(오버플로우)
    • 따라서 홀수를 사용해야 한다.

 

  equals와 마찬가지로 Lombok, AutoValue같은 프레임워크는 자동으로 hashCode를 만들어준다. 직접 메소드를 정의하지 않고, 이러한 기능들을 사용하여 실수를 안하는 것이 좋다.