개발관련 책

Effetive java 3장 정리

MIN우 2023. 7. 3. 13:31
728x90

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 메서드 구현 방법은 아래와 같다.

  1. == 연산자를 이용해 입력이 자기 자신의 참조인지 확인한다. (단순한 성능 최적화용)
  2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.
  3. 입력을 올바른 타입으로 형변환한다. (3번을 거치면 성공!)
  4. 피연산자와 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.
    → 하나라도 다르면 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가 다르면 동등성 비교는 입구컷이라는 뜻

 

 

 

equals and hashcode blog: https://velog.io/@rg970604/JAVA-Equals%EC%99%80-HashCode%EB%A5%BC-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0#equals%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC--%EB%8F%99%EC%9D%BC%EC%84%B1%EA%B3%BC-%EB%8F%99%EB%93%B1%EC%84%B1%EC%9D%98-%EA%B0%9C%EB%85%90

 

 

 

 

728x90