[Java] 객체비교시 왜 equals()와 hashcode() 둘 모두를 재정의해야 하는가?

devdo·2022년 1월 14일
4

Java

목록 보기
31/59

결론부터 말하자면,

객체비교는 그 객체가 VO(Value Object)일 때 한다.
VO에서의 필드값과 해쉬코드값을 동등성, 동일성을 비교해줄 때 오버라이딩(overriding)해서 사용한다.

동등성 : 객체가 주소값이 다르더라도 내용(필드값)이 같다면 같다고 보는 것. ex) equals()
동일성 : 객체가 주소값이 다르면 아무리 같은 내용이더라도 같지 않다고 보는 것. ex) ==

즉, VO 비교를 위해서는 값만 비교하면 되기에 (=동등성 비교를 위해) 원래 equals() 기능(Object기능의 equals()는 동일성 비교를 한다)을 재정의해줘야 한다!

그리고 equals()메서드를 오버라이딩했다면 또다른 문제가 생길 수 있기에 hashCode()도 같이 오버라이딩할 수밖에 없는 이유를 설명해보겠다.


예시를 보자.아래와 같이 Student 클래스가 있다고 하자.

public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

이 때 name과 age 필드값이 똑같은 두 Student 객체를 equals()로 비교하면 어떻게 될까?

    @Test
    public void equalsTest() {

        Student s1 = new Student("dsg", 30);
        Student s2 = new Student("dsg", 30);

        System.out.println("result: " + s1.equals(s2));
    }

결과는 false로 나온다.

두 개의 Student 객체(student1, student2)는 name과 age가 서로 같은 객체이다. 이러면 같은 객체라고 생각할 수 있어 equals()로 비교해도 같다고 생각할 수 있지만, equals() 본래 비교 기능은 그렇지 않다!


원래 equals() 메서드는 값, 주소값 모두 비교하는 기능이다.(==와 동일)

자바의 최상위 클래스인 Object 클래스의 원래 equals()는 아래와 같은 기능으로써 비교 연산자인 == 과 동일한 결과를 리턴한다. 참조값(객체의 주소값)이 같은지를 확인하는 기능이다.

// Object의 기본 원래 equals 메서드
public boolean equals(Object obj) {
    return (this == obj);
}

지금의 자주쓰는 equals()메서드(String에서)는 재정의된 것!

자바에서는 두 객체(VO)를 동등성(객체 필드값 같은지) 비교할 때 equals() 메소드를 사용한다. equals() 메소드는 두 객체를 비교해서 논리적으로 동등하면 true를 리턴하고 그렇지 않으면 false를 리턴한다.

논리적으로 비교하는 것은 둘의 참조값이 아니라 객체 내부 value를 비교하는 것을 의미한다.

이 equals함수를 재정의한 대표적인 예가 String class이다.
String class는 equals() 메소드를 재정의해서 주소값 비교가 아닌 문자열 '값'만을 비교한다.
(참고로 반대로 '주소값'을 비교하는 건 '==' 연산자를 많이 쓴다.)

하지만 VO 같은 경우 안에 각각 변수들까지 비교해야 하기에 equals() 메서드를 더 재정의할 수 밖에 없는 것이다!

예제 코드를 다시 보자.

import java.util.Objects;

public class Student {

    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
//        return super.equals(obj); 기존 Object클래스 구현 내용
        if (this == obj) return true; //같은 객체를 참조하여 참조값이 같은경우 true를 바로 리턴해준다.
        if (obj == null || getClass() != obj.getClass()) return false; //비교하는 객체가 null인지 클래스가 같은지 체크한다.
        Student student = (Student) obj;
        return age == student.age &&
                Objects.equals(name, student.name); //객체 내부의 값들을 비교하여 리턴한다.
    }
}

public class MainTest {

    public static void main(String[] args) {

        Student s1 = new Student("dsg", 24);
        Student s2 = new Student("dsg", 24);

        System.out.println("s1.equals(s2) = " + s1.equals(s2));
        System.out.println("s1.hashCode() = " + s1.hashCode());
        System.out.println("s1.hashCode() = " + s2.hashCode());

    }
}

결과

s1.equals(s2) = true
s1.hashCode() = 1975012498
s1.hashCode() = 1808253012

결과를 보면, 이처럼 값은 같으니 재정의한 equals()이제 true로 리턴해준다. 하지만 다른 문제가 아직 남아 있다. 바로 hashCode() 반환값은 다르기 때문에 아직도 이둘은 완전히 같은 객체라고 보기는 힘들다.

이러니 hashcode() 메서드도 재정의할 수 밖에 없는 것이다.


Java hashcode란

객체 hashcode란 객체를 식별하는 하나의 고유 정수값을 말한다.

