일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- spring
- 세마포어
- 메모리
- 디지털
- 신입사원
- 프로그래밍
- 스터디
- 자바
- 알고리즘
- Public
- java
- 깃허브
- 스프링
- 운영체제
- 컴퓨터공학
- CS
- github
- 공채
- 컴퓨터과학
- package-private
- 우리카드
- 개발
- 이펙티브 자바
- 뮤텍스
- IT
- Effective Java
- 신입
- OS
- 깃
- 정보처리기사
- Today
- Total
주니어 개발자 성장기
아이템 10. equals는 일반 규약을 지켜 재정의하라 본문
3장
Object
는 객체를 만들 수 있는 구체 클래스지만 기본적으로는 상속해서 사용하도록 설계되었다. Object에서final
이 아닌 메서드(equals
,hashCode
,toString
,clone
,finalize
)는 모두 재정의(overriding)를 염두에 두고 설계된 것이라 재정의 시 지켜야 하는 일반 규약이 명확히 정의되어 있다.
일반 규약에 맞게 해당 메소드를 overriding 해야 일반 규약을 활용하는 클래스들(HashMap
, HashSet
)이 오동작하지 않게 된다. finalize
는 이전 장에서 다루었으므로 더이상 언급하지 않는다.
개요
equals
메서드는 overriding하기 쉬워 보이지만 곳곳에 함정이 있으므로 필요하지 않은 경우 overriding하지 않는 것이 최선이다. 그냥 두게되면 해당 인스턴스는 오직 자기 자신과만 같게 된다. 그럼 equals
를overriding할 필요가 없는 경우는 어떤 경우가 있을까?
- 각 인스턴스가 본질적으로 고유할 때
EX) 싱글톤 객체,Enum
- 논리적인 동치성을 검사할 필요가 없는 경우
EX) 문자열 - 상위 클래스에서 재정의한 equals가 하위 클래스에도 적절하다.
EX)AbstractList
←List
,AbstractSet
←Set
, - 클래스가
private
이거나package-private
이고equals
메서드를 호출할 일이 없다.public
이면equals
가 호출되지 않음을 보장할 수 없기 때문이다.
반대로 위 경우를 제외하고 객체 식별성(object identify; 두 객체가 물리적으로 같은가)이 아니라 논리적 동치성을 비교해야 하며 상위 클래스에서 논리적 동치성을 비교하도록 equals
가 재정의 되지 않았을 때 equals를 overriding 해주어야 한다.
equals
일반 규약
equals
메서드를 재정의할 때는 Object
명세에 적힌 다음과 같은
- 반사성(reflexivity)
A.equals(A) == true
- 대칭성(symmetry)
A.equals(B) == B.equals(A)
- 추이성(transitivity)
A.equals(B) && B.equals(C)
→A.equals(C)
- 일관성(consistency)
A.equals(B) == A.equals(B)
- null-아님
A.equals(null) == false
반사성(reflexivity)
자기 자신을 equals의 매개변수로 호출 했을 때 true를 반환해야 한다.
if(o == this)
return true;
위 코드를 equals
에 넣으면 쉽게 해결된다.
대칭성(symmetry)
대칭성이 위배된 잘못된 코드를 먼저 살펴보자
// 대칭성 위배!
@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;
}
if (o instanceof String)
이 부분이 바로 대칭성을 위배하는 부분이다.
CaseInsensitiveString.equals(String)
이 true이더라도 String.equals(CaseInsensitiveString)
는 String
은 CaseInsensitiveString
를 알고 있지 않으므로 false를 반환하기 때문이다.
대칭성을 얻기 위해서는 다음과 같이 하면된다.
// 수정한 equals 메서드 (56쪽)
@Override public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
간단히 다른 타입(여기서는 String
)까지 지원하려는 것을 포기하면 된다.
또 다른 사례를 보자.
ColorPoint
는 Point
의 서브 클래스이며 equals
를 다음과 같이 구현했다.
@Override public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Point)) {
return false;
}
Point p = (Point) o;
return p.x == x && p.y == y;
}
그리고 아래는 ColorPoint
의 equals
이다.
// 코드 10-2 잘못된 코드 - 대칭성 위배! (57쪽)
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
Point.eqauls(ColorPoint)
가 true를 반환할 수도 있지만 ColorPoint.eqauls(Point)
는 instanceof
구문에 의해서(Point는 ColorPoint 타입이 아니므로) 반드시 false를 반환하며 대칭성을 위반하게 된다.
추이성(transitivity)
그럼 위 코드에서 type까지 고려한다면 어떨까?
// 코드 10-3 잘못된 코드 - 추이성 위배! (57쪽)
@Override 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;
}
이렇게 구현한다면 equals
의 대칭성은 지켜진다. 하지만 아래 코드를 보자.
// 두 번째 equals 메서드(코드 10-3)는 추이성을 위배한다. (57쪽)
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)
, p2.equals(p3)
는 true 이지만 p1.equals(p3)
는 false를 반환하게 된다.
또 하나 큰 문제점은 Point
의 다른 서브 클래스의 equals
를 위와 같이 구현할 경우 StackOverflow
가 일어나게 된다.
그래서 추이성을 지키고자 Point.equals
를 다음과 같이 변경할 수도 있다.
// 잘못된 코드 - 리스코프 치환 원칙 위배! (59쪽)
@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;
}
이렇게 바꾼다면 앞서 말한 예시에서 추이성은 지켜진다. 하지만 객체 지향 프로그래밍에서 중요한 리스코프 치환 원칙을 위반하게 된다.
리스코프 치환 원칙 어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요하다. 따라서 그 타입의 모든 메서드가 하위 타입에서도 똑같이 잘 작동해야 한다.
위 코드에서 Point
의 서브 클래스들을 Point
로 형변환 하여 equals
를 호출한다면 좌표가 같더라도 false
를 반환하게 될 것이다.
그렇다면, 어떻게 해야 equals
의 일반 규약을 지킬 수 있을까?
일반적으로 instanceof 로 상위 클래스(인터페이스) 혹은 자신과 같은 클래스인지 비교하고 필드를 비교하면 되며 서브 클래스는 equals 메서드를 그대로 쓰면 된다. 하지만, ColorPoint
와 같이 값을 추가한 서브 클래스는 일반 규약을 지키면서 리스코프 치환 원칙까지 지키는 것은 불가능하다.
이와 같은 경우 상속 대신 컴포지션을 사용할 것을 책에서는 권장한다.
컴포지션
다음과 같이 객체를 직접 상속하는 것이 아니라 내부에 필드로 가지고 있는 것을 말한다.
// 코드 10-5 equals 규약을 지키면서 값 추가하기 (60쪽)
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);
}
/**
* 이 ColorPoint의 Point 뷰를 반환한다.
*/
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);
}
}
위와 같이 상속 대신 컴포지션 구현시 Point
뷰를 asPoint
로 반환할 수 있으며 이를 통해 equals
일반 규약을 지키는 것이 가능해진다.
일관성(consistency)
한 번 A.equals(B)
가 true이면 그 다음에 A.equals(B)
를 호출해도 true를 반환해야 한다는 것이다. 불변 객체라면 일관성이 항상 보장된다. 하지만 가변 객체라면 일관성이 깨질 수 있다.
URL
의 경우 호스트의 IP 주소를 이용하여 비교하기 때문에 Virtual Host의 경우 일관성이 보장되지 않는다. 네트워크를 통해서 IP 주소를 가지고 오기 때문이다.
따라서, URL
과 같이 신뢰할 수 없는 자원
을 equals
의 판단에 끼어들게 해서는 안된다. 메모리에 있는 객체를 이용한 결정적인(deterministic) 계산만 수행해야 한다.
null-아님
null이 인자로 넘어오면 false를 반환해야 한다는 것이다. instanceof
구문을 사용하면 null 일 경우 false가 반환되므로 instanceof
를 활용하자.
일반 규약을 지키는 구현방법
- 자기 자신인지 확인한다.
if (this == o) { return true; }
- 타입 비교
if (!(o instanceof Point)) { return false;}
- 형 변환
Point p = (Point) o;
- 핵심 필드들이 모두 일치하는지 하나씩 검사한다.
return p.x == x && p.y == y;
여기서 유의할 점은 다음과 같다.
- float과 double 은 == 비교가 아니라, 각각 정적 메서드인
Float.compare(float, float)
과Double.compare(double, double)
로 비교한다. - Reference type은 그 type이 가지고 있는 equals를 호출한다.
- Synchronized Lock(동기화 용 락) 필드 같이 객체의 논리적 상태와 관련 없는 필드는 비교하면 안된다.
- null일 수도 있는 필드를 비교하려면
Object.equals(Object, Object)
로 비교한다.
위를 기반으로 다음과 같이 equals
를 override할 수 있다.
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Point)) {
return false;
}
Point p = (Point) o;
return p.x == x && p.y == y;
}
실제
구현하기가 너무 복잡하다. 따라서 우리는 주로 Tool을 사용한다.
- AutoValue
쓰는 방법이 조금 까다롭다. 컴파일시에 생성되는 코드를 기반으로 하기 때문에, 없는 클래스를 참조해야한다. → 즉, 너무 invasive(침투적)이다. - Lombok
상대적으로 비침투적이다.
자바 11버전일 경우 추천 - record
자바 14버전에 처음 들어왔던 기능
자바 17버전 부터 추천(record에 적절하지 않은 경우면 Lombok) - 인텔리제이에서 만들기
약간 지저분하다. 필드가 늘어나면 매번 다시 만들어야 한다.
주의사항
equals
를 재정의할 때hashCode
도 반드시 재정의하자.- 너무 복잡하게 구현하지 말자.
- Object가 아닌 타입의 매개변수를 받는 equals 메서드는 무용지물이다!
'Java > 이펙티브 자바' 카테고리의 다른 글
아이템 12. toString을 항상 재정의하라. (0) | 2023.09.02 |
---|---|
아이템 11. equals를 재정의하려거든 hashCode도 재정의하라. (0) | 2023.09.01 |
아이템 9. try-finally 보다 try-with-resources를 사용하라. (0) | 2023.07.23 |
아이템 8. finalizer와 cleaner 사용을 피하라 (0) | 2023.07.22 |
아이템 7. 다 쓴 객체 참조를 해제하라. (0) | 2023.07.20 |