티스토리 뷰
728x90
반응형
equals는 일반 규약을 지켜 재정의하라
- 두 객체가 같은지 비교하는 목적으로 사용되는 equals 메서드는 재정의하기 쉬워 보이지만 곳곳에 함정이 도사리고 있습니다.
- 몇가지 규칙들을 제대로 지키지 않을 경우 개발자가 생각한 의도와 다르게 동작해서 프로그램에 오류가 발생할 수 있습니다.
- 재정의를 하지 않는 경우 해당 클래스의 인스턴스는 자기자신과만 같다고 평가하게 됩니다.
- 아래 두개의 인스턴스 객체를 살펴보면 ID, Name, Age는 모두 동일합니다. 하지만 재정의를 하지 않은 인스턴스이기 때문에 둘은 다른 객체로 평가됩니다. 즉 동등성은 성립하지만 동일성은 성립하지 않기 때문입니다. 그래서 동일성도 성립을 하기 위해서는 equals 메서드를 재정의해서 사용해야하는 경우도 있습니다.
재정의하지 않아도 되는 경우
💡각 인스턴스가 본질적으로 고유합니다.
- 값을 표현하는게 아닌 동작하는 개체를 표현하는 클래스가 여기에 해당됩니다.(ex: Thread) Object의 equals만으로 충분합니다.
💡인스턴스의 논리적 동치성을 검사할 일이 없습니다.
- 값을 비교해서 동등한지 비교할 일이 없다면 논리적 동치성 검사를 할 일도 없고, 기본적인 Object의 equals만으로 충분합니다.
💡상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞습니다.
- 상위 클래스에서 구현한 equals로직 만으로 충분한 경우 재정의할게 아니라 상위 클래스에 정의된 equals를 사용합니다.
- Set의 구현체는 AbstractSet이 구현한 equals를 상속받아서 사용합니다. List의 구현체 또한 동일합니다.
💡클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없습니다.
- 이 경우 만약에라도 equals 메서드가 실수로 호출되고자 하는것을 막고싶다면 아래와 같이 재정의하면 됩니다.
@Override
public boolean equals(Object o) {
throw new AssertionError(); // 호출 금지
}
재정의가 필요한 경우
💡객체 식별성이 아닌 논리적 동치성을 확인해야 하는 경우입니다.
- 상위 클래스가 있는 경우 상위 클래스에서 논리적 동치성을 비교하도록 재정의 하지 않은 경우
- 값 클래스가 이에 해당됩니다.(Integer, String)
- 값 클래스라 하더라도 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스인 경우 equals를 재정의하지 않아도 됩니다.
equals 메서드 재정의 규약
💡반사성
- null이 아닌 모든 참조 값 x에 대하여 x.equals(x)는 참이다.
💡대칭성
- null이 아닌 모든 참조 값 x, y에 대하여 x.equals(y)가 참이면 y.equals(x)도 참이어야 한다.
💡추이성
- null이 아닌 모든 참조 값 x, y, z에 대하여 x.equals(y)가 참이면 y.equals(z)도 참이면 x.equals(z)도 참이어야 한다.
💡일관성
- null이 아닌 모든 참조 값 x, y에 대하여 x.equals(y)를 반복해서 호출하면 항상 참을 반환하거나 거짓을 반환해야한다.
💡null 아님
- null이 아닌 모든 참조 값 x에 대하여 x.equals(null)은 거짓이어야 한다.
동치관계를 만족시키기 위한 조건
💡동치 관계란?
- 쉽게 말하면 집합을 서로 같은 원소들로 이루어진 부분집합으로 나누는 연산입니다.
- equals 메서드가 쓸모 있으려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환할 수 있어야 합니다.
💡반사성
- 반사성은 단순히 말하면 객체는 자기 자신과 같아야 한다는 것입니다. x.equals(x) 라는 코드는 항상 참을 반환해야 합니다.
💡대칭성
- 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다는 의미입니다.
- 아래 예제 코드를 보면 cis.equals(s)는 true를 반환하지만 s.equals(cis)는 false를 반환합니다. 그 이유는 cis는 내부적으로 equals를 재정의하여 일반 String을 알고 있지만, s변수의 String에 존재하는 equals에서는 CaseInsensitiveString을 모르기 때문에 false를 반환하게 됩니다.
- 또한 List에 넣은 후 contains 메서드를 사용해 다른 String 변수를 넣고 호출하게 되면 JDK의 버전에 따라 false, true, Exception이 발생할 수 있습니다.
- 이처럼 equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없습니다.
public class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
// 대칭성 위배!
@Override
public boolean equals(Object obj) {
if (obj instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
}
if (obj instanceof String) {
return s.equalsIgnoreCase((String) obj);
}
return false;
}
}
public class EffectiveJavaApplication {
public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString("KDG");
String s = "KDG";
System.out.println(cis.equals(s)); // true
System.out.println(s.equals(cis)); // false
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
System.out.println(list.contains(s)); // false, true, Exception
}
}
💡추이성
- A == B가 같고 B == C와 같다면 A == C도 같아야 합니다.
🧨 대칭성 위배!
- 예제를 보면 colorPoint.equals(point)는 ColorPoint 클래스에서 재정의한 equals 메서드를 타게 됩니다. 이렇게 되면 첫번째 if 조건에서 걸리게 됩니다. point는 Point 클래스이지 ColorPoint 클래스가 아니기 때문입니다. 따라서 false 입니다.
- point.equals(colorPoint)는 ColorPoint 클래스가 Point 클래스의 하위 클래스이고 x, y값이 같으므로 true가 됩니다.
- 아래 예시는 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 obj) {
if (!(obj instanceof Point)) {
return false;
}
Point point = (Point) obj;
return this.x == point.x && this.y == point.y;
}
}
public 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 obj) {
if (!(obj instanceof ColorPoint)) {
return false;
}
return super.equals(obj) && this.color == ((ColorPoint) obj).color;
}
}
public class EffectiveJavaApplication {
public static void main(String[] args) {
ColorPoint colorPoint = new ColorPoint(1, 2, Color.RED);
Point point = new Point(1, 2);
System.out.println(colorPoint.equals(point)); // false
System.out.println(point.equals(colorPoint)); // true
}
}
🧨 추이성 위배!
- a.equals(b)라면 a는 colorPoint이기 때문에 ColorPoint 클래스에 정의된 equals 메서드를 타게됩니다. 그리고 b는 Point 클래스이기 때문에 좌표값만 비교를 하게 되므로 true를 반환합니다.
- b.equals(c)라면 b는 Point이기 때문에 Point 클래스에 정의된 equals 메서드를 타게됩니다. Point 클래스는 x, y 값만 비교하므로 true를 반환합니다.
- a.equals(c)라면 ColorPoint 클래스에 정의된 equals 메서들르 타게됩니다. 둘 다 ColorPoint 클래스의 인스턴스 이므로 x, y 값 뿐만 아니라 color까지 비교하게 되므로 false를 반환합니다.
- 이렇게 A == B이고, B == C 이지만 A != C이므로 추이성에 위배 됩니다.
public 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 obj) {
if (!(obj instanceof Point)) {
return false;
}
// obj가 일반 Point이면 색상을 무시하고 x, y 정보만 비교합니다.
if (!(obj instanceof ColorPoint)) {
return obj.equals(this);
}
// obj가 ColorPoint이면 색상까지 비교합니다.
return super.equals(obj) && this.color == ((ColorPoint) obj).color;
}
}
public class EffectiveJavaApplication {
public static void main(String[] args) {
ColorPoint a = new ColorPoint(1, 2, Color.RED);
Point b = new Point(1, 2);
ColorPoint c = new ColorPoint(1, 2, Color.BLUE);
System.out.println(a.equals(b)); //true
System.out.println(b.equals(c)); //true
System.out.println(a.equals(c)); //false
}
}
🧨 무한 재귀가 발생하는 경우
- cp.equals(sp)를 호출하면 ColorPoint의 equals 메서드가 호출되고 두번째 if문에 걸리게 됩니다. 왜냐하면 넘겨받은 sp는 ColorPoint 클래스의 타입이 아니기 때문이고 조건식은 true가 됩니다. 그렇기 때문에 obj.equals(this)가 실행되면 결과적으로 obj는 SmellPoint 클래스의 인스턴스이기 때문에 SmellPoint 클래스에 재정의된 equals 메서드를 타게됩니다. 그럼 또 SmellPoint 클래스의 입장에서보면 마찬가지로 두번째 if문에 걸리게 됩니다. 그럼 obj는 ColorPoint 클래스의 타입이기 때문에 조건식이 true가되고 지금까지 실행했던 로직을 반대로 타고 또 반대로 타게되므로 무한 재귀에 빠지게 됩니다.
public class SmellPoint extends Point {
private final Smell smell;
public SmellPoint(int x, int y, Smell smell) {
super(x, y);
this.smell = smell;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point)) {
return false;
}
// obj가 일반 Point이면 색상을 무시하고 x, y 정보만 비교합니다.
if (!(obj instanceof SmellPoint)) {
return obj.equals(this);
}
return super.equals(obj) && this.smell == ((SmellPoint) obj).smell;
}
}
public enum Smell {
SWEET
}
public class EffectiveJavaApplication {
public static void main(String[] args) {
Point cp = new ColorPoint(1, 2, Color.RED);
Point sp = new SmellPoint(1, 2, Smell.SWEET);
System.out.println(cp.equals(sp)); // 무한 재귀 발생
}
}
🧨 리스코프 치환 원칙
- 자식 클래스는 최소한의 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 합니다.
- 조금 더 쉽게 말하면, 자식 클래스에서 부모 클래스의 기능을 수행하지 못하는 것은 리스코프 치환 원칙에 위배됩니다.
- 아래 예제 코드에서의 Point 클래스의 equals 메서드 중 두 번째 조건에 의해서 false를 반환하게 됩니다. obj.getClass()에서 ColorPoint가 도출되고 this.getClass()에서는 Point가 도출되니 서로 다르게 됩니다. 그렇기 때문에 false를 반환하게 됩니다.
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
private static final Set<Point> unitCircle = Set.of(
new Point(0, -1),
new Point(0, 1),
new Point(-1, 0),
new Point(1, 0)
);
public static boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
}
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != this.getClass()) {
return false;
}
Point point = (Point) obj;
return this.x == point.x && this.y == point.y;
}
}
public class EffectiveJavaApplication {
public static void main(String[] args) {
Point cp = new ColorPoint(1, 2, Color.RED);
System.out.println(Point.onUnitCircle(cp)); // false
}
}
💡일관성
- 일관성은 두 객체가 같다면 영원히 같아야 한다는 의미입니다.
- 가변 객체는 비교 시점에 따라 서로 다를 수도 혹은 같을 수도 있지만 불변 객체는 한번 다르면 끝까지 달라야 합니다.
- 불변 클래스로 만들기로 했다면 equals가 한번 같다고 한 객체와는 영원히 같다고 답하고, 다르다고 한 객체와는 영원히 다르다고 답하도록 만들어야 합니다.
- 클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안됩니다.
💡not null
- null이 아닌 모든 참조값 x에 대해 x.equals(null)은 false입니다.
💡정리
- == 연산자를 사용해 입력이 자기 자신의 참조인지 확인합니다.(동일성 검사)
- instanceof 연산자를 파라미터의 타입이 올바른지 확인합니다.
- 파라미터를 올바른 타입으로 형변환합니다.
- 앞에서 instanceof 연산자를 사용해 검사를 했기 때문에 100% 성공합니다.
- 파라미터 Object 객체와 자기자신의 대응되는 핵심 필드들이 모두 일치하는지 확인합니다.
- 하나라도 다르면 false를 반환
- 만약 interface기반의 비교가 필요하다면 필드 정보를 가져오는 메서드가 interace에 정의되어 있어야 하고, 구현체 클래스에서는 해당 메서드를 재정의해야 합니다.
- float, double를 제외한 기본 타입은 ==를 통해 비교합니다.
- 참조 타입은 equals를 통해 비교합니다.
- float, double은 Float.compare(float, float)와 Double.compare(double, double)로 비교합니다.
- 이렇게 취급하는 이유는 Float.NaN, -0.0f 특수한 부동소수 값을 다뤄야하기 때문입니다.
- 해당 클래스의 equals 메서드를 사용할 수 있으나 이 메서드들은 오토 박싱을 수반할 수 있으니 성능상 좋지 않습니다.
- 배열의 모든 원소가 핵심 필드라면 Arrays.equals 메서드를 사용합니다.
- null이 의심되는 필드는 Objects.equals(Object, Object)로 비교해 NPE를 예방합시다.
- 성능을 올리고자 한다면 다를 확률이 높은 필드부터 비교합시다.
💡주의사항
- equals를 재정의 했다면 대칭성, 추이성, 일관성에 대한 체크를 꼭 합시다.
- equals를 재정의 한다면 반드시 hashCode도 재정의 합시다.
- equals를 재정의 하는 경우 너무 복잡하게 생각하지 맙십다.
- equals의 파라미터는 Object 이외의 타입으로 선언하지 맙시다(컴파일 에러 발생)
- 구글에서 만든 @AutoValue를 이용해서 equals와 hashCode를 자동적으로 재정의 해봅시다. Lombok의 @EqualsAndHashCode도 있습니다.
728x90
반응형
'스터디 > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바 - Item12. toString을 항상 재정의하라. (0) | 2022.07.08 |
---|---|
이펙티브 자바 - Item11. equals를 재정의하려거든 hashCode도 재정의하라. (0) | 2022.07.07 |
이펙티브 자바 - Item9. try-finally 보다는 try-with-resources를 사용하라. (0) | 2022.07.04 |
이펙티브 자바 - Item8. finalizer와 cleaner 사용을 피하라. (0) | 2022.07.03 |
이펙티브 자바 - Item7. 다 쓴 객체 참조를 해제하라. (0) | 2022.07.03 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- java ThreadLocal
- java userThread와 DaemonThread
- spring boot redisson sorted set
- transactional outbox pattern
- 레이어드 아키텍처란
- JDK Dynamic Proxy와 CGLIB의 차이
- spring boot redis 대기열 구현
- 서비스 기반 아키텍처
- redis 대기열 구현
- spring boot 엑셀 다운로드
- transactional outbox pattern spring boot
- spring boot redisson destributed lock
- pipe and filter architecture
- space based architecture
- spring boot excel download oom
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- microkernel architecture
- service based architecture
- 공간 기반 아키텍처
- spring boot poi excel download
- redis sorted set
- 자바 백엔드 개발자 추천 도서
- pipeline architecture
- spring boot excel download paging
- polling publisher spring boot
- redis sorted set으로 대기열 구현
- 트랜잭셔널 아웃박스 패턴 스프링부트
- spring boot redisson 분산락 구현
- @ControllerAdvice
- 람다 표현식
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
글 보관함