개발하는 두더지

[Effective Java 규칙8] equals를 재정의할 때는 일반 규약을 따르라 본문

Java,Android

[Effective Java 규칙8] equals를 재정의할 때는 일반 규약을 따르라

덜지 2018. 9. 14. 10:28

[Effective Java 규칙8] equals를 재정의할 때는 일반 규약을 따르라

Effective Java 2/E 책과 구글링을 통해 내용을 정리하고 개인적인 견해가 포함된 글입니다.


Object는 객체 생성이 가능한 클래스이지만 기본적으로 상속하여 사용하도록 설계된 클래스이다

Object에 정의된 비 final 메서드 equals, hashCode, toString, clone, finalize는 재정의하도록 설계된 메서드이기때문에

재정의하여 사용해야한다



위 메서드를 사용하는 클래스는 일반 규약을 따라야하는데 HashMap, HashSet처럼 해당 규약에 의존하는 클래스와 함께 사용하면 문제가 생긴다

이번 챕터에서는 위 메서드들을 언제 어떻게 재정의해야하는지를 다룬다.


1. == 연산자를 사용하여 equals 의 인자가 자기 자신인지 검사한다. 단순히 성능 최적화를 위한 것이며, 객체 비교 오버헤드가 클 경우에 위력을 발휘한다

2. instanceof 연산자를 사용하여 인자의 자료형이 정확한지 검사한다. 

3. equals의 인자를 정확한 자료형으로 변환하라.

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

(프로그래밍 이디엄 programming idiom : 프로그래밍 언어에 내장되어 있지 않은 간단한 작업 또는 알고리즘을 기술하는 방법으로 프로그래밍 디자인 패턴의 일종이다. 일반적으로 프로그래밍 언어에서 사용되는 이디엄을 이해하고 사용할 줄 아는 것은 해당 언어에 얼마나 능숙한지 판단하는 기준이 된다.)

5. equals 메서드 구현을 끝냈다면, 대칭성, 추이성, 일관성의 세 속성이 만족하는지 검토하라

equals 메서드를 재정의할 때 준수해야하는 일반적인 규약이다

- 대칭성  :  두 객체에게 서로 같은지 물어보는 것이다. 같은 클래스, 인터페이스끼리 비교할때는 문제가 없어보이지만 실수를하면 쉽게 깨질 수 있다.

public final class CaseInsensitiveString {
private final String s;

public CaseInsensitiveString(String s) {
if(s == null)
throw new NullPointerException();
this.s = 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;
}
}

위와 같이 대소문자를 비교하는 클래스가 있다고 하자. 자신의 클래스 외에 String 객체도 비교할 수 있지만 오히려 String 클래스는 CaseInsensitiveString의 존재를 모른다.

public class MyClass {

public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString("Test");
String s = "test";

System.out.println(cis.equals(s));
System.out.println(s.equals(cis));
}
}

값을 비교해보면 true, false가 나온다. 즉 대칭성이 깨진다는 말이다.

List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
System.out.println(list.contains(s));

또한 List에 추가하여 contains 메서드를 이용해 CaseInsensitive객체가 아닌 String 객체로 값을 포함하고 있는지 확인하면 JVM에 따라 다른 값이 나올 수 있다.

true가 반환될 수도, false가 반환될 수도, exception이 발생할 수도있다. 즉, equals가 따라야할 규칙을 어기면 어떻게 행동하게 될지 예측할 수가 없다.


- 추이성  :  A와 B가 같고 B와 C가 같으면 A와 C가 같아야 한다. 수학적으로 당연한 얘기지만 코드를 잘못짜면 이와 같은 규칙도 쉽게 실수한다.

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

2차원 공간상의 점을 나타내는 클래스가 있다.


public class ColorPoint extends Point {
private final Color color;

public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}

@Override
public boolean equals(Object o) {
if(!(o instanceof Point))
return false;

if(!(o instanceof ColorPoint))
return o.equals(this);

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);
System.out.println(p1.equals(p2));
System.out.println(p2.equals(p3));
System.out.println(p1.equals(p3));

equals 메서드를 재정의하여 비교를하면 true, true, false가 발생한다. 즉 A=B=C 인 추이성이 깨지게 된다.

클래스를 상속하여 필드를 추가하면서 equals 규약을 어기지 않을 방법은 없다. 그럼 어떻게 해야 해결할 수 있을까?

instanceof 대신 getClass 메서드를 사용하면 필드를 추가하더라도 equals 규약을 준수할 수 있다고 하지만 추천하는 방식은 아니다.


바로 나중에 나올 챕터에서 살펴볼 "상속(extends)하지말고 구성(implement)하라" 규칙을 따르는 것이다.

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

public ColorPoint(int x, int y, Color color) {
if(color == null)
throw new NullPointerException();
point = new Point(x, y);
this.color = 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);
}
}


자바 기본 라이브러리 중에 객체 생성 가능한 클래스를 상속하여 필드를 추가한 클래스도 있다. 바로 Timestamp 클래스인데

Date 클래스를 상속하여 nanoseconds 필드를 추가했다. Timestamp 클래스의 equals는 대칭성을 위반하므로 Timestamp와 Date 객체를 같은 컬렉션에 보관하거나 섞어쓰면 문제가 생길 수 있다. 그래서 Timestamp 클래스 주석에 함께 사용하지말라고 경고가 표시되어 있다. 

이처럼 실수로 섞어쓰는 것은 디버깅하기 어려운 문제가 발생할 수 있으므로 이런 방식은 절대로 따라해서는 안된다.


(리스코프 대체 원칙 Liskov substitution principle 은 어떤 자료형의 중요한 속성은 하위 자료형에도 그대로 유지되어서, 그 자료형을 위한 메서드는 하위 자료형에서도 잘 동작해야 하는 원칙이다)


- 일관성 :  일단 한번 같다고 판정되면 값이 변경되지 않는 한 항상 같아야 한다는 규칙이다.

또한 신뢰성이 보장되지 않는 값을 비교하는 equals를 구현하는 것은 하지말아야 한다. 일례로 URL 클래스의 equals 는 URL에 대응되는 호스트의 IP 주소를 비교하여 반환값을 결정하는데 문제는 호스트명을 IP주소로 변환하려면 네트워크에 접속하여 값을 구해야하므로 항상 같은 결과가 나온다는 보장이 없다. 호환성 문제로 인해 앞으로 개선될 여지가 없는 클래스 이지만 equals의 규약을 준수하지 못하는 클래스이다.



참고

프로그래밍 이디엄 뜻

Comments