본문 바로가기

Java

equals와 hashcode는 왜쓰는거지?

 

오늘은 java Object의 메소드 equals와 hashcode에 대해서 알아보겠다.

 

1. Equals()는 뭐하는 메서드인가?

이름에서부터 눈치챘겠지만 매개변수로 들어오는 객체와 자신의 객체가 같은지 비교하는 기능을 한다.

Object의 메서드 구현부를 살펴보자.

 

 

오.. 별거 없다. 그냥 같은 참조값인지 비교한다. 참조변수 두개를 '==' 연산자로 비교하면 참조값이 같은지의 여부를 반환한다.

이렇게 우리가 만드는 객체에서 equals를 사용하면 기본으로 참조가 같은지의 결과값을 반환하게 된다.

 

하지만 난 참조값 같은지보다 객체의 필드가 모두 같으면 같은 객체로 보고 싶은데?

 

그럼 오버라이딩으로 해당 객체에 맞게 구현하면 된다.

class Person {
    String name;
    int age;

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

    @Override
    public boolean equals(Object o) {
        if (o instanceof Person) {
            Person person = (Person) o;
            return name.equals(person.name) &&
                    age==person.age;
        }

        return false;
    }
}

위의 예는 null 값 비교없이 아주 간단하게 구현했다. 실제라면 이름과 나이가 같더라도 같은 사람일 수 없겠지만 여기선 두개의 값이 같다면 같은 사람이라고 가정을 하겠다. 그럼 Person은 위와 같이 가정에 맞게 오버라이딩을 하면 된다.

이제 person의 equals를 호출했을 때 해당 객체와 인자로 넘어온 객체의 이름, 나이가 같은 객체를 true로 결과를 반환한다.

 

 

2. 그럼 equals()는 언제 쓸까?

equals를 쓰는 시점은 같은 타입의 두 참조변수가 같은지 비교할 때 쓴다. 

 

 그럼 '=='으로 쓰면 되는 거 아닌가? 왜 equals라는 메서드를 쓰는거지?

 

개발을 하다보면 참조하고 있는 주소가 다르더라도 같은 값이라고 인식해야하는 경우가 있다. 위와 같이 객체의 상태가 같을 때나 특정 값이 같을 때 등 상황에 따라 특정 객체의 같다는 기준을 다르게 설정해야할 경우가 equals 메서드의 overriding이 필요한 순간이다. equals는 Object의 기본 메서드이기 때문에 java를 쓰는 사람 입장에서 특별히 객체에 대해서 알고 있지 않더라도 해당객체가 equals 기능을 가지고 있고, 그 기능이 뭐하는 메서드인지 알고 있음은 물론 쓸 수 있다는 것을 인지하고 있을 것이다. 

 

 

3. equals()를 overriding하면 hashcode()도 해줘야한다던데 왜 그런거지?

위의 말은 내가 주워들은 말이다. hashcode가 뭐하는 메서드인지부터 알아야한다.

Collection(HashMap, HashTable, HashSet, LinkedHashSet)이  hashcode를 사용한다. 컬렉션 이름에서도 보이지만 보통 hash 값을 통해 값을 구분하는 용도로 사용하는 것이다.

 

hash가 뭐지?

간단히 말해서 주어진 키에 대해 hash 값을 계산하고, 내부적으로 이 값을 사용하여 데이터를 저장하여 빠르게 데이터를 검색할 수 있게 하는 자료구조이다.

 

equals를 오버라이딩할 때 hashCode도 해줘야 한다는 것은 이런것을 의미하지 않을까 생각한다.

equals로는 동일한 객체라고 나와서 hashCode를 사용하는 Collection에 같은 키에 현재 값을 덮어씌운다고 기대했는데 hash키가 같지 않기 때문에 의도하지 않게 새로운 값이 추가가 되는 경우가 발생하기 때문이다.

 

@Test
void equalsTest() {
  Person p1 = new Person("jin", 27);
  Person p2 = new Person("jin", 27);

  assertEquals(p1, p2);
}

@Test
void hashCodeTest() {
  Person p1 = new Person("jin", 27);
  Person p2 = new Person("jin", 27);

  HashMap<Person, Integer> hashMap = new HashMap<>();
  hashMap.put(p1, 1);
  hashMap.put(p2, 1);

  assertEquals(1, hashMap.size());
}

위처럼 가정하면 equalsTest는 통과할 것이라는 건 안다.

하지만 hashCodeTest는 실패한다. equals를 구현했다고 하더라도 hash 값은 다르게 인식이 되기 때문에 p1과 p2 각각의 키로 값이 저장되어 hashMap의 size는 총 2개가 된다.

 

 

그럼 hashCode 작성은 어떻게 하지?

일단 hashCode의 잘못된 예를 참고하겠다.

 

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

 

이렇게 구현하면 해쉬코드가 같으면 값이 충돌이 될 것이고 hashCode의 효과를 전혀 보지 못하는 상황이 발생한다.

 

그럼 올바른 예는 어떤것들이 있을까?

답은 키값이 같은지 구분할 수 있으며, 다른 키값을 가진 객체와의 충돌할 확률이 적을수록 좋다.

 

음.. 예가 필요하다. Arrays.hashcode()의 구현부는 어떻게 되어있는지 살펴보자.

 

여기서 의문점이 든다. 왜 31을 곱하지?

31을 곱하는 이유는 소수이면서 홀수이기 때문이다.
만일 그 값이 짝수였고, 곱셈 결과가 오버플로되었으면 정보는 사라졌을 것이다. 2로 곱하는 것은 비트를 왼쪽으로 shift하는 것과 같기 때문이다. 소수를 사용하는 이점은 그다지 분명하지 않지만 전통적으로 널리 사용된다. 
31의 좋은 점은 곱셈을 시프트와 뺄셈 조합으로 바꾸면 더 좋은 성능을 낼 수 있어서라고 한다.

 

이 방식을 참고하여 hashcode()를 작성해보겠다.

 

@Override
public int hashCode() {
  int result = 1;

  result = 31 * result + name.hashCode();
  result = 31 * result + age;
  return result;
}

 

 

 

 

 


참고

'Java' 카테고리의 다른 글

JDBC가 뭐지?  (0) 2021.05.30
컬렉션을 쓰는 이유  (0) 2021.05.27
[WS live-study] 8주차: 인터페이스  (0) 2021.02.13
[WS live-study] 7주차: 패키지  (0) 2021.02.07
[WS live-study] 6주차: 상속  (0) 2021.02.04