이번 포스팅은 이펙티브 자바의 아이템 중 "equals는 일반 규약을 지켜 재정의하라."
라는 내용에 대한 것 입니다.
그래서 이번에 Object 클래스의 equals를 재정의하는 방법에 대해 다루어 보겠습니다.
개발을 하다 보면 equals() 메서드를 종종 사용하는 경우가 있습니다.
보통 String 클래스에서 Object 클래스의 equals를 오버라이딩한 것을 자주 사용하는 것 같습니다.
여기서 사용한 코드는 백기선님의 이펙티브 자바 완벽 공략 1부
에 나오는 코드를 사용할 것 입니다.
책에서는 eqauls를 재정의하지 않은 것이 최선이라고 이야기합니다.
이러한 경우는 아래와 같습니다.
equals를 재정의 해야하는 경우는, 논리적으로 같은지를 확인해야 할때 사용합니다.
대표적인 예로 String을 들 수 있습니다.
String str1 = new String("test1")
String str2 = new String("test1")
str1==str2 //false
str1.equals(str2) //true
Value Oject(값 클래스)에서 주로 equals를 재정의 해야합니다. 즉, 객체마다 데이터를 가지는 클래스에서 equals를 사용할 경우 재정의를 해서 논리적인 동치성을 확인해야하는 것입니다.
equals를 재정의할 경우 반드시 따라야 하는 규약이 있습니다.
이것은 equals를 재정의 하는 클래스들은 모두 이 규약을 지켜야한다는 것을 의미합니다.
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
@Override //1
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;
}
}
null이 아닌 모든 참조 값 x,y에 대해 x.equals(y)가 true면 y.equals(x)도 true다.
인데, 이것을 위반하고 있습니다. 즉, CaseInsensitiveString에서 재정의한 equals와 String에서 정의한 equals가 다르게 재정의 되었기 때문입니다.CaseInsensitiveString a = new CaseInsensitiveString("test");
String b = "test";
a.equals(b) // true;
b.equals(a) // false;
아래 처럼, 다시 재정의 해보면 어떨까요??
@Override //2
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
jshell> a.equals(b) //터미널에서 jshell이라는 것을 사용하여 테스트해봤습니다.
$16 ==> false
jshell> b.equals(a)
$17 ==> false
jshell> CaseInsensitiveString a = new CaseInsensitiveString("test");
a ==> CaseInsensitiveString@5e9f23b4
jshell> CaseInsensitiveString b = new CaseInsensitiveString("test");
b ==> CaseInsensitiveString@378fd1ac
jshell> a.equals(b)
$20 ==> true
jshell> b.equals(a)
$21 ==> true
헷갈릴 수 있겠지만 정리해 보겠습니다.
"test"
라는 문자열을 가지고 대칭성에 대해 알아보고 있습니다.
하나는 CaseInsensitiveString 객체의 필드값으로 저장되고, 하나는 String 자체에 저장되었습니다.
null이 아닌 모든 참조 값 x,y에 대해 x.equals(y)가 true면 y.equals(x)도 true다.
라는 조건을 만족해야 합니다.만약 getter를 추가해서 equals를 비교하면,
String c = a.getS();
String b = "test"
c.equals(b); //true
b.equals(c); //true
가 나옵니다. 당연합니다. String 타입에 내용도 같기 때문입니다.
아래 클래스는 x 좌표와 y 좌표를 가집니다.
public class Point {
protected final int x;
protected 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;
}
}
Point를 상속하는 ColorPoint라는 클래스를 만들었습니다.
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
}
만약 ColorPoint에서 equals()를 사용하려면, Point에 재정의한 equals를 사용해야 할까요??
그렇게 된다면, 문제가 있습니다.
ColorPoint에는 Color라는 필드가 있는데 Point의 equals는 Color에 관한 비교를 할 수 없기 때문입니다.
그렇다면, ColorPoint에 Color를 비교할 수 있도록 equals를 재정의해보겠습니다.
그런데, 아래 코드처럼, 작성을 하게 되면, 대칭성이 위반하게 됩니다.
@Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint)o).color == color;
}
//대칭성 위반
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
p.eqauls(cp); // true
cp.eqauls(p); // false
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;
}
null이 아닌 모든 참조 값 x,y,z에 대해, x.equals(y)가 true이고, y.equals(z)도 true면, x.equals(z)도 true다.
인데, 이것을 위반하고 있습니다.ColorPoint p1 = new ColorPoint(1,2,Color.RED);
Point p2 = new Point(1,2);
ColorPoint p3 = new ColorPoint(1,2,Color.BLUE);
p1.eqauls(p2); // true
p2.eqauls(p3); // true
p1.eqauls(p3); // false
그러면, Point 객체에 있는 equals를 새롭게 재정의 해보겠습니다.
// 잘못된 코드 - 리스코프 치환 원칙 위배
@Overrid
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를 상속한 ColorPoint에 필드 값이 존재한다면, equals 규약을 지킬 방법이 없어보입니다.
만약, ColorPoint에 필드가 없다면, Point에서 재정의한 equals를 사용해도 될 것 입니다.
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(Point point, Color color) {
this.point = point;
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.equals(color);
}
}
reference
유익한 글이었습니다.