본문 바로가기
Java

[EffectiveJava] Item11 equals를 제정의하려거든 hashCode도 재정의하라

by 신입같은 3년차 2020. 12. 21.
728x90

:) equals를 재정의한 클래스 모두 hashCode도 재정의해야한다


위에 적어놓은 소제목 처럼 hashCode를 재정의하지 않는다면 일반 규약을 어기게 되어 HashMap이나 HashSet같은 Collection에서의 원소로 사용될 때 문제를 일으키게 된다.

Object 명세 규약
  • equals에 사용되는 핵심 필드가 변경되지 않았다면 , 실행되는 동안 hashCode메서드는 몇번을 호출해도 항상 같은 hashCode값을 반환햐야한다.

  • equals가 두 객체를 같다고 판단하였다면 두 객체가 가지고있는 hashCode는 같은 값을 반환해야 한다.

  • equals가 두 객체를 다르다고 했더라도, 두 객체의 hashCode가 서로 다른 값을 반환해야할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테일의 성능이 좋아진다.

실제 개발시 equals에 대해 크게 문제가 되는 문제는 2번째 사항이다.
즉, 논리적으로 같은 객체는 같은 hashCode를 반환해야 한다.

2번째 사항에 따른 문제에는 어떤게 있는지 확인해보겠습니다.

public class Item11 {
    public static void main(String[] args) {
        Person p1 = new Person("123456-1234567", "YundleYundle" , 28);
        Person p2 = new Person("123456-1234567", "YundleYundle" , 28);

        System.out.println(p1.equals(p2));
    }
}


class Person {
    private String nationalIdentificationNumber;
    private String name;
    private int age;

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

    @Override
    public boolean equals(Object o) {
        if(!(o instanceof Person)) {
            return false;
        }

        Person person = (Person)o;
        return this.nationalIdentificationNumber.equals(person.nationalIdentificationNumber);
    }
}

다음 코드는 p1객체와 p2객체를 equals를 통해 비교하였고 Person Class에 equals객체를 재구현 하였습니다. 실행 결과는 당연히 nationalIdentificationNumber(주민번호)가 일치 하기 떄문에 true를 반환하게 됩니다.

이렇게만 보면 문제가 전혀 없어보입니다. 하지만 처음 적었던것처럼 hashCode()를 구현하지 않으면 Collection에서 문제가 있다는 예제를 한번 보도록 하겠습니다.


public class Item11 {
    public static void main(String[] args) {
        Person p1 = new Person("123456-1234567", "YundleYundle" , 28);
        Person p2 = new Person("123456-1234567", "YundleYundle" , 28);

        HashMap hashMap = new HashMap();
        hashMap.put(p1, "YundleYundle");

        String o = (String)hashMap.get(p1);
        System.out.println("p1을 통해 get = " + o);

        String o2 = (String)hashMap.get(p2);
        System.out.println("p2를 통해 get = " + o2);
    }
}


class Person {
    private String nationalIdentificationNumber;
    private String name;
    private int age;

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

    @Override
    public boolean equals(Object o) {
        if(!(o instanceof Person)) {
            return false;
        }

        Person person = (Person)o;

        return this.nationalIdentificationNumber.equals(person.nationalIdentificationNumber);

    }
}

위의 예제는 YundleYundle이라는 String 값을 p1객체 key로 하여 저장하였고 HashMap에서 get메서드를 통해 p1 key를 이용하여 값을 조회하였습니다.

그다음에는 p2 key를 이용 하여 값을 조회하였습니다.

일단 출력결과만 말하면 다음과 같은 출력결과를 얻을 수 있습니다.

p1을 통해 get = YundleYundle
p2를 통해 get = null

실제로는 p2 key를 통해 get 을하면 YundleYundle이라는 결과가 나올것 같았지만 null 값이 나왔습니다.

이유는 HashMap은 해시코드가 다른 엔트리 끼리는 동치성 비교를 시도조차 하지 않도록 최적화 되어있기 때문입니다.

( 간단하게 말하면 HashCode가 같지 않다면 equals를 실행조차 하지 않는다. )