Object의 hashCode() 메소드가 바로 객체의 hashcode를 반환한다. hashCode()는 객체의 메모리 번지를 이용해서 hashcode를 만들어 리턴하기 때문에 객체 마다 고유의 다른 값을 가져야 한다.

객체의 값을 동등성 비교시 hashCode()를 오버라이딩할 필요가 있는데, 컬렉션 프레임워크에서 HashSet, HashMap, HashTable같은 경우도 두 객체가 동등한지 비교한다.

기존 코드에 이 메서드 내용만 추가해보자.

    @Override
    public int hashCode() {
//        return super.hashCode();  기존 Object클래스 구현 내용
        return Objects.hash(name, age);
    }

Student 클래스에 hashCode()를 오버라이드 하였다. 출력 결과는?

결과

s1.equals(s2) = true
s1.hashCode() = 3093793
s1.hashCode() = 3093793

객체 필드값이 같으면 hashCode 값도 이제 같아져 equals(), hashcode()를 재정의해 객체 동등성(값만 같은지) 비교가 제대로 되었다!


IntelliJ equals(), hashcode() 만들기

인텔리제이에서는 간단하게 동등성비교만 할 수 있게 재정의한 equals(), hashcode()를 만들 수 있다.

단축키 Alt+ins 를 누르면,

Next 버튼 누르고 마지막에 CREATE 버튼만 클릭해주면 된다.

public class Student {

    private String name;
    private int stuNum;

    public Student(String name, int stuNum) {
        this.name = name;
        this.stuNum = stuNum;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Student)) return false;
        Student student = (Student) o;
        return stuNum == student.stuNum && Objects.equals(name, student.name);
    }

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

✅ 어노테이션 사용 @EqualsAndHashCode, @Data

@EqualsAndHashCode 그리고 이 어노테이션을 포함하는 @Data를 사용하면 알아서 재정의한 equals(), hashcode() 만들어 준다.

intellij로 따로 만드는 것보다 어노테이션을 쓰는 것을 더 추천!

@EqualsAndHashCode
// @Data
public class Student {

    private String name;
    private int stuNum;

    public Student(String name, int stuNum) {
        this.name = name;
        this.stuNum = stuNum;
    }

equals()와 hashcode()를 같이 재정의해야 하는 이유

만약 equals()와 hashcode() 중 하나만 재정의 하면 어떻게 될까?

equals()만 재정의하지 않으면 hashcode()가 만든 해시값을 이용해 객체가 저장된 버킷을 찾을 수는 있지만 해당 객체가 자신과 같은 객체인지 값을 비교할 수 없기 때문에 null을 리턴하게 된다. 따라서 원하는 객체를 찾을 수 없다.

반대로 hashcode()만 재정의 하지 않으면 위 예제를 해보면 알겠지만 같은 값 객체라도 hashcode값이 달라진다. 따라서 HashTable 같은 자료구조에서 그 객체를 put한 상태에서 해당 객체가 저장된 버킷을 찾을 수 없다. (key값을 hashcode로 잡기 때문이다. 버킷은 해쉬테이블에서 데이터가 저장된 곳이다.)

Hash Table에 대한 설명은 다음 링크에서 자세히 볼 수 있다. Hash Table에 대한 설명

이러한 이유로 객체의 정확한 동등, 동일 비교를 위해서는 (특히 Hash 관련 컬렉션 프레임워크를 사용할때!)

Object의 equals() 메소드, hashCode()메소드 둘다 같이 재정의해야 한다.


🔸 참고)
하지만 다른 객체임에도 hashCode()가 같은 경우가 발생할 수 있다.(hash 충돌때문에)

두 객체를 equals() 메소드를 사용하여 비교한 경우 true이면 hashCode()도 true여야 한다.
하지만, equals() 메소드가 false를 리턴했다고 하여 hashCode() 값이 같을 수도 있다는 말이다.

이것은 hashTable일 때 특징이며 이 경우 서로 다른 hashCode()를 가지면 hashTable의 성능을 향상시키는 데 도움이 된다??


결론

객체의 비교는 동등성(값만 같은지) 비교로 이뤄져야 하며, 그러기 위해선 그 객체의 필드까지 동등한지 봐야하기 때문에 euqals(), hashCode() 메서드 둘 모두 재정의해주어야 한다!



참고

https://jisooo.tistory.com/entry/java-hashcode%EC%99%80-equals-%EB%A9%94%EC%84%9C%EB%93%9C%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B3%A0-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C

https://velog.io/@99gaga/Java-equals-%EC%99%80-hashCode%EC%9D%98-%EA%B4%80%EA%B3%84

profile
배운 것을 기록합니다.

0개의 댓글