해당 포스트는 "Effective Java" 책의 내용을 요약한 것이다.



※ equals를 재정의할 때는 일반 규약을 따라야 한다.

: equals를 재정의할 때는 객체 동일성이 아닌 논리적 동일성의 개념을 지원하는 클래스 경우, 상위 클래스의 equals가 하위 클래스의 필요를 충족하지 못할 경우 재정의해야 한다. 이러한 경우는 보통 클래스가 값 클래스일 경우이다. 프로그래머는 값 객체에 대한 비교를 객체의 동일성을 보지 않고 객체가 가지는 값에 대한 동일성 때문에 equals를 호출한다. equals를 재정의 할 필요가 없는 값 클래스가 있는 데 그 것은 싱글톤이나 enum 같이 최대 하나의 객체만 존재하는 것들이다. 


- equals 메서드는 동치 관계를 따른다.

1. 반사성 

: null이 아닌 참조 x가 있을 때 x.equals(x)는 true를 반환한다. 이 조건을 어기는 클래스를 만들기는 힘들다.


2. 대칭성 

: null이 아닌 참조 x,y가 있을 때 x.equals(y)와 y.equals(x)는 같은 값을 가진다. 다음 예를 보자. 

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 obj) {
		if(!(obj instanceof Point))
			return false;
		Point p = (Point)obj;
		return p.x==x && p.y==y;
	}
}
class ColorPoint extends Point{
	private final int color;	
	public ColorPoint(int x, int y, int color){
		super(x,y);
		this.color = color;
	}
	@Override
	public boolean equals(Object obj) {
		if(!(obj instanceof ColorPoint))
			return false;
		return super.equals(obj) && ((ColorPoint)obj).color==color;
	}
}
public class test {

	public static void main(String args[]) throws Exception {

		Point x = new Point(1,2);
		ColorPoint y = new ColorPoint(1,2,3);
		x.equals(y);
		y.equals(x);
	}

}

위 예제에서 x.equals(y);는 true 값이 나온다. 하지만 y.equals(x)를 하게 되면 obj instanceof ColorPoint가 false로 조건문이 참이 되 return false가 된다. 대칭성 요구조건을 충족하지 못한다. 그래서 ColorPoint의 equals 메서드를 다음과 같이 수정할 수 있다.

        @Override
	public boolean equals(Object obj) {
		if(!(obj instanceof Point))
			return false;
		
		if(!(obj instanceof ColorPoint))
			return obj.equals(this);
		return super.equals(obj) && ((ColorPoint)obj).color==color;
	}

위와 같이 수정하면 y.equals(x)가 수행될 시 return obj.equals(this)가 실행되 true 값이 나와 대칭성에 만족한다. 하지만 다음에 배울 추이성에 만족하지 못한다.


3. 추이성

: null이 아닌 참조 x,y,z가 있을 때 x.equals(y)가 true이고 y.equals(z)가 true이면 x.equals(z)도 true이다. 바로 위 예제에서 대칭성을 만족 시킨 예제는 추이성을 만족 시키지 못한다. 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이다. 즉 추이성 위반이다. 이 문제는 객체 지향 언어에서 동치 관계를 구현할 때 발생하는 본질적 문제이다. 객체 생성 가능 클래스를 계승하여 새로운 값 컴포넌트를 추가하면서 equals 규약을 어기지 않을 방법은 없다. 따라서 동지관계에 너무 집착하지 않는 게 좋다. 참고로 추이성을 만족 시킬려면 상속 대신 composition을 해야 한다. 


4. 일관성 

: equals를 통해 비교되는 정보에 아무 변화가 없다면 x.equals(y) 호출 결과는 호출 횟수에 상관없이 항상 같아야 한다. 변경 가능한 객체들은 시간에 따라 equals 호출 결과가 다를 수 있다. 하지만 변경 불가능한 객체 사이에서는 일관성이 유지 되야 한다. 그리고 신뢰성이 보장되지 않은 자원들을 비교하는 equals를 구현하지 마라. 일관성이 만족하기 힘들다. 예를 들어 ip 주소를 비교하는 것이다. 네트워크 ip주소는 항상 같다는 보장이 없다. 왠만하면 equals 메서드는 메모리에 존재하는 객체들만 사용하는 게 좋다. 


