책/Effective Java

equals 재정의

728x90
반응형

일단, 결론부터 얘기하자면 

Object Class를 믿고, equals 꼭 필요한 것이 아니면 재정의하지 말자

 

그래도 반드시 재정의를 해야할 때가 있을 수 있으니 고려하고 준수해야할 사항들을 살펴보도록 하자. 

 

 

# 재정의하지 않는 것이 좋은지 다시 확인 

재정의하지 않는 것이 최선인 상황들이 존재한다.

  • 각 인스턴스가 본질적으로 고유할 때
    • ex) 동작하는 개체를 표현하는 클래스 - Thread 
  • 인스턴스의 논리적 동치성(logical equality)을 검사할 일이 없을 때 
  • 상위 클래스에서 재정의한 equals가 하위클래스에서도 적절하게 동작할 때 
    • Constructor Set / List / Map 은 모두 AbstractSet / AbstractList / AbstractMap 으로부터 상속받아서 그대로 사용한다. 
  • 클래스가 private / package-private 이고 equals() 를 호출할 일이 없을 때 

 

equals() 를 재정의해야할 때

  • 객체 식별성이 아닌 논리적 동치성을 확인해야 할 때, 상위 클래스에서 equals 가 논리적 동치성을 비교하지 않을 때
    • ex) 값 클래스 (Integer , String) 등 두 객체가 같은지를 비교하는 것이 아닌, 해당 객체들의 값이 같은지를 알고 싶은 상황
    • 재정의를 통해 Map 의 key와 Set의 원소로 사용할 수 있다. 

 

  • cf) Enum 과 같은 인스턴스 통제 클래스라면 equals 는 재정의하지 않아도 된다. 
    • 논리적으로 같은 클래스가 2개 이상 만들어지지 않기 때문에 

 

equals 를 재정의하며 지켜야할 규약 5가지

 

- 반사성 : null 이 아닌 모든 참조 값에 대해 x.equals(x) => true 

- 대칭성 : null 이 아닌 모든 참조 값 x,y 에 대해 x.equals(y) => true 면 y.equals(x) => true 

 - 추이성 : null이 아닌 모든 참조 값 x,y,z 에 대해  x.equals(y) => true  &&  y.equals(z) => true 면 x.eqauls(z) => true 

 - 일관성 : null이 아닌 모든 참조 값 x,y 에 대해 x.equals(y) => true 면 호출 때 마다 항상 true 여야 한다. 

 - null 아님 : null이 아닌 모든 참조 값 x 에 대해  x.equals(null) => false 다. 

 

그렇다면 왜 equals 메서드를 재정의할 때 이러한 규약을 지켜야 할까

 

 A.
결국 홀로 존재하는 클래스는 없기 때문에, 한 클래스의 인스턴스는 다른 곳에서 빈번히 전달되거나 호출된다. 
그리고 컬렉션 클래스들을 포함해 수 많은 클래스는 전달받은 객체가 equals 규약을 지킨다고 가정하고 동작하기 때문에 
해당 equals 메서드가 정상적으로 재정의 되어있지 않다면 프로그램은 결국 이상하게 동작하고 종료된다. 

즉, 규약을 어기면 해당 객체를 사용하는 다른 객체가 어떻게 반응할지 알 수 없다. 

 

 

  • 반사성 

: 객체는 자기 자신과 같아야 한다. ( 일부로 안 지키려고 하지 않는 이상 지켜진다)

 

  • 대칭성

: 두 객체는 서로에 대한 동치 여부에 대해 똑같은 값을 반환해야한다.

ex) String 객체와 String 필드를 가지는 새로운 클래스(equals 대소문자 무시하는 로직)에서 String 에 대한 equals는 서로 다른 값을 반환한다. 

 

  • 추이성

: 구현 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 수 있는 방법은 존재하지 않는다.

(단, 객체 지향적 추상화의 이점을 포기한다면 가능하다)

 

 리스코프 치환 원칙(LSP)에서 어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요하기 때문에 모든 메서드가 하위 타입에서도 똑같이 잘 작동해야한다.

 

Set 을 포함하여 대부분의 컬렉션은 equals 메서드를 사용하는데,
상속을 받은 하위클래스와의 equals 비교에서는 equals 에서 getClass() == getClass() 를 하게 되면 false 만 반환하게 된다.
(어떤 구체 클래스는 하위 클래스와 같을 수 없기 때문) 

따라서 구체 클래스의 하위 클래스에서 값을 추가할 방법을 선택하는 것이 아닌(추가할 방법이 없다) 
"상속 대신 컴포지션(구성 - 멤버 변수를 포함)을 사용" 하는 방법을 선택하자 

상속 대신 컴포지션을 이용하면 상속 때문에 발생하는 대칭성 , 추이성, 리스코프 치환 원칙에 위배되지 않는 equals() 규약을 해결할 수 있다. 

 

 

  • 일관성

(어느 하나 혹은 두 객체 모두가 수정되지 않는 한) 두 객체가 같다면 영원히 같아야 한다. 

즉, 불변 객체는 한번 다르면 끝까지 달라야한다. 

 

클래스가 불변이든 가변이든, equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해선 안된다. 

 

ex) java.net.URL 의 equals 는 주어진 URL과 매핑된 호스트의 IP 주소를 이용해서 비교하는데, 

이 때 호스트 이름 / IP 주소를 바꾸려면 네트워크를 통해야 하는데, 그 결과가 항상 같다고 보장할 수 없다 - 즉 일관성이 지켜지지 못한다. 

따라서 URL은 equals 재정의를 제대로 지키지 못한 일례이다. 

 

  • Null 아님 

equals 내부에서 instanceof 연산자를 통해 입력 매개변수가 올바른 타입인지 검사하는 것으로 보장한다. 

 


equals() 구현 절차

  • == 연산자를 통해 입력된 파라미터와 자기 자신이 같은 객체인지 검사(동일성 검사)
    • 성능 향상에 도움 
  • instanceof 연산자를 통해 들어온 파라미터 타입이 올바른지 체크
    • null 아님 보장 
    • 같은 interface 를 구현한 클래스 끼리 비교하는 경우에도 
  • 입력을 올바른 타입으로 형변환하여 사용한다. 
    • 파라미터는 Object로 받은 이후 해당 비교하고자 하는 타입으로 형변환
    • 앞서서 instanceof 연산을 통해 무조건 처리 가능
  • 객체와 대응되는 모든 필드가 일치하는지 확인해야함
    • 하나라도 다르다면 false 를 반환 
    • 이 떄 float, double 은 == 이 아닌, .compare()을 통해서 비교하자 
      • 특수한 부동소수 값 등을 다뤄야 할 수도 있다. 
    • 참조 타입은 equals() 를 통해 비교 
    • 기본 타입은 == 을 통해 비교 
    • 배열의 원소가 모두 핵심 필드라면 Arrays.equals 사용 
    • null 이 가능한 필드는 Object.equals(obj, obj)를 통해 확인 
  • 효율 / 성능을 위한 비교는 
    • 다를 확률이 높은 필드부터 비교
    • 비교하는 비용(시간복잡도)를 고려하여 적은 비교 대상을 먼저 비교하자 

 

마지막으로..

  • equals 재정의 후 대칭성 / 추이성 / 일관성 체크 
    • 단위 테스트를 통해 진행
  • equals 작성시, hashCode도 반드시 재정의 요망
  • @AutoValue 를 통해 equals , hashcode 자동 재정의하는 것도 고려

 

728x90
반응형