// eqauls 호출을 막는 매서드
@Overide
public boolean eqauls(Object object) {
throw new AssertionError(); // 호출 금지
}
예시
public final class CaseInsensiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Object.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;
}
해당 클래스는 equals에서 대소문자를 무시한다.
String의 equals와 CaseInsensitiveString의 equals 는 다른 값을 반환한다.
해당 클래스의 equals를 String과 연동시키겠다는 생각을 버려야한다.
결과
@Overide
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.eqaulsIgnoreCase(s);
}
상위 클래스에는 없는 새로운 필드를 하위 클래스에 추가하는 상황
// 상위 클래스 Point
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;
}
... // 나머지 코드 생략
}
eqauls를 수정하지 않으면 Point의 구현이 상속되어서 색상 정보를 무시한 채 비교한다.
// 잘못된 코드 - 대칭성 위배
@Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint)) return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
이 메서드는 point.equals(colorPoint)와 colorPoint.equals(Point)의 결과가 다를 수 있다.
Point의 equals는 색상을 무시하고, ColorPoint의 equals는 입력 매개변수의 클래스 종류가 다르다며 매번 false만 반환할 것이다.
colorPoint.equals(point)가 true를 반환하도록 하면 해결될까? - colorPoint의 색상 정보를 무시하도록
// 잘못된 코드 - 추이성 위배
@Override
public boolean equals(Object o){
if(!(o instanceof Point)) return false;
// o가 일반 Point이면 색상 무시 후 비교
if(!(o instanceof ColorPoint)) return o.equals(this);
// o가 ColorPoint이면 색상까지 비교
return super.equals(o) && ((ColorPoint) o).color = color;
}
이 방식은 대칭성은 지켜주지만, 추이성을 깬다.
또한, 이 방식은 무한 재귀에 빠질 위험도 있다.
💡 Point의 하위클래스 SmellPoint를 만들고 eqauls는 같은 방식으로 구현한다 그런 다음 **myColorPoint.equals(mySmellPoint)**를 호출하면 StackOverflowError를 일으킨다.@Override
public boolean equals(Object o) {
if(o==null ||
리스코프 치환 원칙(Liskov substitution principle)에 따르면, 어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요하다. → 그 타입의 모든 매서드가 하위 타입에서도 똑같이 잘 작동해야 한다.
다른 방법 → 상속 대신 컴포지션을 사용하라
// 컴포지션으로 구현한 예시
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color){
point = new Point();
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);
}
// .... 나머지 코드 생략
}
일관성은 두 객체가 같다면 앞으로도 영원히 같아야 한다는 뜻이다.
가변 객체는 비교 시점에 따라 서로 다를 수도 혹은 같을 수도 있는 반면, 불변 객체는 한번 다르면 끝까지 달라야 한다.
클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안된다.
이런 문제를 피하려면 equals는 항시 메모리에 존재하는 객체만을 사용한 결정적(deterministic) 계산만 수행해야 한다.
null-아님 모든 객체가 null과 같지 않아야 한다는 뜻이다. NullPointerException을 던지는 경우조차 허용하지 않는다.
// 명시적 null 검사 - 필요 없다!
@Override
public boolean equals(Object obj) {
if(obj == null){
return false;
}
...
}
이러한 검사는 필요치 않다.
동치성을 검사하려면 equals는 건네받은 객체를 적절히 형변환한 후 필수 필드들의 값을 알아내야 한다. 그러려면 형변환에 앞서 instanceof 연산자로 입력 매개변수가 올바른 타입인지 검사해야한다.
@Override public boolean equals(Object o){
if(!(o instanceof MyType))
return false;
MyType mt = (MyType) o;
...
}
equals가 타입을 확인하지 않으면 잘못된 타입이 인수로 주어졌을 때 ClassCastException을 던져서 일반 규약을 위배하게 된다.
그런데 instanceof는 첫 번째 피 연산자가 null이면 false를 반환한다.
따라서 입력이 null이면 타입 확인 단계에서 false를 반환하기 때문에 null 검사를 명시적으로 하지 않아도 된다.