equals는 일반 규약을 지켜 재정의하라
이번 장에서는 Java의 최상위 객체인 Object에서 사용되는 메서드(equals, hashCode, toString, clone, finalize)들에 대한 주의사항을 소개한다. 이 중 finalize는 생략한다.
처음 다른 언어를 접하고 Java를 배운 사람들은 대부분 ==
와 .equals
의 차이를 한 번씩은 경험한다. 사소하지만 객체의 동일성은 메모리 주소가 아닌 객체가 가진 상태를 기준한다는 점에서 Java의 객체지향적인 부분이 드러나는 부분이라고 할 수 있다. 이처럼 객체지향언어에서 동치는 물리 또는 논리적 성격 두 가지를 의미할 수 있다.
상속을 통한 확장은 명세가 중요하다. 만약 명세가 제대로 잡히지 않은 메서드를 상속받아 오버라이드(overwrite)하면, 다른 객체와의 통신에서 일관성이 무너지고, 예상하지 못한 일이 발생하기 때문이다.
이번 장에서는 euqals를 재정의해야 할 상황과 그렇지 않은 상황을 제안한다. 다음은 재정의를 하지 않는 상황이다.
- 각 인스턴스가 본질적으로 고유한 경우
- 논리적 동치성을 검사할 일이 없는 경우
- 상위 클래스에서 재정의한 equals가 하위 클래스에 딱 맞는 경우
- equals가 호출될 일이 없는 경우
모두 납득이 가능한 항목이다. 정리하자면, 객체 간 동치성을 확인하는 것이 논리적이 아닌 경우에 해당한다. 따라서 equals를 재정의해야 하는 상황은 당연하게도 객체의 동치성이 논리적인 상황으로 정리할 수 있다. 책에서 좋은 예로 String
, Integer
와 같은 객체를 예로 든다. 우리가 문자열을 비교할 때 문자열 객체가 물리적인 메모리 주소가 동일한지 검증하기를 원하지 않고, 그 안에 포함된 문자열 그 자체가 동일한지를 알기 원할 것이다.
하지만 논리적 동치를 제대로 만족하도록 구현하는 것은 비교적 쉬운 일이 아니다. 이 책에서는 euqals의 일반 규약을 다음과 같이 소개한다.
- 반사성
- 대칭성
- 추이성
- 일관성
- null 아님
먼저 반사성에 대해 알아보자. 반사성은 null이 아닌 경우 자기 자신에 대한 동치성은 항상 true를 만족한다는 규약이다. 코드로 보면 다음과 같은 형태로 표현할 수 있다.
if (this == o) {
return true;
}
단순하지만 euqals 구현 시 불필요한 비교 연산을 차단할 수 있어 메서드 상단에 위치하게 되면 성능적인 이득을 얻을 수 있다.
대칭성은 x = y이면, y = x이다
를 만족하는 규약이다. 간단히 말하자면, 비교 대상인 객체 x와 y는 동일 타입이거나, 서로가 의존성을 가져야 한다. 여기서 의존성을 가진다는 것은 두 객체 간에 강한 결합이 생기기 때문에 추천하지 않는다.
@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은 String을 의존하고 있지만, 그 반대의 경우는 의존하고 있지 않다. 대칭성을 만족하지 못한다고 할 수 있다.
추이성은 x = y이고 y = z이면 z = x이다.
를 만족하는 규약이다. 추이성은 위반되기 쉬운 규약으로 객체 상속 관계에서 포함 관계가 생긴 경우 발생할 수 있다. 정리하자면, 부모 객체에 대해서는 공통으로 가진 상태만 비교하게 되면, 하위 객체에 대한 동치성 검사에서 모순이 생긴다는 것이다. 책에서는 상속을 통한 상태 추가를 하며 equals 규약을 만족할 수 없다고 강하게 말하며, 대안으로 뷰 메서드를 노출하는 것을 제안한다.
마지막으로 일관성은 두 객체의 동치성은 외부 요인에 독립적으로 수행되어야 한다는 규약이다. 다시 말하자면 처음 두 객체가 같고, 변경되지 않는다면 영원히 같아야 한다. 책에서 소개하는 좋은 예는 java.net.URL이 외부 요인에 의해 일관성 규약이 깨지는 것을 설명한다.
equals를 재정의하려거든 hashCode도 재정의하라
앞서 논리적 동치성을 만족하기 위해 equals를 정의한 경우 hashCode도 재정의 해야 한다. Object에 정의된 기본 hashCode 메서드는 이러한 논리적 동치성과 관련 없이 물리적 객체에 대한 hash code를 반환하기 때문에 논리적 동치성은 만족하지만, hash code가 다른 경우가 발생할 수 있다. 만약 동치인 객체에 대해서 서로 다른 hash code를 반환하게 되면 의도하지 않은 동작을 유발한다. 다음은 Object 명세의 일부이다.
If two objects are equal according to the equals method,
then calling the hashCode method on each of the two objects must produce the same integer result.
따라서 hashCode를 재정의할 땐 equals에 활용된 필드가 아니라면 제외해야한다. 그렇지 않으면 논리적 동치인 객체 간에 서로 다른 hash code가 생성될 수 있다.
그리고, 두 객체가 동치가 아니라도, 두 객체의 hash code가 항상 달라야 하는 것은 아니지만 이는 hash table의 성능을 나쁘게 하는 것이기 때문에 주의해야 한다.
It is not required that if two objects are unequal according to the equals method,
then calling the hashCode method on each of the two objects must produce distinct integer results.
However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
clone 재정의는 주의해서 진행하라
Cloneable은 객체 복사를 지원하다는 marker interface이다. 즉, Cloneable을 정의했다면 반드시 clone 메서드를 재정의해야 한다. 만약 clone을 재정의하지 않았다면, Object 기본 clone 메서드에 의해 CloneNotSupportedException 예외가 발생하기 때문이다.
만약 clone을 재정의한 경우 객체 참조에 대해서 주의해야 한다. 만약 참조된 객체에 대한 소유권이 복제된 객체와 공유하게 된다면 객체 상태에 대한 변경을 예측하기 어려워진다.
Comparable을 구현하지 고려하라
Comparable interface는 두 객체를 비교하는 compareTo 메서드가 정의된 interface이다. 두 객체를 비교한다는 점에서 equals와 비슷하지만 중요한 차이점 있다.
단순하게 equals는 boolean을 반환해 객체가 동치인지 아닌지만 반환한다. 반면에 compareTo는 정수를 정수를 반환한다. 그리고 equals는 교환법칙이 성립한다. 반면에 compareTo는 교환법칙이 성립되지 않는다. 따라서 compareTo는 앞뒤에 순서에 대한 상태를 알 수 있다.
'공부' 카테고리의 다른 글
제네릭 (0) | 2023.11.10 |
---|---|
클래스와 인터페이스 (0) | 2023.11.10 |
객체 생성과 파괴 (0) | 2023.11.10 |
Github 계정 GPG 등록하기 (0) | 2022.10.29 |
수강 하면 기록하는 명령어 (0) | 2022.09.18 |