Effective Java - 모든 객체의 공통 메서드(1)

SeungHyuk Shin·2021년 9월 9일
0

Effective Java

목록 보기
4/26
post-thumbnail

아이템 10. equals는 일반 규약을 지켜 재정의하라


equals 메서드는 재정의하기 쉬워 보이지만 곳곳에 함정이 도사리고 있어서 자칫하면 끔직한 결과를 초래한다.

문제를 회피하는 가장 쉬운길은 아예 재정의 하지 않는 것이다. 그러므로 밑의 상황중 하나에 해당하면 재정의 하지 않는것이 최선이다.

  • 각 인스턴스가 본질적으로 고유하다.
    값을 표현하는 것이 아니라 동작하는 개체를 표현하는 클래스가 여기 해당한다.
  • 인스턴스의 '논리적 동치성'을 검사할일이 없다.
  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
  • 클래스가 private이거나 package-private이고 eqauls 메서드를 호출할 일이 없다.

혹시라도 equals가 실수라도 호출되는 걸 막고 싶다면 다음처럼 구현해두자

@Override public boolean eqauls(Object o){
	throw new AssertionError();
}

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

주로 Integer와 String 값 클래스 들이 여기 해당한다.

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

eqauls 메서드는 동치관계를 구현하며, 다음을 만족한다.
1. 반사성 : null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true다.
2. 대칭성 : null이 아닌 모든 참조 값 x,y에 대해, x.equals(y)가 true면 y.equals(x)도 true이다.
3. 추이성 : null이 아닌 모든 참조 값 x,y,z,에 대해, x.equals(y)가 true이고, y.equals(z)도 true이면 x.equals(z)도 true 이다.
4. 일관성 : null이 아닌 모든 참조 값 x,y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
5. null-아님 : null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.

반사성은 단수히 말하면 객체는 자기 자신과 같아야 한다는 뜻이다. 이 요건을 어긴 클래스의 인스턴스를 컬렉션에 넣은 다음 contains 메서드를 호출하면 방금 넣은 인스턴스가 없다고 답할 것이다.

대칭성은 두 객체는 서로 대한 동치 여부에 똑같이 답해야 한다는 뜻이다. 대소문자를 구별하는 문자열을 구현한 다음 클래스를 예로 살펴보자.

public final class CaseInsensitiveString {
	private final String s;
    
    public CaseInsensitiveString(String s){
    	this.s = Objects.requireNoNull(s);
    }
    
    // 대칭성 위배!
    @Override public boolean equals(Object o){
    	if( o instanceof CaseInsensitiveString){
       	  return s.equalsIgnorCase(((CaseInsensitiveString) o).s);
        }
        if (o instanceof String){
       	  return s.equalsIgnoreCase((String) o);
        }
        return false;
    }
}

다음처럼 CaseInsensitiveString과 일반 String 객체가 하나씩 있다고 해보자.

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

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

문제는 CaseInsensitiveString String의 equals는 일반 String을 알고 있지만 String의 equals는 CaseInsensitiveString의 존재를 모른다는 데 있다.

이 문제를 해결하려면 CaseInsensitiveString의 euqlas를 String과도 연동 하겠다는 허황된 꿈을 버려야한다.

그 결과 equals는 다음처럼 간단한 모습으로 바뀐다.

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

추이성은 첫 번쨰 객체와 두번째 객체가 같고 두번째 객체와 세번째 객체가 같다면 첫번째 객체와 세번쨰 객체도 같아야 한다는 뜻이다.

간단히 2차원에서의 점을 표현하는 클래스를 예로 들어보자.

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(Obejct 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;
  }
}

equals 메서드는 어떻게 해야 할까? 그대로 둔다면 Point의 구현이 상속되어 색상정보를 무시 한채 비교를 수행한다.

다음 코드처럼 비교 대상이 또다른 ColorPoint이고 위치와 색상이 같을때만 true를 반환하는 equals를 생각해보자.

@Ovverride public boolean equals(Object O){
  if (!(o instanceof ColorPoint)) // Point 입력시 항상 false
    return false;
  return super.equals(o) && ((ColorPoint) o).color == color;
}

Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1,2,Color.RED);

p.equals(cp)  // true
cp.equals(p) // false

일반 Point를 ColorPoint에 비교한 결과와 그 둘을 바꿔 비교한 결과가 다를 수 있다.
Point의 equals는 색상을 무시하고 ColorPoint의 equals는 입력 매개변수의 클래스 종류가 다르다며 매번 false만 반환할 것이다.

ColorPoint.equals가 Point와 비교할 때는 색상을 무시하도록 하면 해결될까?

@Ovverride public boolean equals(Object O){
  if (!(o instanceof Point)) 
    return false;
    
   // o가 일반 Point면 색상을 무시하고 비교한다.
  if (!(o instancof ColorPoint)
    return o.equals(this);
    
  // o가 ColorPoint면 색상까지 비교한다.
  return super.equals(o) && ((ColorPoint) o).color == color;
}

// 이 방식은 대칭성은 지켜주지만 추이성을 깨버린다.

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

그럼 해법은 무엇일까? 사실 구체 클래스를 확장해 새로운 값을 추가하면서 equals의 규약을 만족시킬 방법은 존재하지 않는다.

구체 클래스의 하위 클래스에서 값을 추가할 방법은 없지만 괜찮은 우회 방법이 있다. "상속대신 컴포지션을 사용하라"는 아이템 18의 조언을 따르면된다.

Point를 상속하는 대신 Point를 ColorPoint의 private 필드로 두고 ColorPoint와 같은 위치의 일반 Point를 반환하는 뷰 메서드를 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);
  }
  
  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);
  }
}

일관성은 두객체가 같다면 앞으로도 영원히 같아야 한다는 뜻이다. 가변 객체는 비교시점에 따라 서로 다를 수도 혹은 같으수도 있는 바면, 불변객체는 한번 다르면 끝까지 달라야한다.

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

예컨대 java.net.URL의 equals는 주어진 URL과 매핑된 호스트의 IP주소를 이용해 비교한다. 호스트 이름을 IP주소로 바꾸려면 네트워크를 통해야 하는데 그 결과가 항상 같다고 보장할 수 없다. URL의 equals를 이렇게 구현한것을 커다란 실수 였으니 절대 따라해서는 안된다.

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

equals를 다 구현했다면 세 가지만 자문해보자. 대칭적인가? 추이성이 있는가? 일관적인가?

0개의 댓글