본문 바로가기
카테고리 없음

03장 모든 객체의 공통 매서드

by hanyugyeong 2023. 10. 24.
반응형
SMALL

아이템 10 equals는 일반 규약을 지켜 재정의하라 

다음에서 열거한 상황 중 하나에 해당한다면 재정의하지 않는 것이 최선이다. 

equals를 재정의 하지 않아도 돼는 경우 

1) 각 인스턴스가 본질적으로 고유하다. 

2) 인스턴스의 '논리적 동시성'을 검사할 일이 없다. 

 -> String 

3) 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다. 

4) 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다. 

equals를 재정의 해야 하는 경우 반드시 일반 규약을 따라야 한다. 

* 반사성: A.equals(A) == true

-> 단순히 말하면 객체는 자기 자신과 같아야 한다는 뜻이다. 

* 대칭성 

-> 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다는 뜻이다 

  // 문제 시연 (55쪽)
    public static void main(String[] args) {
        CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
//        CaseInsensitiveString cis2 = new CaseInsensitiveString("polish");
        String polish = "polish";
        System.out.println(cis.equals(polish));
//        System.out.println(cis2.equals(cis));

        List<CaseInsensitiveString> list = new ArrayList<>();
        list.add(cis);

        System.out.println(list.contains(polish));
    }

ture

false

*추이성 

-> 첫 번째 객체와 두 번째가 같고, 두 번째 객체와 세 번째 객체가 같다면, 첫 번째 객체와 세 번째 객체도 같아야 한다는 뜻이다. 

// 단순한 불변 2차원 정수 점(point) 클래스 (56쪽)
public class Point {

    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @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;
    }
}

이제 이 클래스를 확장해서 점에 색상을 더해보자.

public class ColorPoint extends Point {
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }
}

비교 대상이 또 다른  ColorPoint이고 위치와 색상이 같을 때만 true를 반환하는 equals를 생각해보자.

    @Override public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        return super.equals(o) && ((ColorPoint) o).color == color;
    }

Point의 equals는 색상을 무시하고, ColorPoint의 equals는 입력 매개변수의 클래스 종류가 다르다며 매번 false만 반환할 것이다. 

//    // 코드 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 규약을 만족시킬 방법은 존재하지 않는다. 

 

리스코프 치환 원칙 위배(어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 똑같이 잘 작동해야 한다. 이는 앞서의 "Point의 하위 클래스는 정의상 여전히 Point이므로 어디서든 Point로써 활용될 수 있어야 한다"

    @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;
    }

주어진 점이 (반지름이 1인) 단위 원 안에 있는지를 판별하는 메서드가 필요하다고 해보자 

// Point의 평범한 하위 클래스 - 값 컴포넌트를 추가하지 않았다. (59쪽)
public class CounterPoint extends Point {
    private static final AtomicInteger counter =
            new AtomicInteger();

    public CounterPoint(int x, int y) {
        super(x, y);
        counter.incrementAndGet();
    }
    public static int numberCreated() { return counter.get(); }
}
public class CounterPointTest {
    // 단위 원 안의 모든 점을 포함하도록 unitCircle을 초기화한다. (58쪽)
    private static final Set<Point> unitCircle = Set.of(
            new Point( 1,  0), new Point( 0,  1),
            new Point(-1,  0), new Point( 0, -1));

    public static boolean onUnitCircle(Point p) {
        return unitCircle.contains(p);
    }

    public static void main(String[] args) {
        Point p1 = new Point(1,  0);
        Point p2 = new CounterPoint(1, 0);

        // true를 출력한다.
        System.out.println(onUnitCircle(p1));

        // true를 출력해야 하지만, Point의 equals가 getClass를 사용해 작성되었다면 그렇지 않다.
        System.out.println(onUnitCircle(p2));
    }
}

일관성은 두 객체가 같다면 앞으로도 영원히 같아야 한다는 뜻이다 

클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안된다. 

java.net.URL의 equals는 주어진 URL과 매핑된 호스트의 IP 주소를 이용해 비교한다. 호스트 이름은 IP 주소로 바꾸려면 네트워크를 통해야 하는데, 그 결과가 항상 같다고 보장할 수 없다. 

public static void main(String[] args) throws MalformedURLException {
        long time = System.currentTimeMillis();
        Timestamp timestamp = new Timestamp(time);
        Date date = new Date(time);

        // 대칭성 위배! P60
        System.out.println(date.equals(timestamp));
        System.out.println(timestamp.equals(date));

        // 일관성 위배 가능성 있음. P61
        URL google1 = new URL("https", "about.google", "/products/");
        URL google2 = new URL("https", "about.google", "/products/");
        System.out.println(google1.equals(google2));
    }

양질의 equals 메서드 구현 방법을 단계별로 정리해보자 

1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다. 

2.instanceof 연산자로 입력이 올바른 타입인지 확인한다. 

3. 입력을 올바른 타입으로 형변환한다. 

4. 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다. 

-> 모든 필드가 일치하면 true를 하나라도 다르면 false를 반환한다. 

 

주의사항 

1. equals를 재정의할 땐 hashCode도 반드시 재정의하자. 

2. 너무 복잡하게 해결하려 들지 말자 

