equals() 메서드와 hashcode() 메서드에 대해 살펴보겠습니다.
equals()
우선 equals() 메서드는 두 객체의 값이 같은지를 비교하는 메서드입니다.
그래서 아래 코드의 결과를 보면 다음과 같습니다.
String str1 = "Hi";
String str2 = "Hi";
System.out.println(str1 == str2); // == : 주소 비교 (false)
System.out.println(str1.equals(str2)); // equals : 값 비교 (true)
String 객체는 heap 영역에 생성되는데 각각의 주소가 달라, 단순 비교를 하면 false가 나오게 됩니다.
그렇다면 여기서 String과 같은 문자열이 아니라, 객체 자료형일 경우에는 equals가 어떻게 동작하는지 살펴보겠습니다.
static class Person{
String name;
int age;
Person(String name, int age){
this.name = name;
this.age = age;
}
}
public static void main(String[] args) {
Person p1 = new Person("동동",25);
Person p2 = new Person("동동",25);
System.out.println(p1.equals(p2)); //false가 출력!
}
동일한 내용의 객체를 만들었는데도 false가 출력되는 이유는, 역시 마찬가지로 객체의 주소를 이용하여 비교하기 때문입니다.
public boolean equals(Object obj) {
return (this == obj);
}
이것은 equals 메서드 내부를 들여다보면 이해할 수 있습니다.
내부에서 == 를 이용해 두 객체의 주소를 비교하는 것을 확인할 수 있습니다. 즉 객체를 비교할 때에는 equals()를 써도 결국 ==를 쓰는 것과 다른 것이 없습니다.
+ 그럼 String에도 equals 메서드를 사용했는데, 왜 주소가 아닌 값을 비교했나요?
String에서는 String.class의 재정의된 equals 메서드를 사용한다.
String.class의 equals 메서드는 아래처럼 값을 비교하게끔 구현되어 있는 것을 볼 수 있다.
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
그래서 만약 값을 비교해 true가 나오게 하려면, equals()를 재정의를 해주어야 합니다.
equals() 재정의
객체 자료형을 비교할 때, 객체의 필드 값을 기준으로 비교를 하고 싶으면 equals() 를 오버라이딩해서 재정의를 해주면 됩니다.
@Override
public boolean equals(Object o){
if(o == this) return true;
if(o instanceof Person){
Person person = (Person) o;
if(person.name.equals(this.name)&& person.age == this.age){
return true;
}
}
return false;
}
재정의한 equals는 위와 같습니다.
이것은 아래의 규칙을 따르는 데 코드와 하나씩 비교하며 살펴보겠습니다.
1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
if(o == this) return true;
2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.
if(o instanceof Person){
...
}
return false;
}
3. 입력을 올바른 타입으로 형변환
Person person = (Person) o;
4. 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 하나씩 검사
if(person.name.equals(this.name)&& person.age == this.age){
return true;
}
hashCode()
hashCode는 각 인스턴스의 고유한 값을 말합니다.
public static void main(String[] args) {
Person p1 = new Person("동동",25);
Person p2 = new Person("동동",25);
System.out.println(p1.hashCode()); //1808253012
System.out.println(p2.hashCode()); //589431969
}
주소 값이 객체에 저장된 메모리 위치를 알려주는 것이라면, 해시 코드는 객체의 주소 값으로 만든 고유한 숫자 값입니다. 해싱 기법을 통해 해시 코드를 만든 후 반환되기 때문에, 서로 다른 두 객체는 같은 해시 코드를 가질 수 없게 됩니다.
자바의 hashCode 메서드를 들어가보면, 구현부가 없는 메서드를 볼 수 있습니다. 여기서 native라는 생소한 키워드는 OS가 가지는 메서드를 의미합니다.
이전 JVM을 다룬 포스팅에서 짧게나마 JNI(Java Native Interface)에 대해 살펴보았는데, JNI는 C언어와 같은 로우 레벨 언어로 작성된 네이티브 코드를 JVM에서 실행시키기 위한 것이라고 했습니다. 그 중 하나가 바로 hashCode() 메서드입니다.
equals와 hashCode는 왜 같이 재정의해야 할까
equals를 재정의하면, hashCode도 함께 재정의해라는 말을 한 번쯤은 들어봤을 것입니다.
두 객체가 같다고 equals를 재정의하게 되면, hashCode도 재정의해서 같은 hashCode를 갖게 해야합니다. 만약 hashCode를 재정의 하지 않으면, hash를 사용하는 컬렉션에서 문제가 발생합니다.
코드를 통해 살펴보겠습니다.
class Person{
String name;
int age;
Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o){
if(o == this) return true;
if(o instanceof Person){
Person person = (Person) o;
if(person.name.equals(this.name)&& person.age == this.age){
return true;
}
}
return false;
}
}
우선 Person 클래스에 equals만 재정의를 했습니다.
public static void main(String[] args) {
Set<Person> persons = new HashSet<>();
persons.add(new Person("동동" ,25));
persons.add(new Person("동동" ,25));
System.out.println(persons.size());
}
두 객체의 필드 값이 같아 같은 객체로 나온다면, 1이 출력이 되어야하지만 2가 출력이 됩니다.
이것은 Set 자료 구조가 중복 값을 어떻게 판별하는지를 살펴보면 이해할 수 있습니다.
해시 값을 사용하는 HashSet, HashMap과 같은 컬렉션은 객체가 같은지 비교할 때 위와 같은 과정을 거치게 됩니다.
해쉬 코드와 리턴 값을 확인하고, 같으면 equals 메서드의 리턴 값이 true가 되어야 논리적으로 같은 객체라고 판단하게 되는 것입니다. 그래서 equals 비교도 하기 전에 hashCode의 리턴 값으로 다른 객체라 판단하게 됩니다.
그래서 위 문제를 해결하기 위해 hashCode도 재정의를 함께 해주어야합니다.
hashCode 재정의
class Person{
String name;
int age;
Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o){
if(o == this) return true;
if(o instanceof Person){
Person person = (Person) o;
if(person.name.equals(this.name)&& person.age == this.age){
return true;
}
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
이렇게 hashCode()를 재정의 해주어 name 필드 값을 통해 hashCode가 만들어지게 됩니다.
public static void main(String[] args) {
Person p1 = new Person("동동",25);
Person p2 = new Person("동동",25);
System.out.println(p1.hashCode()); //1473343
System.out.println(p2.hashCode()); //1473343
}
}
동일한 해시코드가 만들어지는 것을 볼 수 있습니다.
Set<Person> persons = new HashSet<>();
persons.add(new Person("동동" ,25));
persons.add(new Person("동동" ,25));
System.out.println(persons.size());
이후 동일하게 Set에 넣고 size를 출력하면, 1이 나오게 됩니다.
참고자료
https://www.baeldung.com/java-hashcode
https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode/
https://inpa.tistory.com/entry/JAVA-%E2%98%95-equals-hashCode-%EB%A9%94%EC%84%9C%EB%93%9C-%EA%B0%9C%EB%85%90-%ED%99%9C%EC%9A%A9-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0
https://donghyeon.dev/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C%EC%9E%90%EB%B0%94/2021/01/04/eqauls%EB%A5%BC-%EC%9E%AC%EC%A0%95%EC%9D%98-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95/
'Java > Java 를 파헤쳐보자' 카테고리의 다른 글
[Java 파헤쳐보기] Java의 가비지 컬렉터(Garbage Collector) (0) | 2023.12.16 |
---|---|
[Java 파헤쳐보기] Java의 바이트 코드 눈으로 확인해보기 (0) | 2023.12.08 |
[Java 파헤쳐보기] JVM을 파헤쳐보자 (0) | 2023.12.01 |
[Java 파헤쳐보기] Generic (제너릭) - PECS (0) | 2023.02.24 |
[Java 파헤쳐보기] Generic (제너릭) (0) | 2023.02.23 |