Item 10. equals
equals는 일반 규약을 지켜 재정의하라
equals는 재정의하기 쉬워 보이지만, 잘못 재정의하면 프로그램이 오동작할 수 있다. 때문에 필요한 경우가 아니면 재정의하지 않는 것이 좋고, 다음의 상황이면 재정의할 필요가 없다.
각 인스턴스가 본질적으로 고유하다.
인스턴스의 '논리적 동치성(logical equality)'을 검사할 일이 없다.
상위 클래스에서 재정의한 equals가 하위 클래스의 equals에서도 알맞게 동작한다.
클래스가 private이거나 package-private이고, equals 메서드를 호출할 일이 없다.
위 상황이 아니라 논리적 동치성을 검사해야 한다면, 다음의 규약을 따라 재정의해야 한다.
equals 재정의 규약
반사성(reflexivity): null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true다.
대칭성(symmetry): null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
추이성(transitivity): null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y)가 true이고 y.equals(z)도 true면 x.equals(z)도 true다.
일관성(consistency): null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
null-아님: null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.
얼핏보면 당연한 규약들이지만, 실수로 어길 수 있는 규약들이다. 위의 규약들을 어긴 예시들은 아래와 같다.
반사성
악의적인 의도가 없다면 어길 일이 없다.
대칭성
class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.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;
}
}
class Main {
public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
// 대칭성 위배
System.out.println(cis.equals(s)); // true
System.out.println(s.equals(cis)); // false, 다른 타입이기 때문에 false
}
}
추이성
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;
}
}
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) {
// 구현 내용
}
}
위의 ColorPoint 클래스 내의 equals 구현 내용에 따라 규약 위배 여부가 결정된다. 우선 아래와 같이 구현하게 되면 대칭성이 위배된다.
class ColorPoint extends Point {
// ...
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) {
return false;
}
return super.equals(o) && ((ColorPoint) o).color == color;
}
}
class Main {
public static void main(String[] args) {
Point p = new Point(1, 2);
ColorPoint cp1 = new ColorPoint(1, 2, Color.RED);
// 대칭성 위배
System.out.println(p.equals(cp1)); // true
System.out.println(cp1.equals(p)); // false
}
}
이를 수정하여 Point 클래스에 대한 비교를 추가하면 대칭성은 지켜지지만, 추이성이 위배된다.
class ColorPoint extends 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;
}
}
class Main {
public static void main(String[] args) {
Point p = new Point(1, 2);
ColorPoint cp1 = new ColorPoint(1, 2, Color.RED);
ColorPoint cp2 = new ColorPoint(1, 2, Color.BLUE);
// 추이성 위배
System.out.println(p.equals(cp1)); // true
System.out.println(cp1.equals(cp2)); // false
System.out.println(p.equals(cp2)); // true
}
}
만약 instanceof 대신 getClass를 사용하면 추이성은 지켜지지만, 대칭성이 위배된다. 또한 Point의 하위클래스인 ColorPoint가 더이상 Point로써 사용될 수 없게 된다.(리스코프 치환 원칙 위배)
class ColorPoint extends Point {
// ...
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != getClass()) {
return false;
}
ColorPoint cp = (ColorPoint) o;
return super.equals(o) && cp.color == color;
}
}
class Main {
public static void main(String[] args) {
Point p = new Point(1, 2);
ColorPoint cp1 = new ColorPoint(1, 2, Color.RED);
// 대칭성 위배
System.out.println(p.equals(cp1)); // true
System.out.println(cp1.equals(p)); // false
}
}
이와 클래스를 확장하는 경우에는 equals 규약을 지키는 것은 불가능하다고 볼 수 있지만, 우회하는 방법이 있다.
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);
}
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 == color;
}
}
위 방법으로 equals 규약을 지킬 수 있지만, ColorPoint를 Point와는 더이상 상속 관계는 아니게 된다.
일관성
equals의 판안데 신뢰할 수 없는 자원이 끼어들지 않도록 해야 한다.
null-아님
instanceof 연산자로 입력 매개변수가 올바른 타입인지 확인하면 명시적으로 null 검사를 할 필요가 없다. 입력이 null이면 타입 확인 단계에서 false를 반환하므로 null 검사를 명시적으로 하지 않아도 된다.
class Test {
// ...
// 명시적 null 검사
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
// ...
}
// 묵시적 null 검사
@Override
public boolean equals(Object o) {
if (!(o instanceof Test)) {
return false;
}
// ...
}
}
equals 메서드 구현시 주의사항
== 연산자를 사용해 입력이 자기 자신의 참조인지 확인
자기 자신이면 true를 반환
단순한 성능 최적화용으로 비교 작업이 복잡한 상황일 때 좋음
instanceof 연산자로 입력이 올바른 타입인지 확인
가끔 해당 클래스가 구현한 특정 인터페이스를 비교할 수도 있음
이런 인터페이스를 구현한 클래스라면 equals에서 (클래스가 아닌) 해당 인터페이스를 사용해야함
입력을 올바른 타입으로 형변환
2번에서 instanceof 연산자로 입력이 올바른 타입인지 검사 했기 때문에 이 단계에선 오류가 발생하지 않음
입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사
모두 일치해야 true를 반환하도록 구현
기본 타입은 ==로 비교하고 참조타입은 equals로 비교
float, double 필드는 정적 메서드 Float.compare(float, float)와 Double.compare(double, double)로 비교
Float.equals(float)나 Double.equals(double)는 오토 박싱을 수반해 성능상 좋지 않음
배열 필드는 원소 각각을 지침대로 비교
모두가 핵심 필드라면 Arrays.equals()를 사용
NullPointException 발생을 예방하기 위해 Object.equals(object, object)로 비교
필드의 비교 순서를 작은 비용이 드는 필드부터 큰 비용이 드는 필드 순으로 비교
eqauls를 재정의할 땐 hashCode도 반드시 재정의
Last updated
Was this helpful?