결론
꼭 필요한 경우가 아니면 equals를 재정의하지 말자. 재정의해야 할 경우라면 클래스의 핵심 필드 모두, 다섯 가지 규약을 지켜가며 비교하자.
equals 메소드를 재정의하지 않을 경우, 클래스의 인스턴스는 오직 자기 자신과만 같게 된다. 꼭 필요한 경우가 아니라면 재정의하지 말자.
equals를 재정의하면 안 되는 경우
1. 각 인스턴스가 본질적으로 고유할 때
값 클래스(ex. Integer, String 등)가 아닌 동작하는 개체를 표현하는 클래스(ex. Thread)를 말한다.
앞서 말했듯, 이런 클래스의 경우 이미 Object의 equals 메소드에서 구현되어 있다.
2. 인스턴스의 논리적 동치성을 검사할 일이 없을 때
당연한 말이다. 그렇지 않은 경우는 Object의 equals 메소드에 구현돼 있다.
ex. java.util.regex.Pattern은 equals를 재정의해서 두 Pattern의 인스턴스의 정규표현식을 비교
3. 상위 클래스에서 정의한 equals가 하위 클래스에도 딱 들어맞을 때
얘도 당연한 말..
ex. Set 구현체는 AbstractSet, List 구현체는 AbstractList, Map 구현체는 AbstractMap이 구현한 equals를 상속받아 쓴다.
4. 클래스가 private나 package-private이고 equals 메소드를 호출할 일이 없을 때
equals 메소드가 호출되는 걸 막고 싶다면 오버라이딩하여 예외를 던지면 된다.
@Override
public boolean equals(Object obj) {
throw new AssertionError();
}
equals를 재정의해야 하는 경우
: 객체 식별성이 아닌 논리적 동치성을 비교할 때/상위 클래스에서 의도에 맞게 재정의되지 않았을 때
예시로 값 클래스(ex. Integer, String)가 있다. 그러나 인스턴스 통제 클래스(아이템 1)나 Enum(아이템 34)라면 equals를 재정의하지 않아도 된다. 논리적으로 같은 인스턴스가 2개 이상 만들어지지 않으므로 논리적 동치성과 객체 식별성이 동일하게 된다.
equals 메소드 재정의 일반 규약 : 동치관계
반사성
null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true
대칭성
null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)가 true 👉 y.equals(x)가 true
추이성
null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y)가 true && y.equals(z)가 true 👉 x.equals(z)가 true
일관성
null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하여도 항상 true만 혹은 false만 반환한다.
null-아님
null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false
이제 하나씩 살펴보자..
반사성
객체는 자기 자신과 같아야 한다. 억지로 어기지 않는다면 위반되지 않는다.
대칭성
아래 코드는 대칭성을 위반한다. CaseInsensitiveString의 equals 메소드는 재정의되었기 때문에 true를 반환하지만, String의 equals 메소드는 CaseInsensitiveString를 컨트롤할 수 없으므로 false를 반환한다.
public final 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;
}
}
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
cis.equals(s); // true
s.equals(cis); // false
또한 대칭성이 위반된 상태에서 contains 메소드를 호출하면 즉, containsequals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다.
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
list.contains(s); // true? false? 알 수 없음
이를 해결하기 위해선 CaseInsensitiveString끼리만 비교하도록 하면 된다.
@Override public boolean equals(Object o){
return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
*equalsIgnoreCase()
대소문자 구분 없이 비교해주는 메소드
추이성
상위 클래스를 상속받아 새로운 필드를 추가할 때 추이성을 자주 위반한다.
구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
아래 예시를 보자.
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;
}
}
재정의 1 👉 대치성 위배
@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.equals(cp); // true
cp.equals(p); // false
재정의 2 : ColorPoint.equals와 Point를 비교할 때는 색상을 무시 👉 추이성 위배, 무한 재귀
아래 코드는 추이성을 위반한다.
@Override public boolean equals(Obejct o){
if(!(o instanceof Point))
return false;
if(!(o instanceof ColorPoint)) // Point일 경우
return o.equals(this);
return super.equals(o) && ((ColorPoint) o).color == 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); // true
p2.equals(p3); // true
p1.equals(p3); // false
심지어 아래 코드는 무한 재귀를 일으킨다.
//SmellPoint.java의 equals
@Override public boolean equals(Obejct o){
if(!(o instanceof Point))
return false;
if(!(o instanceof SmellPoint))
return o.equals(this);
return super.equals(o) && ((SmellPoint) o).color == color;
}
public static void main(){
ColorPoint p1 = new ColorPoint(1,2, Color.RED);
SmellPoint p2 = new SmellPoint(1,2);
p1.equals(p2);
// 1. ColorPoint의 equals: 2번째 if문 때문에 SmellPoint의 equals로 비교
// 2. SmellPoint의 equals: 2번째 if문 때문에 ColorPoint의 equals로 비교
// 3. 1~2 무한 재귀로 인한 StackOverflow Error
}
재정의 3 👉 리스코프 치환 원칙 위배
해당 방법은 instanceof를 getClass로 바꾼 것이다. 즉, 같은 클래스의 객체와 비교할 때만 true를 반환한다.
Point의 하위 클래스는 여전히 Point이므로 어디서든 Point로 활용될 수 있어야 한다는 리스코프 치환 원칙에 위반된다.
@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
객체가 속한 클래스의 정보를 나타내는 Class 객체를 반환하는 Java의 Object 클래스의 메소드이다.
*리스코프 치환 원칙(LSP)
객체 지향 프로그래밍에서 하위 클래스는 상위 클래스의 대체 가능성을 유지해야 한다는 원칙이다. 다시 말해 하위 클래스의 객체로 대체해도 기존의 프로그램이 오류 없이 동작해야 한다. 이는 객체 지향 프로그래밍에서 코드의 재사용성과 유지보수성을 높인다.
equals의 규약을 지키면서 구체 클래스의 하위 클래스에 값을 추가할 수 있도록 우회하는 방법
방법 1. 상속 대신 컴포지션을 사용하라(아이템 18)
Point를 상속하는 대신 Point를 ColorPoint의 private 필드로 두고, ColorPoint와 같은 위치의 일반 Point를 반환하는 뷰 메소드(아이템 6)를 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);
}
public Point asPoint(){ // ColorPoint의 Point 뷰를 반환한다.
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);
}
}
i) ColorPoint : ColorPoint
ColorPoint의 equals로 color까지 비교
ii) ColorPoint : Point
ColorPoint의 asPoint로 Point로 변환돼 Point의 equals를 이용해 x, y 비교
iii) Point : Point
Point의 equals로 x, y 비교
cf. 추상 클래스의 경우 equals 규약을 지키면서 값을 추가할 수 있다.
*컴포지션
기존 클래스가 새로운 클래스의 구성 요소로 쓰인다. 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 된다. 컴포지션을 통해 새 클래스의 인스턴스 메소드는 기존 클래스에 대응하는 메소드를 호출해 그 결과를 반환한다.
일관성
가변 객체는 비교 시점에 따라 달라질 수도 있으나, 불변 객체는 항상 일관적이여야 한다.
일관성을 지키기 위해선 가변이든 불변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 하면 안 된다.
ex. java.net.URL의 equals는 주어진 URL과 매핑된 호스트의 IP 주소를 이용해 비교한다. 호스트 이름을 IP 주소로 바꾸려면 네트워크를 통해야 하므로 일관성에 위배된다.
따라서 equals는 항시 메모리에 존재하는 객체만을 사용한 결정적 계산만 수행해야 한다.
null-아님
:모든 객체가 null과 같지 않아야 한다.
아래처럼 명시적으로 null하는 것은 좋지 않다.
@Override
public boolean equals(Object o) {
if(o == null) {
return false;
}
}
아래처럼 올바른 타입인지 검사하자.
@Override
public boolean equals(Object o) {
if(!(o instanceof MyType)) {
return false;
}
MyType myType = (MyType) o;
}
총정리 : 양질의 equals 메소드 구현 방법
1. == 연산자를 이용해 자기 자신의 참조인지 확인한다.
2. instanceof 연산자로 입력이 올바른 타입인지 확인한다. 그렇지 않다면 false를 반환한다.
만약 특정 인터페이스로 구현한 서로 다른 클래스끼리 비교하고 싶다면, equals에서 해당 인터페이스를 사용해야 한다.
ex. Set, List, Map, Map.Entry 등의 컬렉션 인터페이스
3. 입력을 올바른 타입으로 형변환한다.
4. 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 하나씩 검사한다.
만약 인터페이스를 사용했다면, 입력의 필드 값을 가져올 때도 그 인터페이스의 메소드를 사용해야 한다.
+ 꿀팁
◾ 기본 타입 필드(float, double 제외)
== 연산자로 비교
◾ 참조 타입 필드
equals 메소드로 비교
◾ float, double 필드
정적 메소드인 Float.compare(float, float)/Double.compare(double, double)로 비교
cf. Float.equals와 Double.equals 메소드는 오토박싱을 수반하여 성능상 좋지 않다.
◾ 배열 필드
원소 각각 지침대로 비교한다. 모든 원소가 핵심 필드라면 Arrays.equals 메소드를 사용하자.
◾ null도 정상 값으로 취급하는 참조 타입 필드
정적 메소드인 Objects.equals(Object, Object)로 비교해 NullPointerException 발생을 예방하자.
◾ 비교하기 복잡한 필드를 가진 클래스
필드의 표준형을 저장한 후 표준형끼리 비교하자. 특히 불변 클래스(아이템 17)에 효과적이다. 가변 클래스라면 값이 변할 때마다 표준형을 갱신해줘야 한다.
◾ 필드의 비교 순서는 equals의 성능을 좌우한다.
다를 가능성이 크거나 비교하는 비용이 싼 필드를 먼저 비교하자.
파생 필드가 객체 전체의 상태를 대표하는 경우, 파생 필드부터 비교
◾ equals를 재정의할 땐 hashCode도 반드시 재정의하자(아이템 11)
◾ 너무 복잡하게 해결하려 들지 말자.
◾ Object 외의 타입을 매개변수로 받는 equals 메소드는 선언하지 말자.
아래 코드는 재정의가 아니라 다중정의(아이템 52)한 것이다. @Override 어노테이션을 일관적으로 사용하면 이러한 실수를 예방할 수 있다.(아이템 40)
public boolean equals(MyClass o) {
}
AutoValue 프레임워크
: equals(hashCode도 가능)의 테스트를 대신해주는 오픈소스
@AutoValue 어노테이션만 추가하면 된다. 클래스가 수정되더라도 자동으로 알아채지 못한다는 단점이 있으나 상당히 편리하다.
저의 이해를 도와준 포스트..
[이펙티브 자바] 아이템 10. equals는 일반 규약을 지켜 재정의 하라
equals를 다 구현했다면 세 가지만 자문해보자.대칭적인가? 추이성이 있는가? 일관적인가?equals 메서드를 재정의하지 않고 그냥 두면, 그 클래스의 인스턴스는 오직 자기 자신과만 같게 된다. 각
velog.io

'Language > Java' 카테고리의 다른 글
[effective java] 아이템 12. toString을 항상 재정의하라. (0) | 2023.03.09 |
---|---|
[effective java] 아이템 11. equals를 재정의하려거든 hashCode도 재정의하라. (0) | 2023.03.09 |
[effective java] 아이템 9. try-finally보다는 try-with-resources를 사용하라. (0) | 2023.03.08 |
[effective java] 아이템 8. finalizer와 cleaner 사용을 피하라. (0) | 2023.03.08 |
[effective java] 아이템 7. 다 쓴 객체 참조를 해제하라. (0) | 2023.03.04 |