아이템 11. equals를 재정의하려거든 hashCode도 재정의하라 

핵심 정리:  hashCode 규약 

* equals 비교에 사용하는 정보가 변경되지 않았다면 hashCode는 매번 같은 같은 값을 리턴해야 한다. (변경되거나, 애플리케이션을 다시 실행했다면 달라질 수 있다.)

* 두 객체에 대한 equals가 같다면, hashCode의 값도 같아야 한다. 

* 두 객체에 대한 equals가 다르더라도, hashCode의 값은 같을 수 있지만 해시 테이블 성능을 고려해 다른 값을 리턴하는 것이 좋다. 

 

public class HashMapTest {

    public static void main(String[] args) {
        Map<PhoneNumber, String> map = new HashMap<>();

        PhoneNumber number1 = new PhoneNumber(123, 456, 7890);
        PhoneNumber number2 = new PhoneNumber(123, 456, 7890);

//         TODO 같은 인스턴스인데 다른 hashCode
//         다른 인스턴스인데 같은 hashCode를 쓴다면?
        System.out.println(number1.equals(number2));
        System.out.println(number1.hashCode());
        System.out.println(number2.hashCode());

        map.put(number1, "keesun");
        map.put(number2, "whiteship");

        String s = map.get(new PhoneNumber(123, 456, 7890));
        System.out.println(s);
    }
}
true
1720435669
1020923989
null

두 객체가 서로 다른 해시코드를 반환하여 두 번째 규약을 지키지 못한다. 

    @Override
    public int hashCode() {
        return 42;
    }
true
42
42
whiteship

이 코드는 동치인 모든 객체에서 똑같은 해시코드를 반환하니 적법하다. 

모든 객체에게 똑같은 값만 내어주므로 모든 객체가 해시테이블의 버킷 하나에 담겨 마치 연결 리스트(linked list)처럼 동작한다. 그 결과 평균 수행 시간이 O(1)인 해시테이블이 O(n)으로 느려져서, 객체가 많아지면 도저히 쓸 수 없게 된다. 

 

1.성능을 높인답시고 해시코드를 계산할 때 핵심 필드를 생략해서는 안 된다. 

2.hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 말자. 그래야 클라이언트가 이 값에 의지하지 않게 되고, 추후에 계산 방식을 바꿀 수도 있다. 

 

롬복의

@EqualsAndHashCode 추천 

아이템 12. toString을 항상 재정의하라 

* toString은 간결하면서 사람이 읽히 쉬운 형태의 유익한 정보를 반환해야 한다. 

* Object의 toString은 클래스이름@16진수로 표시한 해시 코드 

* 객체가 가진 모든 정보를 보여주는 것이 좋다. 

* 값 클래스라면 포맷을 문서에 명시하는 것이 좋으며 해당 포맷으로 객체를 생성할 수 있는 정적 팩터리나 생성자를 제공하는 것이 좋다. 

* toString이 반환한 값에 포함된 정보를 얻을수 있는 API를 제공하는 것이 좋다. 

* 경우에 따라 AutoValue, 롬복 또는  IDE를 사용하지 않는게 적절할 수 있다. 

 

아이템 13.clone 재정의는 주의해서 진행하라 

아래 블로그 보고 공부했습니다 

https://velog.io/@alkwen0996/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%95%84%EC%9D%B4%ED%85%9C13-clone-%EC%9E%AC%EC%A0%95%EC%9D%98%EB%8A%94-%ED%95%AD%EC%83%81-%EC%A3%BC%EC%9D%98%ED%95%B4%EC%84%9C-%EC%A7%84%ED%96%89%ED%95%98%EB%9D%BC

 

[이펙티브 자바] 아이템13 | clone 재정의는 항상 주의해서 진행하라

clone 메서드 정의 > clone 메서드는 객체의 모든 필드를 복사하여 새로운 객체에 넣어 반환하는 동작을 수행한다. 즉, 필드의 값이 같은 객체를 새로 만드는 것이다. [객체를 복제하여 필드값 비교

velog.io

https://jake-seo-dev.tistory.com/31

 

이펙티브 자바, 쉽게 정리하기 - item 13. clone 재정의는 주의해서 진행하라

clone 재정의는 주의해서 진행하라 Clonable의 역할 복제해도 되는 클래스임을 나타내는 믹스인 인터페이스이다. Object 클래스에 protected clone()이라는 메서드가 있다. Cloneable 인터페이스는 clone() 메

jake-seo-dev.tistory.com

 

아이템 14장 

아래 블로그 보고 공부했습니다 

https://velog.io/@alkwen0996/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%95%84%EC%9D%B4%ED%85%9C14-Comparable%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%A0%EC%A7%80-%EA%B3%A0%EB%A0%A4%ED%95%98%EB%9D%BC

 

[이펙티브 자바] 아이템14 | Comparable을 구현할지 고려하라

Comparable 인터페이스란? 인터페이스는 객체를 정렬하는데 사용되는 메서드인 를 정의하고 있다. 인터페이스를 구현한 클래스는 반드시 를 정의해야 한다. Comparable 인터페이스 특징 자바에서 같

velog.io

 

반응형
LIST