본문 바로가기

Java

Java - equals 재정의시 주의점

이전에 알고 있던 지식(정확한지는 모름)

equals는 두 객체의 참조값을 비교한다. 참조값이 동일해야 true를 반환한다.


 

equals을 재정의하는 것은 신중해야 한다. 다음과 같은 상황이라면 재정의하지 않는 것이 최선이다.

 

 

1. 각 인스턴스가 본질적으로 고유하다.

값을 표현하는게 아니라 동작하는 개체를 표현하는 클래스가 여기에 해당한다. Thread가 좋은 예로 Obect의 equals 메서드는 이러한 클래스에 딱 맞게 구현되었다. 

 

정확한 예시는 아니겠지만 갤럭시 S22 제품 A, B가 있다고 하자. 두 핸드폰은 동일한 것인가?

주인이 다를 수도 있고 하는일도 다를수가 있다. 만약에 같은 주인이라 할지라도 한 핸드폰은 업무용, 한 핸드폰은 개인용으로 용도가 구분될 수 있다. 이럴 경우에는 equals을 재정의하지 않는편이 낫다.

 

2. 인스턴스의 '논리적 동치성'을 검사할 일이 없다.

논리적 동치성의 의미를 모르겠다. 일단 넘어간다.

 

내용 추가) 원서를 해석해보고 논리적 동치성에 대한 의미를 유추해보면

논리적 동치성(logically equivalent)은 두 객체의 value가 동일함을 의미한다.

 

논리적 동치성을 검사할 일이 없다는 것은 객체의 값이 동일함을 따질 필요가 없다는 것이다. 그럴 때는 equals를 재정의할 필요가 없다는 것이다. 아래 코드를 보자

 

@Getter
class Member {
    private String name;

    public Member(String name) {
        this.name = name;
    }
}

public class StudyEqualAndHashcode {

    public static void main(String[] args) {
        Member member1 = new Member("Kim");
        Member member2 = new Member("Kim");

        System.out.println(member1.hashCode());
        System.out.println(member2.hashCode());
        System.out.println(member1.equals(member2));
    }
}

Member 클래스는 필드값으로 name만 있는 단순한 클래스이다. 우리가 Member 객체가 동일한지를 비교할때는 객체를 비교하려는 것이 아닌 객체의 값이 같은지를 알고 싶다. 그것을 기대하고 equals를 쓴다면 실망스런 결과를 가져올 것이다. equals을 기본적으로 참조값을 비교한다. 때문에 같은 내용이라 할지라도 객체가 가리키는 주소가 다르기 때문에 equals 메서드를 사용했을 때 false를 반환한다.

 

위에서는 인스턴스의 '논리적 동치성'을 검사할 일이 없다. 라고 하였다. 위 코드처럼 논리적 동치성(객체가 아닌 값 동치)을 따질 필요가 없을때 equals를 재정의할 필요가 없다는 것이다. 다르게 말해, 위 코드 블럭의 메인 메서드 코드를 짤 일이 없을 때는 재정의하지 말라는 것이다.

 

OUTPUT

99347477
566034357
false

 

3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.

당연한 말 아닌가?

 

4. 클래스가 private이거나 package-private이고 equlas 메서드를 호출할 일이 없다.

그렇다고 한다. 호출할 일이 없는데... 재정의할 필요가 있나?

 


그렇다면 equals를 재정의해야 할 때는 언제일까? 

객체 식별성이(두 객체가 물리적으로 같은가) 아니라 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의디지 않았을 때이다.

 

주로 값 클래스(value classes)들이 여기 해당한다.

 

값 클래스라 해도, 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스라면 equals를 재정의하지 않아도 된다. (인스턴스 통제 클래스, Enum 등) 이런 클래스에서는 어차피 논리적으로 같은 인스턴스가 2개 이상 만들어지지 않으니 논리적 동치성과 객체 식별성(hashcode?)이 사실상 똑같은 의미가 된다.

 

 

한편, equals 메서드를 재정의할 때는 반드시 일반 규약을 따라야 한다. 다음은 Object 명세에 적힌 규약이다.

 

아래 규약은 변수가 null이 아님을 전제한다.

 

반사성 : 찹조 값 x에 대해 x.equals(x)는 true

대칭성 : 참조 값 x, y에 대해 x.equals(y)는 true이면 반대도 true

추이성 : 참조 값 x, y, z에 대해 x.equals(y)가 true이고 y.equals(z)가 true면 x.equals(z)도 true

일관성 : 한번 true면 재검사해도 계속 true

null-아님 : x.equals(null)은 false

 

5가지 규약에 대해 조금 정리를 해보려고 합니다.


대칭성

 

@Getter
final class Member {
    private final String name;

    public Member(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof Member) {
            return name.equals (((Member) o).name);
        }

        if (o instanceof String) {
            return name.equals((String) name);
        }
        return false;
    }
}

public class StudyEqualAndHashcode {
    
    public static void main(String[] args) {
        Member member1 = new Member("Kim");
        String s = "Kim";
        
        System.out.println(member1.equals(s));
        System.out.println(s.equals(member1));
    }
}

 

Output

true
false

 

저자는  객체의 equals를 String과도 연동하겠다는 허항된 꿈을 버려야 한다고 말한다.

 

올바른 equals 재정의

 

@Override
public boolean equals(Object o) {
    return o instanceof Member && ((Member) o).name.equals(name);
}

 


이하 다른 규칙들은 equals 2편에서 정리해보도록 하겠습니다.

 

 

참고 effective java 3/E