[Java] equals()와 hashcode()는 언제 재정의해야 하나
[이펙티브 자바]의 아이템 10, 아이템 11에서 equals()와 hashcode() 재정의에 대한 내용을 자세하게 다루고 있다. 추후에 해당 내용에 대한 포스팅을 할 예정이고 여기서는 간단하게 요약해 알아보고자 한다.
equals()
equals()는 두 객체가 논리적으로 같은가를 확인해야하는데 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때 재정의한다.
예를들어, 레스토랑의 주문 관리 애플리케이션을 만들고 있고, 식당의 메뉴를 나타내는 Menu
클래스를 정의하고 있다고 해보자.
public class Menu {
private String name;
private int price;
...
}
Menu
객체의 두 인스턴스가 이름과 가격이 같다면 이 둘은 “논리적으로 같은 것”으로 고려되어야 한다.
public static void main(String[] args) {
Menu chicken1 = new Menu("핫후라이드", 18000);
Menu chicken2 = new Menu("핫후라이드", 18000);
Menu chicken3 = new Menu("양념치킨", 18000);
// 핫후라이드는 핫후라이드다!
System.out.println(chicken1.equals(chicken2)); // false
// 양념치킨과 핫후라이드는 다른 메뉴이다.
System.out.println(chicken1.equals(chicken3)); // false
}
하지만 모든 참조형 타입의 최상위 클래스인 Object
의 equals() 메서드는 주소값을 비교하기 때문에, 이를 재정의하지 않으면 같은 18,000원 짜리 핫후라이드 메뉴라도 equals()의 결과가 false로 판단된다.
java.lang.Object
public class Object {
...
public boolean equals(Object obj) {
return (this == obj); // 주소값 비교(==)를 한다.
}
}
그렇기 때문에 Menu
의 equals() 메서드를 재정의해서 이름과 가격이 같으면 같은 메뉴라는 것을 재정의해줘야한다.
public class Menu {
private String name;
private int price;
...
@Override
public boolean equals(Object other) {
Menu menu = (Menu) other;
return this.name.equals(menu.name) && this.price == menu.price; // 이름과 가격이 같으면 true를 리턴
}
}
❗️ 주의: 실제로는 equals()를 재정의할때는 일반 규약)을 지켜서 재정의해야한다. 예제를 간단하게 하기 위해 위처럼 구현했다. 보통 IDE에서 equals와 hashcode를 재정의하는 기능을 지원하기 때문에 그걸 사용하면된다.
public static void main(String[] args) {
Menu chicken1 = new Menu("핫후라이드", 18000);
Menu chicken2 = new Menu("핫후라이드", 18000);
Menu chicken3 = new Menu("양념치킨", 18000);
// 핫후라이드는 핫후라이드다!
System.out.println(chicken1.equals(chicken2)); // true
// 양념치킨과 핫후라이드는 다른 메뉴이다.
System.out.println(chicken1.equals(chicken3)); // false
}
hashcode()
equals()를 재정의했다면 반드시 hashcode()도 재정의해야한다. 그렇지 않으면 hashcode의 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap
이나 HashSet
같은 컬렉션의 원소로 사용할 때 문제를 일으키게 된다.
…
If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
…
논리적으로 같은 객체는 반드시 같은 해시코드를 반환해야한다.
왜 그런지 코드로 직접 확인해보자
public static void main(String[] args) {
Map<Menu, Integer> map = new HashMap<>();
map.put(new Menu("후라이드", 18000), 10);
map.put(new Menu("생맥주", 4500), 4);
Integer friedCount = map.get(new Menu("후라이드", 18000));
System.out.println(friedCount); // null
}
friedCount
의 값이 10이 될 것으로 기대했지만 null
이 반환되는 것을 확인할 수 있다. 해시맵에 값을 넣을 때 사용된 해시값과 조회할 때 사용되는 해시값이 다르기 때문이다.
IntelliJ를 사용한다면 equals()와 hashcode() 코드를 쉽게 재정의할 수 있다. 클래스 내에 커서를 두고 맥의 경우 Command + N을 누른뒤 equals & hashcode를 선택하고 어떤 필드를 기준으로 하는지 등을 설정해주면 자동으로 재정의 코드가 작성된다.
hashcode를 재정의하고 나면 friedCount
의 값이 10이 나오는 것을 확인할 수 있다.
반대로 hashcode만 재정의하고 equals를 재정의하지 않는 경우에도 문제가 발생할 수 있는데 이는 HashTable 안의 같은 버킷에 들어있는 객체는 equals()를 통해 값비교를 하기 때문이다.
출처: Implementing our Own Hash Table with Separate Chaining in Java - GeeksforGeeks
따라서 equals()와 hashcode()는 항상 같이 재정의해줘야 한다!