필요성
프로그램에서 일반적으로 '같다' 라는 의미를 어떻게 나타낼까?
가장 쉬운 방법은 동등 연산자(==)를 사용하는 방법이다.
그러나 이 방법은 원시형에 대해서만 유효하다.
(원시형에 대해서는 나중에 따로 작성해보겠다)
원시형이 아닌 참조형에 대해 동등 연산자를 사용하는 경우는, 내부 값이 아닌 메모리 주소값에 대한 비교이다.
그렇다면 객체가 같다는 것은 어떻게 정의할 수 있을까?
이런 문제를 해결하기 위해 equals & hashCode가 등장했다.
equals란
equals는 모든 객체들의 조상, Object에 정의되어 있는 메소드이다.
필요에 따라 모든 객체들은 이를 오버라이딩해 사용할 수 있다.
equals는 어떤 객체가 다른 객체와 '동등한지'를 비교할 수 있는 수단이다.
여기서 동등하다는 키워드에 주목해야 한다. 동일성과는 다르기 때문이다.
그렇다면 어떻게 사용해야 하는가?
equals 사용 규칙
우선, equals를 오버라이딩 하기 전 몇 가지 규칙에 대해 설명하고자 한다.
equals는 다음과 같은 성질을 모두 만족하도록 정의되어야 하기 때문이다.
여기서 말하는 어떤 객체는, null이 아니라고 가정한다.
- reflexive(반사적인): 어떤 객체 x에 대해 x.equals(x)는 true이다.
- symmetric(대칭적인): 어떤 객체 x, y에 대해 x.equals(y)가 true이면, y.equals(x)도 true이다.
- transitive(전이의): 어떤 객체 x, y, z에 대해 x.equals(y)가 true이고, y.equals(z)가 true이면, x.equals(z)도 true이다.
- consistent(일관적인): 어떤 객체 x, y에 대해 x.equals(y)의 호출이 여러번 반복되더라도 결과는 일관적으로 동일하다.
또한 equals는 기본적으로 오버라이딩 하지 않으면 다음과 같다.
// Object class
public boolean equals(Object obj) {
return (this == obj);
}
따라서 자기 자신과 비교를 하는 경우에만 true를 return한다.
그래서 equals에 대한 오버라이딩이 필요한 것이다. 우리가 원하는 비교는 서로 다른 객체의 비교니까.
HashCode란
그러면, equals만 정의하면 객체 비교가 가능하니까 끝난 것이 아닐까?
아니다!!
equals 공식 문서에는 다음과 같은 문구가 적혀있다.
Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.
즉, equals 메소드가 오버라이딩 되었을 때는,
동일한 객체에는 동일한 해시 코드가 존재해야 한다고 명시하는 hashCode 메소드를 같이 관리(재정의) 해야 한다고 주장한다.
그렇다면 hashCode가 무엇이길래?
쉽게 말해 객체에게 할당된 고유 정수값이라고 생각하면 된다.
아래에서 다루겠지만, hashCode는 hashMap에서 유용하게 사용되므로 중요하다.
hashCode 사용 규칙
일단, 공식 문서를 참고해 hashCode에 대한 몇 가지 규약을 살펴보자.
- 어플리케이션 실행 중에는 같은 객체라면 equals에서 비교되는 정보가 수정되지 않는 한 항상 같은 hashCode값을 반환해야 한다. 그리고 같은 응용 프로그램이라도, 구동 환경에 따라 hashCode 정수 값은 바뀔 수 있음에 유의하자.
- 만약 두 객체가 equals에 의해 같다고 판단되었다면, 두 객체는 반드시 hashCode 반환값도 같아야 한다.
- 두 객체가 equals에 의해 다르다고 판단되었다면, hashCode 값도 반드시 달라야 한다. 개발자는 서로 다른 객체들에 대해 각각 다른 hashCode를 가지는 것은, hashTable의 성능 향상에 유용함을 인지해야 한다.
객체가 동등하다고 판단된다면, hashCode도 재정의되야 한다고 주장한다.
왜 재정의해야 하는지는 3번에서 힌트를 얻을 수 있다. 그건 바로 hashTable 때문이다.
왜 hashCode를 재정의 해야할까
Java에서는 HashMap을 제공한다.
이 HashMap은 Key를 equals + hashCode 값을 기반으로 결정한다.
코드를 통해 살펴보자.
HashMap<String, Integer> test = new HashMap<>();
String teo1 = "teo";
String teo2 = new String("teo");
teo1.equals(teo2); // true
test.put(teo1, 1);
test.put(teo2, 2);
System.out.println(test.get(teo1)); // 2
System.out.println(test.get(teo2)); // 2
teo1과 teo2는 다른 객체다.
엄연히 말하면, 동일성 비교에서는 false, 동등성 비교에선 true이다.
String 객체 내부적으로 equals가 오버라이딩 되어 있기 때문이다.
그렇다는 말은, hashCode 값도 같아야 한다.
따라서 hashCode값을 key로 사용하는 hashMap에서는 위와 같은 결과를 예측할 수 있다.
만약 String에서 equals, hashCode를 오버라이딩 하지 않아서, teo1과 teo2가 hashCode 값이 다르다면 어떨까?
HashMap<String, Integer> test = new HashMap<>();
String teo1 = "teo";
String teo2 = new String("teo");
teo1.equals(teo2); // true
test.put(teo1, 1);
test.put(teo2, 2);
System.out.println(test.get(teo1)); // 1
System.out.println(test.get(teo2)); // 2
위와 같은 결과가 나올 것이다.
앞서 말했듯, hashMap은 hashCode를 기준으로 key를 결정한다.
따라서 hashMap 관점에서는 두 객체가 equals로 같다고 하더라도, hashCode가 다르니 다른 객체로 간주한다.
이것이 hashCode 재정의가 중요한 이유이다!
equals가 아무리 true라고 해도, hashCode 값이 다르면 다른 객체로 간주한다.
마치며
객체가 같다는 기준을 세우는 것은 개발자의 몫이다.
비교할 상황이 있을 때에는 항상 equals & hashCode를 재정의하자.
다만, equals & hashCode를 사용하지 않고도 메소드를 하나 만들면 객체 비교는 가능하다.
그래도 비교를 하는 경우에는 Object 객체에서 명시한 equals & hashCode를 되도록 사용하는 것이 좋을 것이다.
이는 코드를 처음 보는 개발자들에게도 의미를 쉽게 전달할 수 있고,
클라이언트도 해당 객체가 어떤 행동을 할지 쉽게 예측이 가능하기 때문일 것이다.
또한 성능 상의 이점도 존재할 수 있다.
가령 동일한 객체인데, hashCode가 다르다면 HashMap에서는 여러 원소가 생기기 때문이다.
참고자료
'Java' 카테고리의 다른 글
Java: Throwable 소개 & API (0) | 2023.03.07 |
---|---|
Java: enum의 구현방식 알아보기 (8) | 2023.03.06 |
Java: enum 소개 및 API 파헤쳐 보기 (3) | 2023.03.05 |
Java: 동일성과 동등성 (2) | 2023.02.25 |
Java: Varargs와 Heap Pollution (4) | 2023.02.24 |