
꼭 필요한 경우가 아니라면 equals를 재정의하는 것이 좋지 않다.
equals를 재정의하지 않는 것이 좋은 상황은 대표적으로 아래 4가지 상황이 있다.
1. 각 인스턴스가 본질적으로 고유하다.
@Override
public boolean equals(Object o) {
throw new AssertionError(); // 호출 금지
}Integer, String)처럼 “논리적 동치성”을 비교해야 하는 클래스는 재정의가 필요하다.Object의 equals는 동치관계(equivalence relation)를 구현해야 하며, 다섯 가지 요건을 만족한다.
x.equals(x)는 truex.equals(y)가 true면, y.equals(x)도 truex.equals(null)은 falsepublic final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requiredNonNull(s);
}
// 잘못된 equals (대칭성 위배)
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString) {
// 같거나 다른 CaseInsensitiveString이면 대소문자 구분 없이 비교
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
if (o instanceof String) {
// 만약 o가 일반 String이면, 이것도 case-insensitive로 비교
return s.equalsIgnoreCase((String) o);
}
return false;
}
// ...
}
o instanceof CaseInsensitiveString → CaseInsensitiveString 객체라면,this.s.equalsIgnoreCase(((CaseInsensitiveString) o).s)로 비교한다.o instanceof String → o가 일반 String이면,this.s.equalsIgnoreCase((String) o)로 비교CaseInsensitiveString vs String 비교도 대소문자 무시로 equals를 true/false 판단한다.false를 반환한다.대칭성은 x.equals(y)가 true면, y.equals(x)도 true여야 한다.
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String str = "";
System.out.println(cis.equals(str)); // A
System.out.println(str.equals(cis)); // B
cis.equals(str) (A 라인)o instanceof String 분기에 해당한다."Polish".equalsIgnoreCase("polish") → truecis.equals(str) → truestr.equals(cis) (B 라인)str은 단순한 String 객체 → String의 equals는 같은 문자열 타입이고, 동일한 내용인지 검사한다.cis는 타입이 CaseInsensitiveString이기 때문에 타입이 다르다.str.equals(cis) → false"polish".equals( CaseInsensitiveString ) → falseA라인에서는 true를 반환, B라인에서는 false를 반환하기 때문에 대칭성이 위배된 것이다.
이 문제를 해결하려고 String과도 연동하려고 하면 안된다.
아래와 같이 해결하는 정도로 equals를 오버라이딩 해야한다.
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
이렇게 하면 상대가 CaseInsensitiveString인지 확인한 뒤에 CaseInsensitiveString로 캐스팅하고 대소문자를 무시하고 비교해서 대칭성을 지킬 수 있다.
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;
}
// 잘못된 equals (대칭성은 지키지만 추이성 위배)
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
// 만약 o가 Point이지만 ColorPoint는 아니면,
// o.equals(this)로 역방향 비교를 시도해서 색상을 무시하고 비교하게 된다.
if (!(o instanceof ColorPoint)) {
return o.equals(this);
}
// o가 ColorPoint면, 색상까지 비교
ColorPoint cp = (ColorPoint) o;
return super.equals(cp) && color.equals(cp.color);
}
}
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); // A
p2.equals(p3); // B
p1.equals(p3); // C
p1.equals(p2) (A라인)p1.equals(p2) -> trueColorPoint 내부에서 o가 Point면 o.equals(this) 호출한다.p2.equals(p1)은 x,y만 비교p2.equals(p3) (B라인)p2.equals(p3) -> truePoint 객체p1.equals(p3) (C라인)p1.equals(p3) -> falseColorPoint인 경우에는 색상까지 비교하는데 p1은 RED, p3는 BLUE로 색상이 다르다.@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;
}
getClass() != getClass()o의 실제 클래스가 나의 클래스와 정확히 일치해야만 equals가 true로 나오게 된다.Point와 ColorPoint(하위 클래스)가 절대 같다고 나오지 않게 된다.Point를 상속한 ColorPoint가 x,y만 같으면 같다고 생각하고 싶어도 클래스가 다르다는 이유로 equals가 false가 돼버리게 된다.ColorPoint가 Point를 확장하므로 상위/하위 클래스 간에 equals가 안되게 되는 것이므로 합성(컴포지션) 방법으로 우회하는 방법이다.
public class ColorPoint {
private final Point point; // 합성
private final Color color;
public ColorPoint(int x, int y, Color color) {
this.point = new Point(x, y); // private 필드로 갖고있음
this.color = Objects.requireNonNull(color);
}
// 만약 Point의 x,y로 뭔가 계산이 필요하면 point 필드를 사용
// 뷰 메서드
public Point asPoint() {
return this.point; // 일반 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);
}
// ...
}
public Point asPoint())를 사용.ColorPoint가 내부적으로 Point를 속성(필드)으로만 두고asPoint()로 일반 Point를 뷰로써 반환하게 된다.equals 충돌)가 사라진다.ColorPoint는 Point를 상속하지 않기 때문에Point인지 아닌지로 인한 equals 충돌이 없어진다.Point와 ColorPoint를 동일 타입으로 다룰 일이 없게된다.ColorPoint만의 로직으로 가능해진다.ColorPoint는 색상까지 포함한 비교와 같이 어떤 식으로 equals를 구현할지 스스로 결정하게된다.Point는 이 과정에 대해 아무것도 모른다(별개의 클래스).== 연산자를 사용해 입력이 자기 자신의 참조인지 확인if (this == o) return true;instanceof 연산자로 입력이 올바른 타입인지 확인if (!(o instanceof MyClass)) return false;MyClass other = (MyClass) o;trueObjects.equals(a, b)을 통해 NullPointException 예방equals 재정의할 때 hashCode()도 재정의!Object -> @Override하면 실수 예방 가능(https://mvnrepository.com/artifact/com.google.auto.value/auto-value/1.10.1)
AutoValue 라이브러리를 쓰면 equals, hashCode, toString 등을 자동으로 만들어준다고 한다.AutoValue_MyClass 클래스가 생성되어, 올바른 equals가 구현된다.AutoValue 프레임워크를 사용하면 편리하게 작성이 가능하다.