Java

연산자 == /equals() 그리고 equals()와 hashCode()를 같이 재정의해야 하는 이유

자몽포도 2022. 6. 18. 11:01

일단 왠만하면 재정의하지 말자. 최대한 다른 방법을 생각해내자.

그리고 할거면 hashcode도 재정의하자.

 

== 연산자 VS Object class equals 메서드 


1. == 연산자

 

== 연산자는 객체를 식별할 수 있는 다르게 말하면 동일성을 판단할 수 있는 연산자이다. == 연산자는 참조값을 비교하는 것으로 같은 메모리 공간을 바라보고 있는지를 판단하는 것이다. 여기서 참조값은 identityHashCode와는 다르다는 것을 알아야 한다. 관련 테스트를 통해 알아보자.

 

참고로 hashCode 매서드를 재정의하지 않는다면 hashCode 매서드는 identityHashCode를 반환하는 것으로 알고 있다.

 

@DisplayName("identityHashCode는 참조값이 아니다. 그렇기 때문에 고유한 값을 반환하지 않는다.")
@Test
void identityHashCode() {

    class People {
        Integer id;

        public People(Integer id) {
            this.id = id;
        }
    }
    Set<Integer> set = new HashSet<>();

    for (Integer i = 0; i < 1000000; i++) {
        set.add(new People(i).hashCode());

    }
    assertThat(set.size()).isEqualTo(1000000);
}

 

OUTPUT

false
Expected :1000000
Actual :999765

 

 


2. Object class - equals

equals는 Java 최상위 객체인 Object 의 매서드로  == 연산과 동일하다.

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

 

하지만, 자바에서 제공하는 String, Integer 등은 equals가 재정의되어 있다.

그리고 이에 따라 hashCode() 매서드도 재정의되어 있다.

이팩티브 자바에 따르면 equals 을 재정의 할 때 hashCode도 재정의되어야 한다.

만약 equals만 재정의하고 hashCode 매서드를 내버려 둔다면 hashMap, hashSet 과 같은 자료구조를 사용할 때 심각한 문제가 발생하게 된다. 이는 추후 아래에서 코드를 통해 알아보겠다.

 

 

String 타입의 equals


객체의 참조값이 아닌 객체의 정보가 동일한지를 판단하는 즉 동등성(equlity)을 가지는지를 판단하는 대표 매서드가 String 타입의 equals 이다. eqauls 메서드는 더보기와 같이 정의 되어 있다.

더보기
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String aString = (String)anObject;
        if (coder() == aString.coder()) {
            return isLatin1() ? StringLatin1.equals(value, aString.value)
                              : StringUTF16.equals(value, aString.value);
        }
    }
    return false;

이제 코드를 통해 String 타입의 equals를 이해해보자.

 

String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");

System.out.println(String.format("str1 == str2 : [%b]", str1 == str2));
System.out.println(String.format("str1.equals(str2) : [%b]", str1.equals(str2)));
System.out.println(String.format("str2 == str3  : [%b]", str2 == str3));
System.out.println(String.format("str2.equals(str3) : [%b]", str2.equals(str3)));

 

OUTPUT

str1 == str2 : [true]
str1.equals(str2) : [true]
str2 == str3 : [false]
str2.equals(str3) : [true]

 

1. str1 == str2

str1/str2는 모두 같은 참조값을 바라보고 있다. 그렇기 때문에 true이다.

 

2. str1.equals(str2)

String의 equals 동등성(equlity)을 확인한다. 두 객체 모두 abc라는 정보를 담고 있으므로 true 이다.

 

3. str2 == str3

str3 new 를 통해 인스턴스를 만들어 새로운 참조값을 바라보게 한다. 따라서 str2/str3 의 참조 값은 다르게 되고 그렇기 때문에 false 이다.

 

4. str2.equals(str3)

2와 마찬가지로 논리적으로 동치이기 때문에 true이다.

 

 

 

 

Object 타입의 equals


People 이라는 클래스는 equals에 대한 재정의가 없기 때문에 Object 클래스와 equlas 기능이 같다.

 

 

 

class People {
    String id;
    
    public People(String id) {
        this.id = id;
    }
}

People people1 = new People("people1");
People people2 = people1;
People people3 = new People("people1");

