[JAVA] equals와 hashcode
hashCode()
hashCode()는 객체의 해시코드를 반환하는 메서드이다.
Object클래스의 hashCode()는 객체의 주소를 int 형으로 변환해서 반환한다. 그래서 객체마다 다른 값을 가지게 된다.
Object 클래스의 hashCode() 실제 코드를 살펴보자.
public class Object {
...
public native int hashCode();
}
native 키워드가 붙어있는데 이는 이 메서드가 OS의 메서드(C, C++, 어셈블리어로 작성됨)를 호출해서 사용한다는 뜻이다. 즉, native 키워드를 붙이면 OS가 가지고 있는 메서드를 마치 자바가 가지고 있는 것처럼 호출해서 사용할 수 있다는 것이다.(이를 JNI라고 한다. JNI는 자바와 자바가 아닌 언어가 서로 호출해서 사용할 수 있는 기술이다.)
hashCode는 hash를 이용하는 모든 메소드에서 사용된다. 예를 들어 HashMap에서 key값이 겹치면 안되기 떄문에 hash값을 이용해서 비교를 하게 된다.
equals()
equals()는 객체의 동일한지 비교하는 함수이다.
객체 간의 ==은 두 객체의 주소값을 비교하지만 equals()는 두 객체의 내용을 비교하는 메서드이다.
== 동작
==는 같은 위치이면 true를 반환한다.
int x = 1;
int y = 1;
System.out.println(x == y) // true
x와 y는 따로 선언되었지만 int가 primitive 자료형이기 때문에 1이라는 값은 메모리 안에 하나만 저장되고 x와 y는 그 하나의 1을 바라보게 된다. 즉, 위치가 완전히 동일하기 때문에 true를 반환한다. 그러므로 primitive 자료형을 비교할 때에는 ==을 쓰는 것이 맞다.
참고로 String, Charcter 같은 특정 클래스들은 primitive 자료형과 비슷한 성질을 가지기 때문에 ==이 primitive 자료형과 동일하게 작동한다.
equals 탄생 배경
하지만 객체를 비교할 때에는 상황이 다르다. 보통 객체가 생성될 때에는 new 키워드를 통해 생성되고 메모리 영역에 주소값을 가지게 된다. 즉, 객체 안에 내용이 완전히 동일하더라도 다른 주소 값을 가지게 되는 것이다. 그러므로 == 키워드를 쓰면 false가 나오게 된다.
객체 안의 내용을 비교해야하는 필요성이 생겼고 이를 위해 equals() 메소드가 탄생한 것이다.
모든 객체의 부모인 Object 클래스의 equals() 메소드는 ==과 같이 주소값을 비교한다.
public class Object {
...
public boolean equals(Object obj) {
return (this == obj);
}
}
사실 Object클래스 자체는 구체적인 내용이 없기 때문에 비교할 것이 주소값 밖에 없다.
예시1
다음과 같은 클래스가 있다.
class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
}
id와 name이 동일하면 같다고 하고싶다.
그러나 Person 클래스로 두 인스턴스를 생성하고 equals로 비교하면 당연히 false가 나온다.(Object 클래스의 equals를 그대로 사용하기 때문)
그래서 equals를 오버라이딩 해야한다.
class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
return (obj instanceof Person p)
&& p.id == this.id
&& p.name.equals(this.name);
}
}
이렇게 하면 id와 name이 같을 때 true를 반환하게 된다.
equals()만 오버라이딩 했을 때의 문제점
class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
return (obj instanceof Person p)
&& p.id == this.id
&& p.name.equals(this.name);
}
}
위 Person 클래스는 문제점이 있다. 그것은 바로 hash 값이 다르다는 것이다.
즉, HashMap, HashSet 같이 hash 값을 이용하는 경우 문제가 발생할 수 있다. 이 컬렉션들은 key의 클래스에 있는 hashCode() 메서드를 이용해서 고유한 식별 값을 만드는데 Person 클래스의 hashCode()는 오버라이드 되지 않았기 때문에 부모인 Object 클래스의 hashCode()(주소값을 이용하여 hash 값을 만드는)를 이용한다. 그렇기 때문에 모든 인스턴스의 hash 값은 다를 수 밖에 없다.
위의 Person 클래스를 이용해 HashMap을 만들어보자.
HashMap<Person, Integer> map = new HashMap<Person, Integer>();
Person a = new Person(1, "name");
Person b = new Person(1, "name");
map.put(a, 1);
map.put(b, 2);
System.out.println(map.size()); // 2
우리는 a와 b의 내용이 동일하기 때문에 동일한 키로 두 번 넣어줘서 map.size()가 1이 되기를 기대했다. 하지만 HashMap에서는 hash 값이 다르기 때문에 다른 것으로 인식하여 서로 다른 키로 인식한다.
그러므로 우리는 Person에 hashCode()를 오버라이딩 하여 내용이 같으면 같은 해시 값을 반환하게 해야한다.
hashCode()만 오버라이딩 했을 때의 문제점
class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
위와 같이 hashCode만 오버라이딩 했을 때에도 hash를 이용한 컬렉션에서 문제가 생긴다.
보통 hash를 이용한 컬렉션들은 첫번째로 hash 값을 통해 식별한다. 하지만 객체가 완전히 다르더라도 같은 hash 값을 가질 수 있는데 이를 해시 충돌이라고 한다.
이러한 경우 보통 equals를 이용하여 식별을 하기 때문에 문제가 발생한다.
해결
class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
return (obj instanceof Person p)
&& p.id == this.id
&& p.name.equals(this.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
라이브러리를 활용하여 hash 값을 만드는데 여기서 중요한건 equals의 기준이 id와 name이 동일한 것이기 때문에 hashCode()에서도 id와 name을 이용해서 만들어야한다는 것이다!
정리
결국에는 equals나 hashcode를 오버라이딩 할 때에는 혹시나 모르는 상황을 대비해 둘 다 오버라이딩 하는 것이 좋다!(이펙티브 자바에도 나와있음.)