"equals는 일반 규약을 지켜 재정의하라"
아래 경우중 하나라도 해당한다면 재정의하지 않는 것이 좋다.
java.util.regax.Pattern
은 equals를 재정의해 두 Pattern의 정규표현식을 비교Set
은 AbstractSet
이 구현한 equals를 상속받아 사용List
는 AbstractList
, Map
은 AbstractMap
@Override public boolean equals(Object o) {
throw new AssertionError(); // 실수로라도 호출될 일이 없도록 방지
}
객체 식별성(object identity, 두 객체가 물리적으로 같은가)가 아니라 논리적 동치성을 확인해야 하며, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되어 있지 않았을 때이다.
equals가 논리적 동치성을 비교하도록 재정의하면 Map의 키와 Set의 원소로 사용할 수 있게 된다.
equals 메서드는 동치관계를 구현하며 아래 조건들을 만족한다.
x.equals(x) == true
x.equals(y) == true
면 y.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;
}
x.equals(y) == true
이고 y.equals(z) == true
면, x.equals(z) == true
x.equals(y)
를 반복해서 호출하면 항상 true거나 항상 false를 반환x.equals(null) == true
동치 관계: 집합을 서로 같은 원소들로 이뤄진 부분집합으로 나누는 연산
이 부분집합을 동치류(equivalence class, 동치클래스)라고 한다.
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
}
}
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);
}
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;
}
}
@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
를 상속하는 대신 Point
를 ColorPoint
의 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를 이용해 색상 값까지 비교ColorPoint
와 Point
비교: ColorPoint
의 asPoint 메서드를 이용해 Point
로 바꾸고 Point
의 equals를 이용해 x, y를 비교Point
끼리 비교: Point
의 equals로 x, y 비교해결 - 추상클래스의 하위클래스 사용하기
추상 클래스(상위 클래스)의 인스턴스를 만드는 것이 불가능하기 때문에 하위 클래스끼리 비교가 가능하다.