Java/The Java

[Java] equals()와 hashcode()는 언제 재정의해야 하나

어썸오184 2021. 8. 8. 17:33
728x90
반응형

[이펙티브 자바]의 아이템 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를 선택하고 어떤 필드를 기준으로 하는지 등을 설정해주면 자동으로 재정의 코드가 작성된다.

E0750856-9557-4D08-95A9-A8036BB4C8A6

hashcode를 재정의하고 나면 friedCount의 값이 10이 나오는 것을 확인할 수 있다.

반대로 hashcode만 재정의하고 equals를 재정의하지 않는 경우에도 문제가 발생할 수 있는데 이는 HashTable 안의 같은 버킷에 들어있는 객체는 equals()를 통해 값비교를 하기 때문이다.

출처: Implementing our Own Hash Table with Separate Chaining in Java - GeeksforGeeks

따라서 equals()와 hashcode()는 항상 같이 재정의해줘야 한다!

728x90
반응형