🙋 들어가며
오늘은 자바의 최상위 객체 Object
에 equals()
와 hashCode()
에 대해서 이야기해 보겠습니다.
이번 글을 통해서 각 메서드를 언제, 어떻게 사용하는지 알아보고
왜 두 메서드를 같이 재정의해야 하는지 알아보겠습니다.
🟰 equals()
문자열을 비교할 때 많이 사용하는 친숙한 메서드입니다.
이 메서드를 왜 사용하는지 이해하기 위해서는 동일성
과 동등성
의 개념을 알아야 합니다.
간단하게 정리하면 다음과 같습니다. 그리고 이해를 위한 예시를 작성해 보겠습니다.
- 동일성(identity) : 같은 참조를 가진다.
- 동등성(equality) : 같은 값을 가진다.
String str1 = new String("hejow");
String str2 = new String("hejow");
System.out.println(str1 == str2); // false
System.out.println(str1.equals(str2)); // true
자바에서 동일성을 비교할 때는 ==
을 사용하고, 동등성을 비교할 때는 equals()
를 사용합니다.
예시의 첫 출력에서는 new
키워드로 새로운 주소를 만들었기 때문에 false
가 출력되고,
다음 출력에서는 같은 값인지 비교하므로 true
가 출력이 됩니다.
🤔 Objects.eqauls()
이제 equals()
는 동등성 비교를 위해서 만들어짐을 알았습니다.
비슷하게 생긴 Objects.equals()
와 비교하면서 조금 더 알아보겠습니다.
다음은 각 메서드의 내부 구현입니다.
// Object.equals()
public boolean equals(Object obj) {
return (this == obj);
}
// Objects.equals()
public static boolean equals(@Nullable Object a, @Nullable Object b) {
return (a == b) || (a != null && a.equals(b));
}
Object.equals()
의 내부 구현을 보면 ==
을 사용해서 비교합니다. 즉, 동일성을 비교하고 있습니다.
반면에, Objects.equals()
는 내부적으로 Object.equals()
를 사용하고 있습니다.
그리고 정적(static) 메서드이고 null
을 인자로 받을 수 있다는 차이가 존재합니다.
여기서 우리는 재정의하지 않은 Object.equals()
는 동일성 비교를 하고 있기 때문에,
구현한 객체에서 동등성 비교를 위한 메서드인 equals()
를 올바르게 사용하기 위해서는 재정의를 해야 한다는 것을 알 수 있습니다.
🔐 hashCode()
이번에는 hashCode()
를 언제 사용하는지 알아보겠습니다.
hashCode()
는 이름 그대로 hash code를 반환하는데, 이 코드를 통해서 객체를 식별합니다.
🤔 Hash Code == Memroy Address?
객체를 식별하는 값을 듣자니 메모리 주소가 떠오릅니다. 😯
과연 hash code는 메모리 주소일까요? 간단하게 예시를 통해서 확인해 보겠습니다.
다음과 같이 좌표를 나타내는 Point
라는 객체를 만들었습니다.
그리고 예시를 위해서 equals()
와 hashCode()
가 잘 구현되어 있다고 가정해 보겠습니다.
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// equals(), hashCode()...
}
다음 코드를 동작시키면 어떻게 결과가 나올까요?
Point point1 = new Point(1, 2);
Point point2 = new Point(1, 2);
System.out.println("point1 == point2 : " + (point1 == point2));
System.out.println("point1.equals(point2) : " + point1.equals(point2));
System.out.println("point1.hashCode() == point2.hashCode() : " + (point1.hashCode() == point2.hashCode()));
위 간단한 예시를 통해서 hash code는 메모리 주소가 아니다를 알 수 있습니다.
보다 자세한 내용은 스택 오버플로우를 참고하시기 바랍니다.
🤷♂️ 그래서 언제 사용하나요?
hashCode()
는 hash 기반의 Collection
에서 사용합니다.
우리가 자주 사용하는 HashMap, HashSet, HashTable 등에서 사용합니다.
다음 글에서 자세하게 내부적으로 어떻게 찾고, 값이 만들어지는지 설명하겠습니다.
🧐 왜 같이 재정의할까?
우리가 자주 사용하는 Lombok
에서도 @EqualsAndHashCode
를 제공하고,
인텔리제이의 Generate
기능도 두 메서드를 같이 재정의합니다.
왜 같이 재정의하도록 지원하는 걸까요??
먼저, equals()
를 재정의해야 하는 이유는 앞에서 가볍게 언급했듯이 동일성을 비교를 하기 때문입니다.
따라서 재정의를 하지 않았다면 다음 코드는 false
를 출력하게 됩니다.
Point point1 = new Point(1, 2);
Point point2 = new Point(1, 2);
System.out.println(point1.equals(point2));
다음으로, hashCode()
도 같이 재정의해야 하는 이유는 HashSet
을 사용한 예시를 보면서 이야기해 보겠습니다.
1️⃣ equals()만 재정의
Person
객체는 이름과 나이를 가집니다. 그리고 다음과 같이 equals()
만 재정의했습니다.
public class Person {
private final String name;
private final 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) {
return this.age == person.age && Objects.equals(this.name, person.name);
}
return false;
}
}
그리고 살펴볼 예시는 다음과 같습니다.
Set<Person> people = new HashSet<>();
people.add(new Person("john", 27));
people.add(new Person("john", 27));
System.out.println(people.size()); // 1 ??
Set
이라는 자료구조는 중복이 존재하지 않습니다.
중복을 구분할 수 있도록 equals()
를 구현했으므로 우리가 예상하는 결과는 1 입니다.
하지만 예상과 다르게도 코드에서 출력되는 값은 2 입니다.
2️⃣ hashCode()만 재정의
이번에는 hashCode()
만 재정의한 경우를 보겠습니다.
hash code는 객체를 식별하기 위한 값이므로, 예시를 위해서 같은 값을 리턴하도록 구현했습니다.
@Override
public int hashCode() {
return 0;
}
같은 예시에서, 식별값이 같아 중복이 없을 것 같지만 예상과 다르게 1 을 출력합니다. 대체 왜 그럴까요??
💥 Hash 자료구조의 동작원리
Hash 자료구조는 다음과 같은 순서를 기반으로 동작합니다.
먼저 hashCode()
를 호출해서 같은지 비교하고, 같다면 equals()
를 호출해서 또 한 번 검사합니다.
이전의 예시를 해설하면, equals()
만 재정의한 경우 hashCode()
의 값이 다르므로 HashSet
에 추가되었습니다.
반면에, hashCode()
만 재정의하면 equals()
는 동일성 비교를 하므로 다른 주소값 때문에 HashSet
에 추가되었습니다.
두 메서드를 같이 정의하지 않으면 코드가 어떻게 동작할지 예측할 수 없습니다.
따라서, equals()
를 재정의했다면 반드시 hashCode()
를 재정의해야 합니다.
각 메서드의 내부에 작성되어 있는 문서와 함께 equals()
와 hashCode()
를 조금 더 알아보겠습니다.
📝 equals() 메서드 규약
다음 그림은 equals()
에 작성되어 있는 문서입니다.
정리하면 null
이 아닌 객체 x, y, z에 대해서 다음을 만족해야 합니다.
- 반사성 :
x.equals(x)
는 참이다. - 대칭성 :
x.equals(y)
가 참이면,y.equals(x)
도 참이다. - 추이성 :
x.equals(y)
와y.equals(z)
가 참이면,z.equals(x)
는 참이다. - 일관성 : 반복해서
eqauls()
를 호출해도 같은 결과를 가진다. - Not-Null :
x.equals(null)
은 거짓이다.
equals()를 재정의할 때는 반드시 위의 5가지 규약을 잘 지켜야합니다. 사실 안 지키기가 오히려 더 힘듭니다 😜
📝 hashCode() 메서드 규약
다음 그림은 hashCode()
에 작성되어 있는 문서입니다.
정리하면 다음과 같습니다.
equals()
에 사용되는 정보가 변하지 않았다면hashCode()
도 같은 값을 반환해야 한다.equals()
가 같다고 판단했으면 두 객체의hashCode()
는 같아야 한다.equals()
가 다르다고 판단했어도 두 객체의hashCode()
는 같을 수 있다. (해쉬 충돌)
우리가 지금까지 알아본 내용이 작성되어 있고,
3번 규약을 통해서 같은 hash 값이 항상 같은 값을 가짐을 의미하는 것이 아니다 라는 것까지도 알게 되었습니다. 😋
😋 정리
- eqauls()는 동등성 비교를 위해서 사용한다.
- hashCode()는 객체 식별을 위해서 사용한다.
- eqauls()와 hashCode()는 반드시 같이 재정의해야 한다.
-Reference
https://stackoverflow.com/questions/1961146/memory-address-of-variables-in-java/20680667#20680667
Effective Java: item 15, 16
😋 지극히 개인적인 블로그지만 댓글과 조언은 제 성장에 도움이 됩니다 😋
'언어 공부 > Java' 카테고리의 다른 글
[Java] 올바른 Collection 선택하기 (0) | 2023.08.09 |
---|---|
[Java] Hash란? (feat. Hash Collection) (0) | 2023.07.21 |
[Java] 가변성 (feat. Generic) (0) | 2023.07.05 |
[Java] Optional 바르게 사용하기 (0) | 2023.06.22 |
[Java] 빌드툴 (feat. Gradle) (0) | 2023.06.05 |
댓글