Effetive java 3장 정리
10. euals 는 일반 규약을 지켜 재정의하라
- 각 인스턴스가 본질적으로 고유하다. 값을 표현하는 것이 아니라 동작하는 객체를 표현하는 클래스가 해당한다. ex) Thread
- 인스턴스의 '논리적 동치성(logical equality)'을 검사할 일이 없다.
- 상위 클래스에서 재정의한 equals가 하위 클래스에도 해당된다. ex) AbstractSet → Set 구현체, AbstractList → List 구현체, ...
- 클래스가 private거나 package-private고 equals 메서드를 호출할 일이 없다. 혹시라도 equals를 호출하는 위험을 막고 싶다면 AssertionError를 던지자.
@Override
public boolean equals(Object o) {
throw new AssertionError();
}
- 반사성(reflexivity): null이 아닌 모든 참조 값 x에 대해 x.equals(x)는 true이다.
→ 단순하게 말하자면 객체는 자기 자신과 같아야 한다는 뜻이다. - 대칭성(symmetric): null이 아닌 모든 참조 값 x,y에 대해 x.equals(y)가 true면 y.equals(x)도 true이다.
→ 두 객체를 서로를 각각 비교했을 때 같은 값을 반환해야 한다. - 추이성(transitivity): null이 아닌 모든 참조 값 x,y,z에 대해 x.equals(y)가 true, y.equals(z)가 true이면 x.equals(z)도 true이다
→ 삼단논법. A가 B와 같고, B가 C와 같으면 A는 C와 같다.
→ 상속시 쉽게 어길 수 있는데, 새로운 필드를 추가하면서 equals 규약을 만족할 수 있는 방법은 없다. → 그래서 상속 대신 private 필드로 두고, 이를 반환하는 메서드를 두는 방법으로 우회할 수 있다. - 일관성(consistency): null이 아닌 모든 참조 값 x,y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환해야한다.
→ 수정되지 않는 한 영원히 같아야한다. - null이 아닌 모든 참조 값 x에 대해 x.equals(null)은 false이다.
이를 지키기 위한 equlas 메서드 구현 방법은 아래와 같다.
- == 연산자를 이용해 입력이 자기 자신의 참조인지 확인한다. (단순한 성능 최적화용)
- instanceof 연산자로 입력이 올바른 타입인지 확인한다.
- 입력을 올바른 타입으로 형변환한다. (3번을 거치면 성공!)
- 피연산자와 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.
→ 하나라도 다르면 false. 다를 확률이 높은 것부터 검사한다.
→ 기본 타입 필드는 ==, 참조 타입은 equals, float와 double은 Float 혹은 Double의 compare메서드를 사용한다.
→ null을 정상 값으로 취급하는 필드는 Objrcts.equals(Object, Object)를 이용하자.
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "area code");
this.prefix = rangeCheck(prefix, 999, "prefix");
this.lineNum = rangeCheck(lineNum, 9999, "line num");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
}
equals가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 같은 값을 반환해야 한다
11. equals를 재정의하려거든 hashCode도 재정의하라
전형적인 hashCode 메서드
@Override
public int hashCode() {
int result = Short.hashCode(areaCode); // 1
result = 31 * result + Short.hashCode(prefix); // 2
result = 31 * result + Short.hashCode(lineNum); // 3
return result;
}
1: 가장 핵심필드중의 하나를 골라서 그 필드의 해시코드 값을 구합니다. 이 필드가 primitive타입이라면 해당하는 타입의 wrapper타입의 hashCode메서드를 사용해서 해쉬코드 값을 구하면 됩니다.
2: 남은 필드들을 순서대로 31 * result + (그 필드의 해시코드 값)을 더해서 result에 더해줍니다.
3: 2와 마찬가지로 더해서 리턴해주면 됩니다.
그렇다면 왜 하필 31을 곱해주는지 의문이 생길수도 있습니다. 두 가지 이유가 있습니다. 첫 번째는 홀수라는 점입니다. 짝수를 쓰게되면 뒤에 부분의 값이 0으로 채워져 오버플로가 발생해서 정보를 잃게될 수 있습니다. 두 번째는 홀수중에 31인 이유는 어떤 개발자분이 사전에 있는 모든 단어를 해싱 후 어떤 숫자를 썼을떄 가장 충돌이 적게나는지 테스트해서 나온 숫자입니다.
12. toString을 항상 재정의하라
toString은 기본적으로 클래스이름@해시코드를 반환한다.
toString 메서드는 디버깅시 로그를 남길 때 많이 사용되는데, 따라서 메서드를 간결하고 사람이 읽기 쉬운 형태로 재정의하는 것이 좋다.
재정의할 때는 가진 주요 정보를 모두 반환하는 것이 좋고, 포맷을 결정했다면 그 의도를 명확히 해야한다.
포맷을 명시했다면 추후에 이에 맞춰 사용해야한다는 것(유연성 저하)을 주의하자.
또한 toString을 파싱하여 사용하지 않도록 toString에 포함되는 값은 반환값을 얻을 수 있는 API를 제공하는 것이 좋다.
의문: Getter어노테이션 호출 시 자동으로 toString 오버라이딩해주는데 다시 재정의해줘야하나 ..?
14. Comparable을 구현할지 고려하라
· Comparacle 인터페이스는 compareTo 메서드를 갖는다.
· compareTo는 두 가지만 빼면, Object의 equals와 같다.
- compareTo는 단순 동치성 비교에 더해 1. 순서까지 비교할 수 있으며, 2. 제네릭하다.
- Comparable을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서(natural order)가 있음을 뜻한다.
public interface Comparable<T> {
int compareTo(T t);
}
@EqualsAndHashCodeequals, hashCode 자동 생성
- equals : 두 객체의 내용이 같은지, 동등성(equality) 를 비교하는 연산자
- hashCode : 두 객체가 같은 객체인지, 동일성(identity) 를 비교하는 연산자
자바 bean에서 동등성 비교를 위해 equals와 hashcode 메소드를 오버라이딩해서 사용하는데,
@EqualsAndHashCode어노테이션을 사용하면 자동으로 이 메소드를 생성할 수 있다.
callSuper 속성을 통해 eqauls와 hashCode 메소드 자동 생성 시 부모 클래스의 필드까지 감안할지의 여부를 설정할 수 있다.
@EqualsAndHashCode(callSuper = true)로 설정시 부모 클래스 필드 값들도 동일한지 체크하며, false(기본값)일 경우 자신 클래스의 필드 값만 고려한다.
그럼 hashcode는 왜 필요할까? - Hash Collection 객체 때문
- 위의 예시만 본다면 equals만 잘 재정의하면 모든 객체의 동등성이 보장될 것 같지만, 아쉽게도 모든 객체가 그렇지는 않다.
- 이 예외는 Hash 값을 사용하는 Hash Collection 자료구조(HashMap, HashSet, HashTable) 때문에 일어난다
public static void main(String[] args) {
Set<Car> cars = new HashSet<>();
cars.add(new Car(1, "sonata"));
cars.add(new Car(1, "sonata"));
System.out.println(cars.size()); // 2 -> HashSet 사이즈가 1이 아닌 2임
}
위의 예시를 보자, 만약 컬렉션에 중복되지 않은 객체를 넣으라는 요구사항이 있다고 가정해 보자
중복을 불허하는 Set을 사용하여 완전히 상태가 같은 객체를 두 번 set에 집어넣었다. 이론상으로는 중복이기에 Hashset의 길이가 1이어야 하지만 프로그램을 돌리면 길이가 2가 나온다. 동등성 보장에 실패한 것이다.
보장되지 않는 이유는?
참고한 블로그에 사진으로 이 내용이 명확히 설명되어 인용을 하도록 하겠다
위에서 언급 한 Hash를 사용하는 Collection들(HashMap, HashSet, HashTable)은 객체의 동동성 비교를 다음 과정과 같이 수행한다
출처 : https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode/
즉, 일단 hashcode값이 서로 같아야만 equals 메소드로 객체비교를 수행하는 것이다. Hashcode가 다르면 동등성 비교는 입구컷이라는 뜻