이펙티브 자바!
equals를 재정의할 때는 일반 규약을 따르자
equals 메서드는 재정의하기 쉬워 보이지만 실수할 여지가 많다.
만약 재정의 하지 않는다면 그 경우에는 자기 자신하고만 같다. 만약 아래 조건이 부합한다면 그래도 된다.
1. 각각의 객체가 고유하다.
2. 클래스에 논리적 동일성 검사 방법이 있건 없건 상관 없다.
3. 상위 클래스에서 재정의한 equals가 하위 클래스에서 사용하기에도 적합하다.
4. 클래스가 private 또는 packing-private선언 되고 equals메서드를 호출할 일이 없다.
equals 메서드는 동치 관계를 구현한다.
1. 반사성 : null이 아닌 참조 x가 있을때 x.equals(x)는 true를 반환한다.
2. 대칭성 : null이 아닌 참조 x와 y가 있을 때 x.equals(y)는 y.equals(x) 가 true일때만 true를 반환한다.
3. 추이성 : null이 아닌 참조 x,y,z가 있을 때 x.equals(y)가 true이고 x.equals(z)가 true면 x.equals(z)도 true이다.
4. 일관성 : null이 아닌 참조 x,y가 있을 때 equals 통해 비교되는 정보에 아무 변화가 없다면 호출 결과는 횟수에 상관 없이 항상 같아야 된다.
5. null 아닌 참조 x에 대하여 x.equals(null)은 항상 false이다.
대칭성에 대해 보자
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;
}
}
의도는 좋으나 일반 문자열과도 호환되도록 하려는 시도를 하고 있다.
예를들어보자
CaseInsensitiveString caseInsensitiveString = new CaseInsensitiveString("Polish");
String s = "polish";
caseInsensitiveString.equals(s)는 true로 반환하지만 s.equals(caseInsensitiveString)는 false로 반환할 것이다.
CaseInsensitiveString는 equals에 String을 알지만 String의 equals는 CaseInsensitiveString을 모른다.
그러므로 CaseInsensitiveString의 equals는 String과 상호작용을 하지 않도록 해야된다.
추이성에 대하 알아보자
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 that = (Point) o;
return that.x == this.x && that.y == y;
}
}
위와 같이 불변의 객체의 있다고 가정하자.
이 클래스를 상속 받아 색상 정보를 추가해 보자.
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 ColorPoint)) {
return false;
}
return super.equals(o) && ((ColorPoint) o).color == color;
}
}
이 메소드의 문제점은 Point객체와 ColorPoint 객체를 비교하는 순서를 바꾸면 다른 결과를 반환한다. 대칭성이 깨진다.
Point point = new Point(1, 2);
ColorPoint colorPoint = new ColorPoint(1, 2, Color.RED);
System.out.println(point.equals(colorPoint));
System.out.println(colorPoint.equals(point));
그럼 대칭성을 갖기 위해 색상정보를 무시하면 어떨까
@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;
}
그럼 대칭성은 되지만 추이성이 깨진다.
Point point = new Point(1, 2);
ColorPoint colorPoint = new ColorPoint(1, 2, Color.RED);
ColorPoint colorPoint1 = new ColorPoint(1, 2, Color.black);
System.out.println(point.equals(colorPoint));
System.out.println(colorPoint.equals(point));
System.out.println(colorPoint.equals(colorPoint1));
나머지는 다 true가 나오지만 마지막은 false가 나온다.
이번엔 일관성이다.
일관성은 객체가 변경되지 않는한 계속 같아야 한다는 것이다. 변경 가능한 객체들간의 동치 관계는 시간에 따라 달라질 수 있지만 변경 불가능한 객체 사이의 동치 관계는 달라질 수 없다.
신뢰성이 보장 되지 않는 자원들은 equals 구현을 삼가하는 편이 낫다.
null에 대한 비 동치성
모든 객체는 null과 동치 관계에 있지 아니 한다. e.equals(null)을 호출해서 우연하게도 true가 나온다면 생각하기 조차도 어려운 것이긴 하나, nullpointexception예외가 발생하는 상황은 그렇지 않다.
상당수 클래스는 equals안에서 null조건을 명시적으로 검사해서 이런 예외가 발생하지 않도록 한다.
if(o == null){
return false;
}
그런데 굳이 이렇게 할 필요 없이 instanceof연산자를 사용해 자료형이 정확한지 검사한다.
if (!(o instanceof ColorPoint)) {
return false;
}
첫번째 피연산자가 null이면 두번째 피연산자의 자료형에 상관 없이 무조건 false 이다.
지금까지 설명한 내용을 종합해보면 아래와 같이 지침들을 따라줘라
- == 연산자를 사용하여 equals의 인자가 자기 자신인지 검사하라.
- instanceof 연산자를 사용하여 인자의 자료형이 정확하지 검사하라.
- equals의 인자를 정확한 자료형으로 변환하라.
- 중요 필드 각각이 인자로 주어진 객체의 해당 필드와 일치하는지 검사하라.
- equals 메서드 구현이 끝냈다면, 대칭성, 추이성, 일관성의 세 속성이 만족되는지 검토하라,
- equals를 구현할 때는 hashcode도 재정의하라.
- 너무 머리 쓰지 마라.
- equals 메서드의 인자형을 Object 에서 다른것으로 바꾸지 마라.
물론 다 설명 하지는 않았지만, 위와 같이 구현을 권장한다.
일이 많다.