kmg28801 / effective-java

2 stars 0 forks source link

3장 모든 객체의 공통 메서드 #2

Open seojeonghyeon opened 4 months ago

seojeonghyeon commented 4 months ago

3장 모든 객체의 공통 메서드


Object는 객체를 만들 수 있는 구체 클래스이지만 기본적으로 상속해서 사용하도록 설계되어 있다. Object에서 final이 아닌 메서드(equals, hashCode, toString, clone, finalize)는 모두 재정의(overriding)을 염두에 두고 설계된 것이라 재정의 시 지켜야할 일반 규약이 명확히 정의되어 있다.

Item10. equals는 일반 규약을 지켜 재정의하라

equals를 재정의 하지 않는 것이 최선이다. 아래의 경우에 해당한다면 equals를 재정의 할 필요가 없다.

ex 2) 동치성을 검사할 필요가 없는 경우 문자열 “Hello” == “Hello”

equals를 재정의 해야 한다면 반드시 Object 명세에 적힌 일반 규약을 따라야 한다.

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_09 07 20 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_09 07 20 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_09 07 20
- 이렇게 쓰겠다는 것은 CaseInsensitiveString을 String과 동급으로 쓰겠다는 의도가 담겨 있다.(잘못된 생각)
- CaseInsensitivieString은 String을 알고 있기 때문에 처리가 가능하지만 String은 CaseInsensitiveString을 알지 못하기 때문에 처리가 불가능하다.

ii) **수정된 equals 메서드(56페이지)**

```java
// 대칭성 위배 54-55 페이지
public final class CaseInsensitiveString {
    private final String s;

    public CaseInsensitiveString(String s) {
        this.s = Objects.requireNonNull(s);
    }

    //수정된 equals 메서드(56페이지)
    @Override
    public boolean equals(Object obj) {
        **return (obj instanceof CaseInsensitiveString) &&
                (((CaseInsensitiveString) obj).s.equalsIgnoreCase(s));**
    }

    public static void main(String[] args) {
        CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
        String polish = "polish";
        **System.out.println(cis.equals(polish));**

        List<CaseInsensitiveString> list = new ArrayList<>();
        list.add(cis);
        **System.out.println(list.contains(polish));**
    }
}
```
%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_09 39 43 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_09 39 43 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_09 39 43
- 본인이 CaseInsensitiveString이라면, CaseInsensitivieString만을 지원해야 한다. 다른 타입을 지원해서는 안된다.
- 다른 타입을 지원하기 시작하면 대칭성이 깨 지기 쉽다.
%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_12 59 44 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_12 59 44 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_12 59 44
- p.equals(cp)를 할때 cp는 Point을 상속 받았기 때문에 Point의 (obj instanceof Point)의 true에 해당한다.
- ColorPoint 입장에서 Point를 바라보면 ColorPoint가 아니다.(ColorPoint가 더 구체적인 클래스) 그래서 false가 나오게 된다.

ii) 추이성 위배

```java
public enum Color {
    RED, ORANGE, YELLOW, GREEN, BLUE
}

// 단순한 불변 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 obj) {
        if (!(obj instanceof Point)) {
            return false;
        }
        Point p = (Point) obj;
        return p.x == x && p.y == y;
    }
}

//Point에 값 컴포넌트(Color)를 추가 (56페이지)
public class ColorPoint extends Point{
    private final Color color;

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

    // 코드 10-3 잘못된 코드 - 추이성 위배! (57페이지)
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point)) {
            return false;
        }

        // obj가 일반 Point면 색상을 무시하고 비교한다.
        if (!(obj instanceof ColorPoint)) {
            return obj.equals(this);  //Point
        }

        // obj가 ColorPoint면 색상까지 비교한다.
        return super.equals(obj) && ((ColorPoint) obj).color == color;
    }

    public static void main(String[] args) {
        // 두 번째 equals 메서드(코드 10-3)는 추이성을 위배한다.
        ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
        Point p2 = new Point(1, 2);
        ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
        System.out.printf("%s %s %s%n", p1.equals(p2), p2.equals(p3), p1.equals(p3));
    }
}
```
%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_14 22 15 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_14 22 15 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_14 22 15
- 굉장히 위험한 코드, ColorPoint와 동일한 레벨에 있는 서브 클래스를 만든 다음 서로 equals를 구현하면 서로 equals를 호출하면서 method가 호출될 때마다 stack이 쌓이면서 stackoverflow가 발생하게 된다.
- p1과 p2는 같다고 나올 것이다. (Color가 무시되기 때문에) p2와 p3도 같다고 생각을 하게 될 것이다. p1과 p3를 비교할 때에는 ColorPoint이기 때문에 Color까지 비교하게 되고 서로 다르다고 생각하게 된다.