System.out.println(String.format("people1 == people2 : [%b]", people1 == people2));
System.out.println(String.format("people1.equals(people2) : [%b]", people1.equals(people2)));

System.out.println(String.format("people1 == people3 : [%b]", people1 == people3));
System.out.println(String.format("people1.equals(people3) : [%b]", people1.equals(people3)));

 

OUTPUT

people1 == people2 : [true]
people1.equals(people2) : [true]
people1 == people3 : [false]
people1.equals(people3) : [false]

 

1. people1 == people2

people2 = people1 이므로 동일할 수 밖에 없다. true

 

2. people1.equals(people2)equals 매서드가 재정의되어 있지 않기 때문에 1과 결과가 같을 수밖에 없다. true

 

3. people1 == people3

people1, people3 모두 새롭게 생성된 인스턴스다. 동일할 수 없다. flase

 

4.people1.equals(people3)

equals 매서드가 재정의되어 있지 않기 때문에 3과 결과가 같을 수밖에 없다. false

 

 

여기 까지 정리

 

1. == 참조값이 같은지를 판단한다. 그래서 객체 식별성, 동일성(identity)을 판단할 수 있다.

2. equals 는 Object 클래스의 매서드이다. 기본적으로 == 과 동일하다. 자바에서 제공하는 대부분의 객체는 equals가 재정의 되어 있다.

3. 같은 정보를 가지고 있는지 판단하는 것은 동등성(equality)을 판단하는 것이다. 대표적인 것이 String 객체의 equals다.

 

 

 

마지막으로 equals를 재정의할 때 hashCode도 같이 재정의해야 되는 이유에 대해 알아보겠다.

개인적인 결론으로는

절대로 함부로 equals를 재정의하지 말자. 그 이유는 equals에 맞춰 hashCode 매서드는 일단 짜는 것은 현재 나로서는 굉장히 어려워 보인다. 그래서 그냥 짜여져 있는 equals를 잘 사용하자는게 내 결론

 

 

equals를 재정의하면 영향을 받는 것들이 존재한다. 대표적으로 HashSet과 같은 자료 구조

 

먼저 HashCode가 동일한지를 판단한다. 이후 eqauls 를 판단한다. 두 판단이 모두 true면 동등 객체라고 판단한다.

 

@DisplayName("hashCode 와 equals 를 같이 재정의해야하는 이유")
@Test
void hashcodeAndEquals() {

    class People {
        String id;

        public People(String id) {
            this.id = id;
        }

        @Override
        public boolean equals(Object obj) {
            People people = (People) obj;
            return this.id == people.id;
        }
    }

    People people1 = new People("1");
    People people2 = new People("1");

    HashSet<People> hashSet = new HashSet<>();
    System.out.println(String.format("people1 과 people2 는 동등한가 [%b]", people1.equals(people2)));

    hashSet.add(people1);
    hashSet.add(people2);

    assertThat(hashSet.size()).isEqualTo(1);

}

 

OUTPUT

people1 과 people2 는 동등한가 [true]

Expected :1
Actual   :2

 

 

우리는 eqauls 재정의를 통해 우리는 두 객체가 동등하다고 판단할 수도 있다. 그렇다면 HastSet에는 두 객체가 동일하기 때문에 size가 1이라고  생각하는 것이 합리적인 추론일수도 있다. 하지만 Hash 관련 자료구조는 먼저 hashCode 값이 동일한지를 판단한다고 한다.

 

그래서 두 객체가 동등할지언정 먼저 hashCode 값이 다르기 때문에 두 객체가 동등하다고 판단하지않고 Set에 두 객체 모두 들어가게 된다.

 

이렇게 equals 만 재정의할 경우 우리의 의도와는 다른 결과가 발생할 수 있기 때문에 함께 재정의하라는 의미인 것 같다.

 

 

일단 잘 재정의한 hashCode는 절대 아니겠지만 위와 같은 문제를 해결하려면 hashCode 재정의 해야한다.

@Override
public int hashCode() {
    return Integer.parseInt(this.id);
}

 

위처럼 hashCode를 재정의하면 문제를 해결할 수 있다.

 

 

 

참고자료

  • Effective Java 3/E - (아이템11, equals를 재정의하려거든 hashCode도 재정의하라)