Effective Java - Item10 (1)

초보개발·2022년 8월 9일
0

JAVA

목록 보기
15/15

"equals는 일반 규약을 지켜 재정의하라"

equals를 재정의하면 안되는 경우

아래 경우중 하나라도 해당한다면 재정의하지 않는 것이 좋다.

  1. 각 인스턴스가 본질적으로 고유하다.
    값 표현(Integer, String)이 아닌 동작하는 개체를 표현하는 클래스가 해당된다.
  2. 인스턴스의 논리적 동치성(equality)을 검사할 일이 없다.
    java.util.regax.Pattern은 equals를 재정의해 두 Pattern의 정규표현식을 비교
  3. 상위 클래스에서 재정의한 equals가 하위 클래스에서도 적용할 수 있다.
    SetAbstractSet이 구현한 equals를 상속받아 사용
    ListAbstractList, MapAbstractMap
  4. 클래스가 private이거나 package-private이고 equals를 호출할 일이 없다.
	@Override public boolean equals(Object o) {
		throw new AssertionError(); // 실수로라도 호출될 일이 없도록 방지
	}

equals를 재정의해야 할 경우

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

  • 주로 값 클래스들(Integer, String)이 해당

equals가 논리적 동치성을 비교하도록 재정의하면 Map의 키와 Set의 원소로 사용할 수 있게 된다.

equals 재정의시 지켜야할 규약

equals 메서드는 동치관계를 구현하며 아래 조건들을 만족한다.

  • 반사성: null이 아닌 모든 참조 값 x에 대해 x.equals(x) == true
  • 대칭성: null이 아닌 모든 참조 값 x, y에 대해, x.equals(y) == truey.equals(x) == true
// 대칭성 위배 코드
    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;
}
  • 추이성: null이 아닌 모든 참조 값 x, y, z에 대해x.equals(y) == true이고 y.equals(z) == true면, x.equals(z) == true
  • 일관성: null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true거나 항상 false를 반환
  • null-아님: null이 아닌 모든 참조 값 x에 대해, x.equals(null) == true

동치 관계: 집합을 서로 같은 원소들로 이뤄진 부분집합으로 나누는 연산
이 부분집합을 동치류(equivalence class, 동치클래스)라고 한다.

  1. 반사성 - 객체는 자기 자신과 같아야 한다.
public class Example {
    private String name;

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

    public static void main(String[] args) {
        List<Example> list = new ArrayList<>();
        Example ex = new Example("hello");
        list.add(ex);
        System.out.println(list.contains(ex)); // true
    }
}
  1. 대칭성 - 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다.
    public final class CaseInsensitiveString {
      private final String s;

      public CaseInsensitiveString(String s) {
        this.s = Obejcts.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;
      }
    }

CaseInsensitiveString의 equals는 String을 알고 있지만 String의 equals는 CaseInsensitiveString을 모른다.

    CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
    String s = "polish";

    cis.equals(s); // true
    s.equals(cis); // false

해결 - CaseInsensitiveString끼리만 비교하도록 변경한다.

    @Override public boolean equals(Object o){
      return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); 
    }
  1. 추이성 - 첫번째 객체와 두번째 객체가 같고 두번째 객체와 세번째 객체가 같다면 첫번째 객체와 세번째 객체도 같아야 한다.
    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;
        }
	}
  1. 잘못된 코드 - 대칭성 위배
      @Override 
      public boolean equals(Object o) {
          if(!o instanceof ColorPoint)
              return false;
          return super.equals(o) && ((ColorPoint) o).color == color;
      }
	 public static void main(){
         Point p = new Point(1,2);
         ColorPoint cp = new ColorPoint(1,2, Color.RED);
         p.equals(cp);    // true
         cp.equals(p);    // false
    }

Point의 equals는 색상을 무시하고 ColorPoint의 equals는 입력 매개변수의 클래스 종류가 달라서 false만 반환할 것이다.
2. 잘못된 코드 - 추이성 위배

   @Override 
   public boolean equals(Obejct o){
        if(!(o instanceof Point))
          return false;
        if(!(o instanceof ColorPoint)) // o가 Point면 색상을 무시하고 비교
          return o.equals(this);
		// o가 ColorPoint면 색상까지 비교
        return super.equals(o) && ((ColorPoint) o).color == color;
   }
   public static void main(){
        ColorPoint p1 = new ColorPoint(1,2, Color.RED);
        Point p2 = new Point(1,2);
        ColorPoint p3 = new ColorPoint(1,2, Color.BLUE);
        p1.equals(p2);    // true 
        p2.equals(p3);    // true 
        p1.equals(p3);    // false
      }

p1.equals(p3)가 false를 반환하므로 추이성을 위배했으며 이 방법은 무한 재귀에 빠질 위험도 있다.

    public SmellPoint extends Point {
        @Override 
        public boolean equals(Obejct o){
          if(!(o instanceof Point))
            return false;
          // ColorPoint의 equals: SmellPoint의 equals로 비교
      	  // SmellPoint의 equals: ColorPoint의 equals로 비교
          if(!(o instanceof SmellPoint)) //
            return o.equals(this);
          return super.equals(o) && ((SmellPoint) o).color == color;
        }
    }
    public static void main(){
      ColorPoint myColorPoint = new ColorPoint(1,2, Color.RED);
      SmellPoint mySmellPoint = new SmellPoint(1,2);
      myColorPoint.equals(mySmellPoint); // StackoverflowError 발생
    }

사실 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 없다.
3. 잘못된 코드 - 리스코프 치환 원칙 위배
equals에서 instanceof 대신 getClass 검사를 사용하라는 것이 아니다.
아래 코드는 equals는 같은 구현 클래스의 객체와 비교할 때만 true를 반환하는 것 처럼 보이지만 실제로 활용할 수 없다.

    @Override public boolean equals(Object o){
      if(o == null || o.getClass() != getClass())
        return false;
      Point p = (Point) o;
      return p.x == x && p.y == y;
    }

리스코프 치환 원칙 = 어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요하다.
즉, Point의 하위 클래스는 여전히 Point이므로 어디서든 Point로 활용될 수 있어야 한다. 또한 그 타입의 모든 메서드가 하위 타입에서도 똑같이 잘 작동해야 한다.

해결 - 상속 대신 컴포지션을 사용하라(Item18)

컴포지션: 기존 클래스가 새로운 클래스의 구성 요소로 사용된다.
기존 클래스를 확장하는 것이 아닌 새로운 클래스를 생성하고 private 필드로 기존 클래스의 인스턴스를 참조한다.
컴포지션으로 새 클래스의 인스턴스 메서드들은 기존 클래스에 대응하는 메서드를 호출해 그 결과를 반환한다.

Point를 상속하는 대신 PointColorPoint의 private 필드로 두고 ColorPoint와 같은 위치의 일반 Point를 반환하는 view 메서드(Item6)를 public으로 추가한다.

public class ColorPoint {
    private final Point point;
    private final Color color;

	public ColorPoint(int x, int y, Color color) {
		point = new Point(x, y);
		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);
  }
}
  • ColorPoint 끼리 비교: ColorPoint의 equals를 이용해 색상 값까지 비교
  • ColorPointPoint 비교: ColorPoint의 asPoint 메서드를 이용해 Point로 바꾸고 Point의 equals를 이용해 x, y를 비교
  • Point끼리 비교: Point의 equals로 x, y 비교

해결 - 추상클래스의 하위클래스 사용하기
추상 클래스(상위 클래스)의 인스턴스를 만드는 것이 불가능하기 때문에 하위 클래스끼리 비교가 가능하다.

0개의 댓글