equals와 hashcode의 재정의

주노·2023년 4월 14일
0

기술부채 알쓸신잡

목록 보기
7/15
post-thumbnail

서론

아주 예전에 무턱대고 equals를 정의한적이 있었다.

public class Person {
    private final String name;
    private final Integer money;

    public Person(String name, Integer money) {
        this.name = name;
        this.money = money;
    }

    @Override
    public boolean equals(Object obj) {
        return super.equals(obj);
    }
}

위와 같이 equals를 구성했을 때 Object 클래스에 정의되어있는 equals를 사용하게된다.

public boolean equals(Object obj) {
   return (this == obj);
}

해당 equals는 주소값으로 객체가 동일한지만 비교하기 때문에 동일한 인스턴스가 아니면 무조건 다르다는 결과가 나온다.

다양한 문제상황을 보면서 equals와 hashcode를 재정의 해야하는 이유를 생각해보자.

기대하는 상황

public class Main {
    public static void main(String[] args) {
        Person junho = new Person("junho", 1000);
        Person junho2 = new Person("junho", 1000);

        System.out.println(junho.equals(junho2)); // true를 기대
        
        List<Person> people = List.of(junho, junho2);
        long count = people.stream().distinct().count();
        
        System.out.println(count); // 1을 기대
    }
}

위와 같이 junho라는 객체와 junho2라는 객체가 존재한다고 가정해보자.

이름도 같고, 가지고있는 돈도 같다.
이 경우 논리적으로 같기 때문에 두 객체를 같은 객체로 취급하고자 한다.

현재 Person 객체는 다음과 같이 구성되어있다.

public class Person {
    private final String name;
    private final Integer money;

    public Person(String name, Integer money) {
        this.name = name;
        this.money = money;
    }
}

이 상태에서는 위 메소드의 결과가 다음과 같이 나온다.

equals를 재정의하기

자바에서 모든 객체는 암시적으로 Object라는 객체를 상속받게 된다.

Object에는 기본적으로 equals라는 메소드가 정의되어있고 이는 위에서 말했다시피 주소값만 비교한다.

그렇다면 equals를 재정의하여 객체를 비교하면 되는거 아닌가? 단순히 필드값만 비교하면 안될까?

equals를 재정의할 때 지켜야하는 규약을 살펴보며 위 생각이 잘못되었음을 알아보자.

equals 정의 시 지켜야하는 규약

반사성 (Reflexivity)

null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true다.

대칭성 (Symmetry)

null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.

추이성 (Transitivity)

null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y)가 true고 y.equals(z)도 true면 x.equals(z)도 true다.

일관성(Consistency)

null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환해야 한다.

null-아님

null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.

기대하는 값을 만족시키기

위 성질들을 한번 봤으니 자바에서 어떻게 equals를 재정의하는지 확인해보자.

equals 재정의

다음과 같이 Person 클래스에 equals를 재정의했다.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return Objects.equals(name, person.name) && Objects.equals(money, person.money);
}

위에서 기대했던 값이 잘 나오는지 확인해보자.

public class Main {
    public static void main(String[] args) {
        Person junho = new Person("junho", 1000);
        Person junho2 = new Person("junho", 1000);

        System.out.println(junho.equals(junho2)); // true를 기대
        
        List<Person> people = List.of(junho, junho2);
        long count = people.stream().distinct().count();
        
        System.out.println(count); // 1을 기대
    }
}

equals에 대한 기대값은 정상적으로 true가 나온다!

그런데 왜 중복제거를 수행하는 people.stream().distinct().count();는 정상적으로 동작하지 않을까?

stream의 distint()

stream에서 distinct는 어떤 과정으로 동작할까?

메소드를 타고 타고 내려가다보니 중복을 제거하는 과정에서 DistinctOps.makeRef()라는 메소드를 사용하는것을 알 수 있었다.

뭔가 요상하게 생겼고 이해하고 싶어지는 마음이 싹.. 사라진다.
하지만 중복제거를 하는 과정에서 HashSet을 사용한다는 키워드를 얻을 수 있었다!

HashSet의 동등성 비교과정

코드를 분석하며 설명하기에는 너무 말이 길어지므로 간략하게 정리해보자.

HashMap, hashSet등과 같이 hash값을 사용하는 Collection들이 존재한다.

public static void main(String[] args) {
    Person junho = new Person("junho", 1000);
    Person junho2 = new Person("junho", 1000);

    System.out.println(junho.hashCode());
    System.out.println(junho2.hashCode());
}

hashcode를 재정의하지 않았을 때 위와 같이 서로 다른 hashcode 값을 가진다.

hashcode 재정의

다음과 같이 hashcode를 재정의해보자.

@Override
public int hashCode() {
    return Objects.hash(name, money);
}

필드로 가지고있는 name, money를 기준으로 hash값을 생성한다.

public static void main(String[] args) {
    Person junho = new Person("junho", 1000);
    Person junho2 = new Person("junho", 1000);

    System.out.println(junho.hashCode());
    System.out.println(junho2.hashCode());
}

이제 기대값을 모두 만족하는지 확인해보자

public static void main(String[] args) {
    Person junho = new Person("junho", 1000);
    Person junho2 = new Person("junho", 1000);

    System.out.println(junho.equals(junho2)); // true를 기대

    List<Person> people = List.of(junho, junho2);
    long count = people.stream().distinct().count();

    System.out.println(count); // 1을 기대
}

결론

이펙티브 자바 규칙 8 - equals 재정의 / 오버라이드 라는 키워드만 봤을 때는 감이 잘 와닿지 않았었다.

직접 문제를 겪어보니 왜 equals와 hashcode를 재정의해야 하는지 알 수 있었다.

객체간 논리적 동등성을 비교해야한다면 equals, hashcode를 재정의하도록 하자.

Reference

https://meaownworld.tistory.com/entry/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EA%B7%9C%EC%B9%99-8-equals-%EC%9E%AC%EC%A0%95%EC%9D%98-%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%93%9C-%EB%B0%A9%EB%B2%95?category=710953

https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode/

https://kephilab.tistory.com/92

https://sunshot1.tistory.com/7

profile
안녕하세요 😆

0개의 댓글