# Item10. equals는 일반 규약을 지켜 재정의하라

연어는결국강으로·2023년 2월 4일
0

이펙티브자바

목록 보기
5/7
  • equals는 재정의하는게 쉽지 않다.

equals를 재정의 하지 않아도 되는 경우

  • 각 인스턴스가 본질적으로 고유하다.
  • 인스턴스의 ‘논리적 동치성’을 검사할 일이 없다.
    • 클라이언트가 논리적 동치성을 검사하는 방식이 필요하지 않다고 생각한다면
    • → Object의 기본 eqauls만으로 해결된다.
  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
  • 클래스가 private거나 package-private이고 eqauls 매서드를 호출할 일이 없다.
    // eqauls 호출을 막는 매서드
    @Overide
    public boolean eqauls(Object object) {
    	throw new AssertionError(); // 호출 금지
    }

eqauls를 재정의해야 할 때

  • 객체 식별성이 아니라 논리적 동치성을 확인해야 하는데,
  • 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의하지 않았을 때
  • 주로 값 클래스가 해당된다.
    • 같이 값이 같은 인스턴스가 둘 이상 만들어 지지 않는다면 재정의 x( Item1 정적 팩터리 매서드, Item34 Enum )

equals 일반 규약

  • 전제 : 변수 x, y, z는 null이 아닌 모든 참조값이다.
  • 반사성 : x에 대해 x.equals(x) = true 이다.
  • 대칭성 : x, y에 대해 ( x.equals(y) == true ) → ( y.equals(x) == true ) 이다.
  • 추이성 : x, y, z에 대해 ( x.equals(y) == true ) & ( y.equals(z) ==true ) → ( x.equals(z) == true )
  • 일관성 : x, y에 대해 x.equals(y)를 반복 호출하면 항상 true 나 false를 반환한다.
  • null - 아님 : x.equals.(null)은 항상 false이다.

