equals 메서드는 재정의하기 쉬워 보이지만 곳곳에 함정이 도사리고 있어서
자칫하면 끔직한 결과를 초래할 수 있습니다.
문제를 회피하는 가장 쉬운 길은 재정의하지 않는 것으로, 그렇게 두면
클래스의 인스턴스는 오직 자기 자신과만 같게 됩니다.
재정의하지 않는 것이 최선인 경우
1. 각 인스턴스가 본질적으로 고유하다.
값을 표현하는 것이 아니라, 동작하는 개체를 표현하는 클래스를 표현할 때는 재정의 하지 않는 것이 더 좋습니다. 예로 Thread가 있고, equals 메서드는 Thread에 가장 딱 맞게 구현되어 있습니다.
2. 인스턴스의 '논리적 동치성'을 검사할 일이 없다.
Pattern 클래스를 사용할 때 만약 equals를 재정의해서 사용하는 것은, 두 인스턴스가 같은 정규표현식을 나타내는지 검사하는 방법으로 사용될 수 있습니다. 하지만 설계자는 이런 방식이 필요하다고 하지 않습니다. 그렇기 때문에 재정의할 필요가 없습니다.
3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
Set, List, Map 구현체들은 AbstractMap으로부터 equals를 상속받아서 사용하는데 이 경우 상위 클래스인 AbstractMap에서 재정의한 equals가 Set, List, Map에서 사용할 수 있도록 정의되어 있기 때문에 굳이 재정의할 필요가 없습니다.
4. 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.
위험을 회피하는 스타일이라 equals가 실수로라도 호출되는 걸 막고싶다면 다음과 같이 재정의를 해줍니다.
@Override
public boolean equals(Object o){
throw new AssertionError(); // 호출 금지 exception
}
재정의하는 경우
1. 두 객체가 물리적으로 같은 것이 아니라 논리적 동치성을 확인해야 할 때
두 값 객체를 equals로 비교하는 프로그래머는 객체의 값이 같은지를 알고 싶은 경우가 많습니다. 그런데 이때 equals가 논리적 동치성을 확인하도록 재정의되어 있지 않다면 재정의 해주는 것이 좋습니다.
equals 메서드 재정의 할 때 반드시 지켜야 하는 일반 규약
1. 반사성 (reflexivity) - 객체는 자기 자신과 같아야 한다.
null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true이다.
x==x (TRUE)
2. 대칭성 (symmetry) - 서로에 대한 동치 여부에 똑같이 답해야 한다.
null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)가 true이면 y.equals(x)도 true이다.
x==y (TRUE) --> y==x (TRUE)
3. 추이성 (transitivity) - 첫번째와 두번째 객체가 같고, 두번째 세번째 객체가 같으면 첫번째와 세번째도 같다.
null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y)가 true이고, y.equals(z)도 true이면 x.equals(z)도 true이다.
x==y && y==z (TRUE) --> x==z (TRUE)
4. 일관성 (consistency) - 두 객체가 같다면 수정되지 않는 한 영원히 같아야 한다.
null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
x==y (TRUE) --> x==y (TRUE) / x==y (FALSE) --> x==y(FALSE) [반복해도 같은 값이 나온다]
5. null-아님
null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false이다.
x==null (FALSE)
equals를 재정의할 대 이 규약을 잘 지켜야 합니다. 이 규약을 어기면 프로그램이 이상하게 동작하거나 종료될 것이고, 원인 코드는 굉장히 찾기 어려울 것입니다.
equals 메서드가 사용되려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환할 수 있어야 합니다.
equals 메서드 구현 방법
- == 연산자를 사용해 입력이 자기 자신이 참조인지 확인한다.
- instanceof 연산자로 입력이 올바른 타입인지 확인한다.
- 입력을 올바른 타입으로 형변환한다.
- 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.
equals 재정의 시 hashCode 재정의 필요성
equals() 를 재정의 한 후 hashCode() 를 재정의 하지 않으면, warning error가 발생합니다. 이는 equals() 를 재정의 했으면 hashCode() 도 재정의를 해줘야하기 때문입니다.
왜냐하면 자바의 규칙 중 두 객체의 equals 결과값이 true라면, 해시코드의 값도 같아야 하는 규칙이 있기 때문입니다. 그리고 hashCode를 재정의하지 않는다면, Collection의 HashSet, HashMap, HashTable 등을 사용할 때 오류가 발생하기 때문입니다.
예를 들어, 중복을 허용하지 않는 Set을 사용한다고 가정하면, 정보가 같은 두 객체가 있을 때 hashCode()를 거치고, equals()를 거쳐서 둘다 true일 때 동등한 객체라는 판단을 하게 됩니다.
그런데 hashCode()를 재정의해주지 않으면 각각 객체가 필드 값이 같아도, hashCode() 값이 다르기 때문에 다른 객체로 인식이 되어 중복을 허용하지 않는 Set에 두개의 객체가 모두 들어가게 됩니다.
hashCode 메서드 표준 정의
객체가 동등한 것을 필드를 통해 증명하기 위해 필드의 값으로 hashCode를 일치시켜줍니다. 그 결과 Collection의 Set처럼 중복을 허용하지 않는 객체를 사용할 때, hashCode()에서 true를 return하게 됩니다.
// 필드가 1개 일 때
@Override
public int hashCode(){
int result = Integer.hashCode(equalInt);
return result;
}
// 필드가 2개 이상 일 때
@Override
public int hashCode(){
int result = Integer.hashCode(equalInt);
result = 31 * result + Integer.hashCode(equalIntTo);
return result;
}
클래스에 비슷한 필드가 여러 개일 때는 해시 코드가 중복될 가능성이 있기 때문에 31을 곱해서 해시 효과를 크게 높여줍니다. 이는 중복을 줄여주기 때문에 성능 향상을 해줄 수 있습니다.
31을 곱해주는 이유는 홀수이면서, 소수이기 때문입니다. 만약 짝수이고 오버플로가 발생한다면 정보를 잃게되기 때문입니다.
[출처]
이펙티브 자바 Effective Java 3/E - 조슈아 블로크 저/ 개앞맵시 역
'Book Record > Effective Java 3E' 카테고리의 다른 글
[Effective Java] null이 아닌, 빈 컬렉션이나 배열을 반환하라 (2) | 2022.12.22 |
---|---|
[Effective Java] try-finally 보다는 try-with-resources를 사용하라 (1) | 2022.12.01 |
[Effective Java] 다 쓴 객체 참조를 해제하라 (1) | 2022.12.01 |
[Effective Java] 불필요한 객체 생성을 피하라 (0) | 2022.12.01 |
[Effective Java] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2022.12.01 |