그렇다고 hashCode를 다음과 같이 구현하면 안됩니다 . 지금 보는 코드는 절대 사용해서 안되는 hashCode의 예 입니다.

@Override
public int hashCode() {
    return 42;
}

이 코드는 동치인 모든 객체에 대해 똑같은 hashCode를 반환하니 좋다고 볼수 있습니다. 하지만 이 코드는 모든 객체에게 똑같은 hashCode값만 내어주므로 모든 객체게 해시테이블의 버킷 하나에 담겨 마치 연결리스트처럼 동작한다.

이렇게 작성해버리면 평균 수행시간이 늘어나 객체가 많아지면 성능저하의 원인이 됩니다.

좋은 해시 함수라면 서로 다른 인스턴스에 대해 다른 해시코드를 반환해야한다. 이 말은 Object명세의 세 번째 규약이 요구하는 속성입니다.

이상적인 해시 함수는 주어진 인스턴스들을 32비트 정수 범위에 균일하게 분배해야 한다. 이상적인 내용이지만 이 이상은 현실적으로 실현하기는 어렵습니다. 하지만 비슷하게는 구현이 가능하여 Effective Java에서 좋은 hashCode를 작성하는 간단한 요령이 있습니다.

:) 좋은 해시를 만드는 요령


1. int 변수 result를 선언한 후 값c로 초기화 한다. 이때의 c는 해당 객체의 첫번째 핵심 필드를 단계 2.a방식으로 계산한 해시코드이다. ( 이때의 핵심 필드란 equals 비교에 사용되는 필드를 말한다.

2. 해당 필드의 해시코드 c를 계산한다.

  • 기본 타입 필드라면 Type.hashCode(f)를 수행한다. (Type은 해당 기본타입의 박싱 Class를 의미한다)

  • 참조타입이라면그그 참조타입의 hashCode를 호출한다. 다만 필드의 값이 null이라면 0을 사용한다.

  • 필드가 배열이라면 핵심 원소 각각을 별도의 필드처럼 다룬다. 배열에 핵심원소가 하나도 없다면 단순히 상수 0을 사용하고, 모든원소가 핵심 원소라면 Arrays.hashCode를 사용하라.

  • 계산한 해시코드 c로 result를 갱신한다.
    result = 31 * result + c;

3. result를 반환한다

이대로 해당 코드를 구현해보면 다음과 같이 구현할 수 있을것 같습니다.

Person class를 hashCode 재구현을 했다고 하면 다음과 같이 볼 수 있을듯 합니다

public class Item11 {
    public static void main(String[] args) {
        Person p1 = new Person("123456-1234567", "YundleYundle" , 28);
        Person p2 = new Person("123456-1234567", "YundleYundle" , 28);

        HashMap hashMap = new HashMap();
        hashMap.put(p1, "YundleYundle");

        String o = (String)hashMap.get(p1);
        System.out.println("p1을 통해 get = " + o);

        String o2 = (String)hashMap.get(p2);
        System.out.println("p2를 통해 get = " + o2);
    }
}


class Person {
    private String nationalIdentificationNumber;
    private String name;
    private int age;

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

    @Override
    public boolean equals(Object o) {
        if(!(o instanceof Person)) {
            return false;
        }

        Person person = (Person)o;

        return this.nationalIdentificationNumber.equals(person.nationalIdentificationNumber) &&
                this.name.equals(person.name) &&
                this.age == person.age;

    }


    @Override
    public int hashCode() {

        int defaultBit = 31;
        int result = this.nationalIdentificationNumber.hashCode();

        result = defaultBit * this.name.hashCode() + result;
        result = defaultBit * Integer.hashCode(this.age) + result;

        return result;
    }
}

위와 같이 hashCode()를 재구현하고 다시 실행해보면 p1 과 p2로 조회시 YundleYundle 결과가 조회되는것을 확인 할 수 있다.

꼭 equals에 사용한 핵심 필드만 사용하여 hashCode를 구현하길 바랍니다.

728x90
반응형

댓글