Object 명세에서 말하는 동치관계

  • 집합을 서로 같은 원소들로 이뤄진 부분집합으로 나누는 연산
  • 반사성 : 객체가 자기 자신과 같아야 함.
  • 대칭성 : 서로에 대한 동치여부에 똑같이 답해야 함.
    • 예시

      	public final class CaseInsensiveString {
      		
      		private final String s;
      
      		public CaseInsensitiveString(String s) {
      			this.s = Object.requireNonNull(s);
      		}
      	
      		@Override
      		public boolean equals(Object o) {
      			if(o instanceof CaseInsensitiveString) 
      				return s.equalsIgnoreCase(
      					(( CaseInsensitiveString o).s);
      			if(o instanceof String)
      				return s.equalsIgnoreCase((String) o);
      			return false;
      		} 
    • 해당 클래스는 equals에서 대소문자를 무시한다.

    • String의 equals와 CaseInsensitiveString의 equals 는 다른 값을 반환한다.

    • 해당 클래스의 equals를 String과 연동시키겠다는 생각을 버려야한다.

    • 결과

      @Overide
      public boolean equals(Object o) {
      	return o instanceof CaseInsensitiveString &&
          	((CaseInsensitiveString) o).s.eqaulsIgnoreCase(s);
       }

  • 추이성 : a=b & b=c → a=c
    • 상위 클래스에는 없는 새로운 필드를 하위 클래스에 추가하는 상황

      // 상위 클래스 Point
      public class Point {
      	private final int x;
      	private final int y;
      
      	public Point(int x, int y) {
      		this.x = x;
      		this.y = y;
      	}
      
      	@Override
      	public boolean equals(Object o) {
      		if(!(o instanceof Point)){
      			return false;
      		Point p = (Point) o;
      		return p.x == x && p.y ==y;
      	}
      // 하위 클래스 
      public class ColorPoint extends Point {
      	private final Color color;
      
      	public ColorPoint(int x, int y, Color color) {
      		super(x, y);
      		this.color = color;
      	}
      
      	... // 나머지 코드 생략
      } 
    • eqauls를 수정하지 않으면 Point의 구현이 상속되어서 색상 정보를 무시한 채 비교한다.

      // 잘못된 코드 - 대칭성 위배
      @Override
      public boolean equals(Object o) {
      	if(!(o instanceof ColorPoint)) return false;
      	return super.equals(o) && ((ColorPoint) o).color == color;
      }
    • 이 메서드는 point.equals(colorPoint)colorPoint.equals(Point)의 결과가 다를 수 있다.

    • Point의 equals는 색상을 무시하고, ColorPoint의 equals는 입력 매개변수의 클래스 종류가 다르다며 매번 false만 반환할 것이다.

    • colorPoint.equals(point)가 true를 반환하도록 하면 해결될까? - colorPoint의 색상 정보를 무시하도록

      // 잘못된 코드 - 추이성 위배
      @Override
      public boolean equals(Object o){
      	if(!(o instanceof Point)) return false;
      	
      	// o가 일반 Point이면 색상 무시 후 비교
      	if(!(o instanceof ColorPoint)) return o.equals(this);
      	
      	// o가 ColorPoint이면 색상까지 비교
      	return super.equals(o) && ((ColorPoint) o).color = color;
      }
    • 이 방식은 대칭성은 지켜주지만, 추이성을 깬다.

    • 또한, 이 방식은 무한 재귀에 빠질 위험도 있다.

      💡 Point의 하위클래스 SmellPoint를 만들고 eqauls는 같은 방식으로 구현한다 그런 다음 **myColorPoint.equals(mySmellPoint)**를 호출하면 StackOverflowError를 일으킨다.

해법은 없다.

  • 이 현상은 모든 객체 지향 언어의 동치관계에서 나타나는 근본적인 문제이다.
  • 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
  • !(필드 추가 & eqauls 규약만족)

  • instanceof 검사를 getClass 검사로 바꾸면 괜찮아 보이지만 그렇지 않다.
@Override
public boolean equals(Object o) {
	if(o==null ||
  • 리스코프 치환 원칙(Liskov substitution principle)에 따르면, 어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요하다. → 그 타입의 모든 매서드가 하위 타입에서도 똑같이 잘 작동해야 한다.

  • 다른 방법 → 상속 대신 컴포지션을 사용하라

    // 컴포지션으로 구현한 예시
    public class ColorPoint {
    
        private final Point point;
        private final Color color;
    
        public ColorPoint(int x, int y, Color color){
            point = new Point();
            this.color = Objects.requireNonNull(color);
        }
    
        /*
         *  이 ColorPoint의 Point 뷰를 반환한다.
         */
        public Point asPoint(){
            return  point;
        }
    
        @Override
        public boolean equals(Object o){
            if(!(o instanceof  ColorPoint))
                return false;
            ColorPoint cp = (ColorPoint) o;
            return cp.point.equals(point) && cp.color.equals(color);
        }
        
        // .... 나머지 코드 생략
    }
    • 자바 라이브러리에서 구체클래스를 확장해 값을 추가한 클래스 예시
      • java.sql.Timestamp는 java.util.Date를 확장한 후 nanoseconds 필드를 추가했다.
      • 그 결과로 Timestamp의 equals는 대칭성을 위배하며, Date와 섞어 쓰면 엉뚱하게 동작할 수 있다.
      • 이런 설계는 실수니 절대 따라 해서는 안된다.
    • 추상 클래스의 하위클래스에서라면 equals 규약을 지키면서도 값을 추가할 수 있다.
  • 일관성은 두 객체가 같다면 앞으로도 영원히 같아야 한다는 뜻이다.

  • 가변 객체는 비교 시점에 따라 서로 다를 수도 혹은 같을 수도 있는 반면, 불변 객체는 한번 다르면 끝까지 달라야 한다.

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

  • 이런 문제를 피하려면 equals는 항시 메모리에 존재하는 객체만을 사용한 결정적(deterministic) 계산만 수행해야 한다.

  • null-아님 모든 객체가 null과 같지 않아야 한다는 뜻이다. NullPointerException을 던지는 경우조차 허용하지 않는다.

    // 명시적 null 검사 - 필요 없다!
    @Override
        public boolean equals(Object obj) {
            if(obj == null){
                return false;
            }
    			...
        }
    • 이러한 검사는 필요치 않다.

    • 동치성을 검사하려면 equals는 건네받은 객체를 적절히 형변환한 후 필수 필드들의 값을 알아내야 한다. 그러려면 형변환에 앞서 instanceof 연산자로 입력 매개변수가 올바른 타입인지 검사해야한다.

      @Override public boolean equals(Object o){
      	if(!(o instanceof MyType))
      		return false;
      	MyType mt = (MyType) o;
      	...
      }
    • equals가 타입을 확인하지 않으면 잘못된 타입이 인수로 주어졌을 때 ClassCastException을 던져서 일반 규약을 위배하게 된다.

    • 그런데 instanceof는 첫 번째 피 연산자가 null이면 false를 반환한다.

    • 따라서 입력이 null이면 타입 확인 단계에서 false를 반환하기 때문에 null 검사를 명시적으로 하지 않아도 된다.

equals 메서드 구현 방법 단계별 정리

  • == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
  • instanceof 연산자로 입력이 올바른 타입인지 확인한다.
  • 입력을 올바른 타입으로 형변환한다.
  • 입력 객체와 자기 자신의 대응되는 ‘핵심’ 필드들이 모두 일치하는지 하나씩 검사한다.

단위테스트를 작성하자

  • equals를 다 구현 했다면 대칭적인지, 추이성이 있는지, 일관적인지 단위 테스트를 작성해 돌려보자
  • equals를 작성하고 테스트하는 일을 대신해주는 프레임워크 → AutoValue

0개의 댓글