🎯 학습 목표
- Equals의 재정의
- 고려할 규약들
📌 Equals의 재정의
equals
를 오버라이딩하는 것은 NPE를 던지거나 원하는 대로 작동하지 않을 가능성이 높다.
재정의를하지 않고 그대로 equals
를 사용하게 된다면 인스턴스는 자기 자신만 같다고 표현한다.
✍️ 동일성 vs 동등성 : 동일성은 같은 참조인지 동등성은 같은 값을 가지는지 판단
🤔 언제 하지 말아야 할까?
1️⃣ 각 인스턴스가 본질적으로 고유
값이 아닌 동작을 나타내는 인스턴스는 같은 인스턴스가 애초에 없다. (Ex. Thread) Object의 equals
로 충분하다.
2️⃣ 인스턴스의 논리적 동치성을 검사할 일이 없다.
논리적 동치성을 검사할 일이 없다는 것은 단순 동일성을 검사한다는 의미이다. Object의 equals
로 충분하다.
3️⃣ 이미 상위 클래스에서 재정의한 Equals가 존재
상위 클래스에 이미 정의된 equals
가 존재한다면 굳이 재정의할 필요가 없다.
4️⃣ 클래스가 private이거나 package-private이면 Equals를 호출할 일이 없다.
혹시라도 equals
의 호출을 막고 싶으면 다음과 같이 구현하면 된다.
@Override
public boolean equals(Object o) {
throw new AssertionError(); // 호출 금지!
}
🧐 이럴 때 재정의하기
객체 식별성(동일성)이 아니라 논리적 동치성(동등성)을 비교하는 경우에는 equals
를 오버라이딩해야 한다.
또한, 상위 클래스에서 재정의되지 않은 경우도 오버라이딩을 해야 한다.
단, 인스턴스가 단 하나만 만들어지는 경우는 비교할 일이 없기 때문에 equals
를 재정의할 필요 없다.
📌 고려할 규약들
1️⃣ 반사성
객체는 자기 자신과 같아야 한다. 이 조건을 만족하지 않기가 더 힘들다.
2️⃣ 대칭성
두 객체의 결과는 같아야 한다. 객체 x, y에 대해서 x.equals(y)
가 참이면 y.equals(x)
가 참이어야 한다.
책에서 제공된 예시는 다음과 같다.
public final class CaseInsensitiveString {
private final String str;
public CaseInsensitiveString(String str) {
this.str = Objects.requireNonNull(str);
}
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString){
return str.equalsIgnoreCase(((CaseInsensitiveString)o).str);
}
if (o instanceof String) { // 대칭성 위배
return str.equalsIgnoreCase((String) o);
}
return false;
}
}
오버라이딩된 equals
를 보면 String
를 고려해서 작성하지만 이는 명백히 대칭성 위반이다.
CaseInsensitiveString
은 String
을 알고 있지만 반대로 String
은 모른다. 따라서 다음과 같은 상황이 발생한다.
CaseInsensitiveString cis = new CaseInsensitiveString("hejow");
String s = "hejow";
cis.equals(s); // true
s.equals(cis); // false
대칭성을 위반했기 때문에 나아가 다음과 같은 코드에서도 문제가 발생한다.
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
list.contains(s); // true or false
JDK에 따라서 contains()
가 다른 결과를 반환할 수 있다.
이렇게 equals 규약을 어기면 그 객체를 사용할 때 다른 객체가 어떻게 반응할지 알 수 없다.
따라서 다음과 같이 대칭성을 만족시켜서 구현해야 한다.
@Override
public boolean equals(Object obj) {
return obj instanceof CaseInsensitiveString &&
((CaseInsensitiveString) obj).str.equalsIgnoreCase(str);
}
3️⃣ 추이성
추이성은 일종의 삼단논법이다. 인스턴스 x, y, z에 대해서 x와 y가 같고, y와 z가 같다면 x와 z도 같아야 한다.
책에서 제공된 예제를 보고 이해해 보자.
public class Point {
private final long x ;
private final long y;
public Point(long x, long y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
}
public class ColorPoint extends Point{
private final Color color;
public ColorPoint(long x, long y, Color color) {
super(x, y);
this.color = color;
}
}
Point
에 정의된 equals
를 사용하면 대칭성과 반사성은 성립된다. 하지만 추가된 색상까지는 비교할 수 없다.
Point point = new Point(0,1);
ColorPoint bluePoint = new ColorPoint(0, 1, Color.BLUE);
point.equals(bluePoint); //true
bluePoint.equals(point); //true
색상까지 비교하기 위해서 ColorPoint
에 equals
를 다음과 같이 재정의한다.
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
public static void main(String[] args) {
Point point = new Point(0,1);
ColorPoint bluePoint = new ColorPoint(0, 1, Color.BLUE);
ColorPoint redPoint = new ColorPoint(0, 1, Color.RED);
bluePoint.equals(point); // true
point.equals(redPoint); // true
bluePoint.equals(redPoint); // false
}
비교해 보면 대칭성은 만족하나 추이성이 깨지는 것을 볼 수 있다.
이와 같이 자식 클래스에서 추가된 필드를 포함하면서 equals 규약을 지킬 수 없다.
🤔 하위 클래스의 필드도 비교할 수 없을까?
상속 대신 컴포지션을 사용한다면 하위 클래스의 필드도 비교할 수 있다.
public class ColorPointFromComposition {
private final Point point;
private final Color color;
public ColorPointFromComposition(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
// (아이템6) Point 뷰를 반환한다.
public Point asPoint() {
return point;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPointFromComposition)) {
return false;
}
ColorPointFromComposition cp = (ColorPointFromComposition) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
4️⃣ 일관성
두 객체가 같다면 영원히 같아야 한다. 시간이 흐르거나 호출 횟수가 많아져도 결과가 달라지면 안 된다는 뜻이다.
특히, 불변 클래스는 항상 동일해야 한다. 그리고, equals()
의 반환 결과에 신뢰할 수 없는 자원이 들어가서는 안된다.
예시로, java.net.URL
의 equals
는 주어진 URL과 매핑된 호스트의 IP 주소를 통해 비교한다.
호스트 이름을 IP 주소로 바꾸려면 네트워크를 통해야 하는데 결과가 항상 같음을 보장할 수 없다.
5️⃣ Not-null
모든 객체는 null
과 같지 않아야 한다. 즉, 모든 객체는 객체.equals(null)
은 거짓이어야 한다.
앞서 말했듯이 equals()
를 잘못 사용하면 NPE
이 발생한다. 따라서 NPE
에 대해서도 안전해야 한다.
그렇다고 객체와 null
을 비교하지 말고 instanceof
를 사용하면 된다.
// 직접 확인하기 보단
@Override
public boolean equals(Object o) {
if (o == null) return false;
}
// instanceof로 확인하자
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
}
😎 정리
equals
를 재정의할 때 발생할 수 있는 이슈들이 있다.
해당 이슈들을 피하기 위해서는 굳이 필요 없는 상황을 피해야 하고, 제시된 규약을 잘 따라야 한다.
재정의해야 할 상황이라면 다음 Step들을 지키자!
==
연산자로 반사성을 확인instanceof
연산자로 입력이 올바른 타입인지 확인- 입력을 올바른 타입으로 형변환
- 입력 객체와 자기 자신의 대응되는 핵심필드들이 모드 일치하는지 하나씩 검사
equals
를 재정의할 때는hashCode
도 반드시 재정의
😋 지극히 개인적인 블로그지만 훈수와 조언은 제 성장에 도움이 됩니다 😋
'개인 공부 > Java (이펙티브 자바)' 카테고리의 다른 글
[이펙티브 자바] 아이템 15 : 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2023.02.08 |
---|---|
[이펙티브 자바] 아이템 13 : clone 재정의는 주의해서 진행하라 (0) | 2023.02.01 |
[이펙티브 자바] 아이템 7 : 다 쓴 객체 참조를 해제하라 (0) | 2023.01.31 |
[이펙티브 자바] 아이템 6 : 불필요한 객체 생성을 피하라 (0) | 2023.01.15 |
[이펙티브 자바] 아이템 5 : 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2023.01.15 |
댓글