5. null이 아닌 참조 x에 대해서 x.equals(null)은 항상 false이다.

: equals 메서드에 항상 if(!o instanceof Class))를 기술해야 되는 데 그 이유 중 하나가 Class 가 null 이면 항상 instanceof 구문에서 false가 반환되기 때문이다.


- equals 메서드 구현 지침

1. == 연산자를 사용하여 equals 인자가 자기 자신인지 검사한다.[if(o==this) return true;] 그 후 instanceof 연산자를 이용해 인자의 자료형이 정확한지 검사한다.


2. 중요 필드 각각이 인자로 주어진 객체의 해당 필드와 일치하는지 검사한다. 

: float, double 기본 자료형을 제외하고 ==연산자로 비교한다. float와 double은 compare 메서드를 사용해 비교한다. 배열 필드의 경우 모든 배열 원소마다 비교해야 한다면 Arrays.equals 메서드를 사용한다. 객체 필드 가운데 null 값을 허용하는 것도 있다. 따라서 NullPointException의 발생을 피하기 위해 다음과 같이 한다. 

(field == null ? o.field==null : field.equals(o.field))

field와 o.field가 같을 때가 많다면 다음과 같이 하는 게 성능상 더 빠르다.

(field==o.field || (field!=null && field.equals(o.field))

필드들을 비교할 때는 다를 가능성이 높거나 비교 비용이 낮은 필드부터 비교하는 게 좋다.


3. equals를 구현할 때는 반드시 hashCode도 재정의 한다.

: equals(Object)가 같다고 판정한 두 객체의 hashCode 값은 무조건 같아야 한다. 그렇지 않으면 Hash 기반 콜렉션에서 오작동하게 된다. 위에서 본 Point 클래스의 경우에도 Hash 기반 콜렉션에 객체를 m.put(new Point(1,2), "a"); 코드로 넣고 m.get(new Point(1,2)); 로 해당 객체를 찾을 때 찾지 못하게 된다. hashCode를 재정의하지 않아 서로 다른 put 할 때의 Point객체와 get 할 때의 Point 객체의 hashCode 값이 다르기 때문이다. 이 문제를 해결하기 위해 hashCode 메서드를 구현해야 한다. hashCode 메서드는 다른 객체에는 최대한 다른 해시 코드를 반환하도록 해야 한다. 만약 hashCode가 모든 객체에 대해서 특정 상수를 반환하게 된다면 Hash table의 같은 버킷에 모두 저장되므로 해시 테이블은 연결리스트가 되버린다. 다른 객체에 다른 해시코드를 반환하는 이상적인 해시 함수를 구현하기는 힘들지만 이상적인 해시 함수에 가까운 해시 함수를 만드는 방법은 힘들지 않다. 다음과 같다. 

a. 0이 아닌 상수를 result 라는 이름의 int 변수에 저장한다.

b. 객체 안에 있는 모든 필드에 대해서(equals 메서드가 비교하는 필드) 아래의 절차를 진행한다.

i)해당 필드에 대한 int 해시 코드 c를 계산한다.

- boolean 이면 f?1:0 을 계산한다.

- byte, char, shor, int 면 (int)f를 계산한다.

- long 이면 (int)(f^(f>>>32))를 계산한다.

- float 이면 Float.floatToIntBits(f)를 계산한다.

- double 이면 Double.doubleToLongBits(f)를 계산하고 결과값에 대해 3번 째 long 절차를 수행한다.

- 필드가 객체여서 equals 메서드에서 equals 메서드를 재귀적으로 호출하는 경우 해당 필드의 hashCode 메서드를 재귀적으로 호출해 계산한다.

- 필드가 배열인 경우 각 원소 별로 계산한다. Arrays.hashCode 메서드를 사용해도 된다.

ii) i)절차에서 나온 해시 코드 c에 대해 result = 31*result +c; 를 수행한다.

c. result를 반환한다.

여기서 주의할 점은 성능을 개선하려고 객체의 중요 부분을 해시 코드 계산 과정에서 생략하면 안 된다. 생략하면 hashCode 함수의 속도는 빠를지 몰라도 해시 값 품질이 좋지 않아 해시 테이블 성능이 많이 안 좋아질 수 있다.

+ Recent posts