iii) 리스코프 치환 원칙 위배

```java
public enum Color {
    RED, ORANGE, YELLOW, GREEN, BLUE
}

// 단순한 불변 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;
    }

    // 잘못된 코드 - 리스코프 치환 원칙 위배 (59페이지)
    // ColorPoint는 ColorPoint 끼리 같고, Point는 Point 끼리 같아야 하지 않을까?
    @Override
    public boolean equals(Object obj) {
        if (obj == null || obj.getClass() != getClass()) {
            return false;
        }
        Point p = (Point) obj;
        return p.x == x && p.y == y;
    }
}

public class CounterPointTest {
    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 Point( 1, 0);

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

        //true를 출력해야 하지만, Point의 equals가 getClass를 사용해 작성되었다면 그렇지 않다.
        System.out.println(onUnitCircle(p2));
    }
}
```
%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_22 12 01 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_22 12 01 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-02_22 12 01
리스코프 치환 원칙 : 상위 타입에서 동작 하는 코드는 하위 타입의 인스턴스를 주더라도 그대로 동작해야 한다.

ex) 대칭성 위배 예제. Timestamp, date

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

        //대칭성 위배! (60페이지)
        System.out.println(date.equals(timestamp));
        System.out.println(timestamp.equals(date));
    }
```
%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-03_19 53 54 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-03_19 53 54 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-03_19 53 54
iiii) Composition 사용(상속이 아닌 필드로 선언)

```java
// 코드 10-5 equals 규약을 지키면서 값 추가하기 (60페이지)
public class ColorPoint {
    private final Point point;
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        point = new Point(x, y);
        this.color = Objects.requireNonNull(color);
    }

    //이 ColorPoint의 Point 뷰를 반환한다.
    public Point asPoint() {
        return point;
    }

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

    @Override
    public int hashCode() {
        return 31 * point.hashCode() + color.hashCode();
    }
}
```

equals 구현 방법

equals 구현 툴

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-05_21 52 53 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-05_21 52 53 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-05_21 52 53
- 빨간색으로 표시되지만 정석적인 방법이 맞다. 자바 애노테이션 프로세서는 자신을 뜯고 고치는 코드를 생성하지 않는다. 추가로 코드를 생성하는 용도로만 만들어 졌다.
- 빨간색으로 없는 데 있는 것처럼 사용해야 한다. 컴파일할 때 만들어 주기 때문에 실행 시 정상 작동한다.
- 많이 쓰지 않는다. 많이 침투적이다. AutoValue를 사용하려면 너무 규약이 많다.

자바 11버전인 경우 → Lombok

자바 17버전인 경우 → Record, Record가 적절하지 않다면 Lombok

주의 사항

완벽 공략 24. Value 기반의 클래스(VO)

클래스 처럼 생겼지만 Int 처럼 동작하는 클래스

Record

public record Point(int x, int y) {
}

public class PointTest {
    public static void main(String[] args) {
        Point p1 = new Point(1, 0);
        Point p2 = new Point(1, 0);
        System.out.println(p1.equals(p2));
        System.out.println(p1);
        System.out.println(p1.x());
    }
}
%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-07_14 53 24 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-07_14 53 24 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-07_14 53 24

equals와 hashCode를 구현해 주고, toString도 구현되어 있음을 알 수 있다.

한 번 쓰게 되면 고칠 수 없고 변수명으로 가져온다.

불변하고 식별자가 없고 딱 안에 들어있는 값으로 equals가 구현되어 있다.

완벽공략 25. StackOverflowError

로컬 변수와 객체가 저장되는 공간의 이름은?

완벽공략 26. 리스코프 치환 원칙

객체 지향 5대 원칙 SOLID 중 하나

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

hashCode 규약

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 obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof PhoneNumber)) {
            return false;
        }
        PhoneNumber phoneNumber = (PhoneNumber) obj;
        return phoneNumber.lineNum == lineNum && phoneNumber.prefix == prefix && phoneNumber.areaCode == areaCode;
    }

    public static void main(String[] args) {
        Map<PhoneNumber, String> map = new HashMap<>();
        map.put(new PhoneNumber(707, 867, 5309),"A");
        System.out.println(map.get(new PhoneNumber(707, 867, 5307)));
    }
}

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);

        System.out.println(number1.equals(number2));
        System.out.println(number1.hashCode());
        System.out.println(number2.hashCode());

        map.put(number1, "kim");
        map.put(number2, "seo");

        String s1 = map.get(number2);
        System.out.println(s1);
        String s2 = map.get(new PhoneNumber(123, 456, 7890)); //null
        System.out.println(s2);
    }
}
%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-07_16 40 21 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-07_16 40 21 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-07_16 40 21

위 코드에서 마지막 출력 값이 null이 나오는 이유는 새로 생성된 객체에 대한 해시값에 해당하는 값이 없기 때문이다. Key에 해당하는 Hash값을 가져온다. 그리고 Hash에 해당하는 버킷의 Object를 꺼내온다.

그렇다면 다른 Key에 대해 같은 Hash를 반환한다면 어떨까? 정상동작하지만 해쉬 충돌이 발생한다. 해쉬 버켓에 들어있는 Object를 Object가 아닌 LinkedList로 바꿔준다. 집어 넣을 때에도 두 개가 같은 버켓의 LinkedList에 들어간다. 그래서 같은 해쉬값을 가지게 되면, 같은 버켓의 LinkedList를 가져와서 equals로 비교를 하게 된다. 거기서 일치하는 값을 가져온다. 이렇게 되면 HashMap을 쓰는 이유가 없어진다. 그냥 LinkedList를 쓰는 것과 동일하다.

hashCode 구현 방법( @EqualsAndHashCode Lombok 애노테이션이 훨씬 낫다)

  1. 핵심 필드 하나의 값의 해쉬값을 계산해서 result 값을 초기화 한다.
  2. 기본 타입은 Type.hashCode 참조 타입은 해당 필드의 hashCode 배열은 모든 원소를 재귀적으로 위의 로직을 적용하거나, Arrays.hashCode result = 31*result + 해당 필드의 hashCode 계산 값
  3. result를 리턴한다.(외부에 hashCode를 노출할 필요가 없다.)

완벽 공략 27. 해쉬맵 내부의 연결 리스트

내부 구현은 언제든지 바뀔 수도 있다.

자바에서 제공하는 LinkedList는 Double Ened LinkedList(이중 말단 연결 리스트), LinkedList의 맨끝이나 맨처음에 추가할 때 성능은 O(1), 조회시 성능은 O(n).

자바 8부터 해쉬 충돌이 자주 발생하는 경우에 대해 성능 최적화가 이뤄졌다. 해쉬 충돌이 자주 발생하는 경우에 대해 넣는 건 O(1)이 걸리기 때문에 문제가 없지만 가져올 땐 O(n)이 걸린다. 여기서 n은 충돌한 엔트리의 갯수이다. 버켓에 쌓여있는 갯수가 8개면, 이진 트리(레드-블랙 트리)로 바꿔준다. 이때 조회할 때 걸리는 O(logn)이 걸린다.

왜 ArrayList가 아니었을까? ArrayList는 배열이기 때문에 그만큼의 사이즈가 필요하다. LinkedList는 레퍼런스 하나만 필요하다. 공간의 효율성을 가져가고자 LinkedList를 사용하지 않았을까?

이중 말단 연결 리스트(Double Ended LinkedList)

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-08_14 12 32 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-08_14 12 32 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-08_14 12 32

헤더에 처음 노드의 참조와 함께 마지막 노드에 대한 참조도 같이 저장되기 때문에 마지막 노드에 대한 접근을 빠르게 할 수 있다는 장점을 가진다.(이중 말단 연결 리스트는 FIFO(first in first out) 구조인 큐(queue)를 구현하기 좋은 방법)

완벽 공략 28. 스레드 안전

멀티 스레드 환경에서 안전한 코드, Thread-safefy

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

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

애매모호한 clone 규약

public final class PhoneNumber implements Cloneable {
    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;
    }

    **//코드 13-1 가변 상태를 참조하지 않는 클래스용 Clone 메서드(79쪽)
    @Override
    public PhoneNumber clone() {
        try {
            return (PhoneNumber) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); //일어날 수 없는 일
        }
    }**

    public static void main(String[] args) {
        PhoneNumber phoneNumber = new PhoneNumber(707, 867, 5309);
        Map<PhoneNumber, String> m = new HashMap<>();
        m.put(phoneNumber, "제니");
        **PhoneNumber clone = phoneNumber.clone();
        System.out.println(clone != phoneNumber); //반드시 true
        System.out.println(clone.getClass() == phoneNumber.getClass()); // 반드시 true
        System.out.println(clone.equals(phoneNumber)); //true가 아닐 수 있다.**
    }
}
%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-08_17 29 27 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-08_17 29 27 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-08_17 29 27

가변 객체의 clone 구현하는 방법

Shallow Copy

public class HashTable implements Cloneable {

    private Entry[] buckets = new Entry[10];

    //LinkedList의 Head에 해당하는 노드
    private static class Entry {
        final Object key;
        Object value;
        Entry next;

        Entry(Object key, Object value, Entry next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public void add(Object key, Object value) {
            this.next = new Entry(key, value, null);
        }

        public Entry deepCopy() {
            return new Entry(key, value, next == null ? null : next.deepCopy());
        }

    **/**
     * Shallow Copy(얕은 복사)
     * 새로 만든 인스턴스 배열의 인스턴스들이 동일한 인스턴스라는 것.
     * 배열 내부에 참조하고 있는 인스턴스들이 COPY한 것과 원본이 동일하다.
     * TODO hashTable -> entryH[],
     * TODO copy -> entryC[]
     * TODO entryH[0] == entryC[0]
     */
    @Override
    public HashTable clone() {
        HashTable result = null;
        try {
            result = (HashTable) super.clone();
            result.buckets = this.buckets.clone(); //shallow copy 라서 위험하다.
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }**

    public static void main(String[] args) {
        HashTable hashTable = new HashTable();
        Entry entry = new Entry(new Object(), new Object(), null);
        hashTable.buckets[0] = entry;
        HashTable clone = hashTable.clone();
        **System.out.println(hashTable.buckets[0] == entry);
        System.out.println(hashTable.buckets[0] == clone.buckets[0]);**
    }
}
%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-08_18 47 54 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-08_18 47 54 %E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2024-06-08_18 47 54

Deep Copy

public class HashTable implements Cloneable {

    private Entry[] buckets = new Entry[10];

    //LinkedList의 Head에 해당하는 노드
    private static class Entry {
        final Object key;
        Object value;
        Entry next;

        Entry(Object key, Object value, Entry next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public void add(Object key, Object value) {
            this.next = new Entry(key, value, null);
        }

        public Entry deepCopy() {
            return new Entry(key, value, next == null ? null : next.deepCopy());
        }

    //deepCopy, 까다롭다. 기존에 있던 키를 그대로 사용했기 때문에 엄청 Deep하진 않다.
    @Override
    public HashTable clone() {
        HashTable result = null;
        try {
            result = (HashTable) super.clone();
            result.buckets = new Entry[this.buckets.length];
            result.buckets = createNewBuckets(); // 
            for (int i = 0; i < this.buckets.length; i++) {
                if (buckets[i] != null) {
                    result.buckets[i] = this.buckets[i].deepCopy(); //83페이지 deep copy
                }
            }
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

    protected Entry[] createNewBuckets(){
        throw new AssertionError();
    }

    public static void main(String[] args) {
        HashTable hashTable = new HashTable();
        Entry entry = new Entry(new Object(), new Object(), null);
        hashTable.buckets[0] = entry;
        HashTable clone = hashTable.clone();
        System.out.println(hashTable.buckets[0] == entry);
        System.out.println(hashTable.buckets[0] == clone.buckets[0]);
    }
}

Clone 대안

clone 안에서 메소드가 재정의 가능하도록 해서는 안된다.

    @Override
    public HashTable clone() {
        HashTable result = null;
        try {
            result = (HashTable) super.clone();
            result.buckets = new Entry[this.buckets.length];
            **result.buckets = createNewBuckets();**
            for (int i = 0; i < this.buckets.length; i++) {
                if (buckets[i] != null) {
                    result.buckets[i] = this.buckets[i].deepCopy(); //83페이지 deep copy
                }
            }
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

    **// 이렇게 쓰면 안된다..!!
    protected Entry[] createNewBuckets(){
        throw new AssertionError();
    }**

하위 클래스에서 오버라이딩하면 동작이 바뀔 수 있다. 생성자도 마찬가지이다. 객체를 생성하는 과정에 끼어드는 행위를 하려면 굉장히 Straight한 룰을 적용하거나 아니면 아에 오버라이딩하지 못하게 막는 게 좋다.

추상 클래스에서 상속을 허용한 계층 구조로 만든다면, Cloneable을 implements 하지 않는 게 좋다. implements하는 순간 해당 클래스의 하위 클래스를 구현하는 개발자에게 많은 짐을 떠안기는 꼴이 된다. 하위 클래스에서 Clonable을 올바르게 구현하려면 어떻게 구현해야 하는지 많은 고민을 하게 된다. 만약 상위 클래스에서 Cloneable을 구현해야 하는 상황이라면 하위 클래스에서 구현하지 못하도록 막는 방법도 있다.

/**
 * 84페이지, 126페이지 일반적으로 상속용 클래스에 Cloneable 인터페이스 사용을 권장하지 않는다.
 * 해당 클래스를 확장하려는 프로그래머에게 많은 부담을 주기 때문이다.
 */
public abstract class Shape implements Cloneable{
    private int area;
    public abstract int getArea();

    /**
     * 84페이지 부담을 덜기 위해서는 기본 clone() 구현체를 제공하여,
     * Cloneable 구현 여부를 서브 클래스가 선택할 수 있다.
     * @return
     * @throws CloneNotSupportedException
     */
//    @Override
//    protected Object clone() throws CloneNotSupportedException {
//        return super.clone();
//    }

    /**
     * 85페이지 Cloneable 구현을 막을 수도 있다.
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
}

이 클래스가 멀티 스레드 환경에 안전한 클래스로 만들어져야 한다면, clone 메서드에 sychronized를 붙여야 한다.

현실적으로는.. 생성자를 쓴다. Cloneable은 사용을 지양해야 한다.

ex. TreeSet

    public static void main(String[] args) {
        Set<String> hashSet = new HashSet<>();
        hashSet.add("kim");
        hashSet.add("seo");
        Set<String> treeSet = new TreeSet<>(hashSet);
    }

    //카피 생성자
    //TreeSet<>() 메소드
    //TreeSet을 만들고
    //Copy한다.
    public TreeSet(Collection<? extends E> c) {
        this();
        addAll(c);
    }

계층 구조가 있을 때 생성자를 쓰면 명확하다. 이전에는 final을 사용하지 못했지만 생성자를 쓰게 되면 final이여도 상관이 없다.(생성자도 처음에 선언해 주기 때문에)

Clone 대신 권장하는 방법

완벽 공략

아이템 14. Comparable을 구현할지 고민하라

compareTo 규약

compareTo 구현 방법1

compareTo 구현 방법2

완벽공략

seojeonghyeon commented 4 months ago

https://seojeonghyeon0630.notion.site/3-ac160a4f32434af188a4e4ee113169f3?pvs=4