Skip to content
\n\n

clone의 규약

\n

1. x.clone() ≠ x

\n

→ 반드시 true

\n

2. x.clone().getClass() == x.getClass()

\n

→ 반드시 true

\n

3. x.clone().equals(x)

\n

→ true일 수도 있고 아닐 수도 있다.

\n

4. x.clone.getClass() == x.getClass()

\n

관례상, 반환된 객체와 원본 객체는 독립적이어야 한다. 이를 만족하려면 super.clone으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.

\n
\n

가변상태를 참조하지 않는 clone 정의

\n
    \n
  • 가변 상태를 참조하지 않는 클래스용 clone 메서드
  • \n
\n
@Override\npublic PhoneNumber clone() {\n    try {\n        return (PhoneNumber) super.clone();\n    } catch (CloneNotSupportedException e) {\n        throw new AssertionError(); // 일어날 수 없는 일.\n    }\n}
\n
    \n
  • 원래 clone의 모습
  • \n
\n
@Override\nprotected Object clone() throws CloneSupportedException {\n    return super.clone();\n}
\n

어떤 클래스(Object)를 상속받아서 Overriding을 할 때, 접근 지시자는 상위 클래스에 있는 접근지시자와 같거나 더 넓은 범위의 접근 지시자를 가져야한다.

\n
\n💡 공변 반환 타이핑(convariant return typing) \n
\n
    \n
  • 메서드를 Overriding(재정의)할 때 재정의 된 메서드의 반환 타입이 상위 클래스의 메서드가 반환하는 하위 유형이 될 수 있다 라는 것
  • \n
\n
\n
\n
    \n
  • 공변 반환 타이핑 예제
  • \n
\n

\"Untitled\"

\n

공변 반환 타이핑의 장점

\n
    \n
  • clone()이라는 메소드를 호출하는 부분에서 타입 캐스팅을 하지 않아도된다.
  • \n
\n
\n

가변 객체를 참조하는 clone 메소드 재정의

\n
    \n
  • 아래의 예제(서적에 나온 예제)로 설명을 하겠습니다.
  • \n
\n
import java.util.Arrays;\n\n// Stack의 복제 가능 버전 (80-81쪽)\npublic class Stack implements Cloneable {\n    private Object[] elements;\n    private int size = 0;\n    private static final int DEFAULT_INITIAL_CAPACITY = 16;\n\n    public Stack() {\n        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];\n    }\n\n    public void push(Object e) {\n        ensureCapacity();\n        elements[size++] = e;\n    }\n\n    public Object pop() {\n        if (size == 0)\n            throw new EmptyStackException();\n        Object result = elements[--size];\n        elements[size] = null; // 다 쓴 참조 해제\n        return result;\n    }\n\n    public boolean isEmpty() {\n        return size ==0;\n    }\n\n    // 원소를 위한 공간을 적어도 하나 이상 확보한다.\n    private void ensureCapacity() {\n        if (elements.length == size)\n            elements = Arrays.copyOf(elements, 2 * size + 1);\n    }\n\n    @Override public Stack clone() {\n        try {\n            Stack result = (Stack) super.clone();\n            return result;\n        } catch (CloneNotSupportedException e) {\n            throw new AssertionError();\n        }\n    }\n\n}
\n

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

\n
    \n
  1. Cloneable 인터페이스를 구현해야한다.
  2. \n
  3. clone() 메소드를 재정의해야한다. → 재정의할 때, protectedpublic 으로 변경해주고, return 타입을 Object 에서 클래스 타입으로 변경해준다.
  4. \n
  5. super.clone() 호출해준다.
  6. \n
\n

3번까지는 가변 상태를 참조하지 않는 clone 하고 같다.

\n
\n

이제 아래부터는 가변 객체의 clone 구현 방법을 알아보자.

\n

가변 객체에서 clone을 구현할 떄 주의해야할 것들.

\n
    \n
  • stack, copy -> elementsS[0, 1] → stack과 copy(result) 동일한 elements를 참조한다. 즉, 다른 인스턴스에서 동일한 배열을 참조하는 것이 된다.
  • \n
\n

따라서 원본이나 복제본 중 하나를 수정하게 된다면 다른 하나도 수정되어 불변식의 깨뜨린다. → 불안한 코드다.

\n

\"Untitled

\n
public static void main(String[] args) {\n    Object[] values = new Object[2];\n    values[0] = new PhoneNumber(123, 456, 7890);\n    values[1] = new PhoneNumber(321, 764, 2341);\n\n    Stack stack = new Stack();\n    for (Object arg : values)\n        stack.push(arg);\n\n    Stack copy = stack.clone();\n\n    System.out.println(\"-----pop from stack-----\");\n    while (!stack.isEmpty())\n        System.out.println(stack.pop() + \" \");\n\n    System.out.println(\"-----pop from copy-----\");\n    while (!copy.isEmpty())\n        System.out.println(copy.pop() + \" \");   \n\n    System.out.println(\"-----같은 인스턴스 인가요?-----\");\n    System.out.println(stack.elements[0] == copy.elements[0]); // 같은 인스턴스 확인 하는 코드\n}
\n
결과 :\n-----pop from stack-----\n321-764-2341 \n123-456-7890\n-----pop from copy-----\nnull \nnull\n-----같은 인스턴스 인가요?-----\ntrue
\n

위와 같이 결과가 stack에서 pop을 했는데 copy에도 영향을 준다.

\n
\n
    \n
  • 위의 코드를 극복하려면 result.elements = elements.clone() 를 추가 해준다. → 얕은 복사
  • \n
\n

코드를 추가해주면, 인스턴스는 동일하지만 각각 배열을 만들어서 복사를 한다. 하지만 얕은 복사이기때문에 인스턴스는 같다. 실제 배열 안에 인스턴스까지 완전히 새로운 복사를 하는 것이 아니다.

\n

따라서 인스턴스를 조작한다면, copy()를 갖고 있는 PhoneNumber클래스에 영향을 준다. → 불안한 코드

\n
@Override\npublic Stack clone() {\n    try {\n        Stack result = (Stack) super.clone();\n        result.elements = elements.clone(); // 추가된 소스 코드 \n        return result;\n    } catch (CloneNotSupportedException e) {\n        throw new AssertionError();\n    }\n}
\n
    \n
  • result.elements = elements.clone() 추가할 경우 결과
  • \n
\n
-----pop from stack-----\n321-764-2341 \n123-456-7890 \n-----pop from copy-----\n321-764-2341 \n123-456-7890 \n-----같은 인스턴스 인가요?-----\ntrue 
\n

같은 인스턴스이지만 각각 배열을 만드는 방식으로 바뀐다.

\n

불안전한 방법이다. 다른 방법을 알아보자.

\n
\n

가변 상태를 갖는 클래스용 재귀적 clone메서드 재정의

\n
    \n
  • 위의 코드보다 나은 방법 → 깊은 복사(deep copy)
  • \n
\n
package me.chapter03.item13;\n\npublic class HashTable implements Cloneable {\n    private Entry[] buckets = new Entry[10];\n\n    private static class Entry{\n        final Object key;\n        Object value;\n        Entry next;\n\n        public Entry(Object key, Object value, Entry next) {\n            this.key = key;\n            this.value = value;\n            this.next = next;\n        }\n\n        public void add(Object key, Object value) {\n            this.next = new Entry(key, value, null);\n        }\n\n//      재귀적 방법\n        public Entry deepCopy(){\n            return new Entry(key, value, next == null ? null : next.deepCopy());\n        }\n\n//    ---------shallow copy---------\n//    @Override\n//    protected HashTable clone() {\n//        HashTable result = null;\n//        try{\n//            result = (HashTable) super.clone();\n//            result.buckets = this.buckets.clone(); // p82. shallow copy이기 때문에 위험하다.\n//            return result;\n//        }catch(CloneNotSupportedException e){\n//            throw new AssertionError();\n//        }\n//    }\n\n//  ---------deep copy---------\n    @Override\n    public HashTable clone() {\n        HashTable result = null;\n        try {\n            result = (HashTable) super.clone(); // 배열을 clone이 아닌 직접만듬.\n            result.buckets = new Entry[this.buckets.length]; // clone용 buckets 배열\n            for (int i = 0; i < this.buckets.length; i++) {\n                if (buckets[i] != null) {\n                    result.buckets[i] = this.buckets[i].deepCopy(); // p83. deep copy\n                }\n            }\n            return result;\n        } catch (CloneNotSupportedException e) {\n            throw new AssertionError();\n        }\n    }\n\n    public static void main(String[] args) {\n        HashTable hashTable = new HashTable();\n        Entry entry = new Entry(new Object(), new Object(), null);\n        hashTable.buckets[0] = entry;\n        HashTable clone = hashTable.clone();\n        System.out.println(hashTable.buckets[0] == entry); // true\n        System.out.println(hashTable.buckets[0] == clone.buckets[0]); //true\n    }\n}
\n
public Entry deepCopy(){\n    return new Entry(key, value, next == null ? null : next.deepCopy());\n}
\n

Entry의 deepCopy 메서드는 재귀적 방법을 사용하고 있다.

\n

이때 Entry의 deepCopy 메서드는 자신이 가리키는 연결 리스트 전체를 복사하기 위해 재귀방법을 쓴다.

\n

하지만

\n

이 방법에는 문제점이 있다.

\n

재귀 호출 때문에 리스트의 원소 수만큼 스택 프레임을 소비하여, 리스트가 길면 스택 오버플로를 일으킬 위험이 있기 때문이다.

\n

이 방법을 피하기 위해 사용하는 것이 반복자 를 사용하는 것이다.

\n
    \n
  • Entry 자신이 가리키는 연결 리스트를 반복적으로 복사한 코드
  • \n
\n
public Entry deepCopy(){\n    Entry result = new Entry(key, value, next);\n    for(Entry p = result ; p.next != null ; p = p.next){\n        p.next = new Entry(p.next.key, p.next.value, p.next.next);\n    }\n    return result;\n}
\n
    \n
  • 반복자를 사용한 결과
  • \n
\n
public static void main(String[] args) {\n    HashTable hashTable = new HashTable();\n    Entry entry = new Entry(new Object(), new Object(), null);\n    hashTable.buckets[0] = entry;\n    HashTable clone = hashTable.clone();\n    System.out.println(hashTable.buckets[0] == entry); // true\n    System.out.println(hashTable.buckets[0] == clone.buckets[0]); // false\n}
\n
\n

Clone 메서드 주의 사항

\n
    \n
  • 일반적으로 상속용 클래스에 Clonealbe 인터페이스 사용을 권장하지 않는다. 해당 클래스를 확장하려는 프로그래머에게 많은 부담을 주기 때문이다.
  • \n
\n

상속을 쓰기 위한 클래스 설계 방식 두 가지가 있다. 알아보자…

\n

1️⃣Cloneable을 직접 구현해주고 하위클래스가 구현을 안해도 되게하는 방법

\n
/**\n * p84, p126 일반적으로 상속용 클래스에 Clonealbe 인터페이스 사용을 권장하지 않는다.\n * 해당 클래스를 확장하려는 프로그래머에게 많은 부담을 주기 때문이다.\n * \n */\npublic abstract class Shape implements Cloneable {\n    private int area;\n\n    public abstract int getArea();\n\n    /**\n     * p84. 부담을 덜기 위해서는 기본 clone() 구현체를 제공하여, Cloneable 구현 여부를 서브(하위) 클래스가 선택할 수 있다.\n     * @return\n     * @throws CloneNotSupportedException\n     */\n    @Override\n    protected Object clone() throws CloneNotSupportedException {\n        return super.clone();\n    }\n    \n}
\n

2️⃣하위 클래스에서 Cloneable을 구현을 못하게 하는 방법 → final

\n
@Override\nprotected final Object clone() throws CloneNotSupportedException {\n    return super.clone();\n}
\n

3️⃣clone()을 구현할 때 고수준의 API를 사용해서 재정의한다.

\n
    \n
  • put, get 등을 말한다.
  • \n
  • 객체는 super.clone()이 만들고, 그 객체의 모든 필드는 고수준API를 통해서 데이터 접근을 한다. → 단점 저수준 보다는 처리속도가 느리다.
  • \n
\n
result = (HashTable)super.clone(); //객체를 만듬\nresult.get(key)\nresult.put(key, value) 
\n
\n

이 마지막을 이야기하기 위해서 앞에 빌드업을 했다..

\n

실질적으로 쓰이는 방법

\n
    \n
  1. 복사 생성자
  2. \n
  3. 복사 팩터리
  4. \n
\n

이 두 가지를 사용한다. 앞서 이야기한 것들을 극복해주는 방법들이다..

\n

1️⃣복사 생성자

\n
public class HashSetExample {\n    public static void main(String[] args) {\n        Set<String> hashSet = new HashSet<>();\n        hashSet.add(\"Dante\");\n        hashSet.add(\"DeokRin\");\n        System.out.println(\"HashSet: \" + hashSet);\n\n        Set<String> treeSet = new TreeSet<>(hashSet);\n        System.out.println(\"TreeSet: \" + treeSet);\n\n    }\n} 
\n

TreeSet 생성자로 hashset을 받는다. 엄격히 말하자면 Collection으로 받는다. 그리고 생성자에서 copy를 해준다.

\n
    \n
  • TreeSet 생성자
  • \n
\n

\"Untitled

\n

복사 생성자의 장점

\n
    \n
  • \n

    생성자를 쓰면 좋은점은 명확해진다는 것이다.

    \n
      \n
    • clone() 메소드는 생성자를 써서 만들지 않기 때문에 어떻게 만들어지는지 불명확하기 때문에 좋지않다.
    • \n
    \n
  • \n
  • \n

    final을 사용할 수 있다.

    \n
      \n
    • clone() 메소드 때문에 final을 못쓰는 것은 손실이 크다. 따라서 생성자를 쓰면 앞서 설정을 해주기 때문이다.
    • \n
    \n
  • \n
  • \n

    해당 클래스가 구현한 ‘인터페이스’ 타입의 인스턴스를 인수로 받을 수 있다.

    \n
      \n
    • 모든 범용 컬렉션 구현체는 Collection이나 Map 타입을 받는 생성자를 제공한다.
    • \n
    \n
  • \n
","upvoteCount":1,"answerCount":6,"acceptedAnswer":{"@type":"Answer","text":"

다시 보니 블로그가 있군요 👍
\n블로그 내용에서 궁금한 점도 있고, 알게 된 점도 있어 몇 자 적어봅니다.

\n
    \n
  1. 초반부에서 protected 에 대한 설명을 작성해주셨습니다.
  2. \n
\n
\n

protected 는 서로 다른 패키지에서 호출할 수 없고, 서로 다른 패키지라도 상속은 가능하다.
\n그렇기 때문에 clone() 를 사용하려는데 Object 클래스가 필요하지만, protected 때문에 다른 패키지에 있는 Objectclone() 메서드를 호출하지 못한다. ㅠㅠ

\n
\n

이 문장을 여러 번 읽어봤는데 이해가 잘 안되네요. 다른 패키지라면 java.lang 패키지를 말하는 건가요~?

\n
    \n
  1. 제가 알기로 Cloneable 인터페이스는 마커용으로 알고 있습니다.
  2. \n
\n
\n

Note that this interface does not contain the clone method. Therefore, it is not possible to clone an object merely by virtue of the fact that it implements this interface. Even if the clone method is invoked reflectively, there is no guarantee that it will succeed.

\n
\n

Javadoc 에 나와 있는 것처럼 Cloneable 인터페이스를 implements 하더라도 복제되는 것을 보장하지 않고, 재정의를 \"잘\"해야 할 것으로 보입니다.

\n
\n

Cloneable 은 복제할 수 있는 클래스라는 것을 JVM 에 알려주기 위한 구분자 역할을 한다. 즉, 복제가 가능한 클래스인지 아닌지 구분해준다.

\n
\n

그래서 복제가 가능한 클래스인지 아닌지 구분해준다는 말이 궁금해서 찾아보니 clone 메서드를 호출하려고 할 때 Cloneable 인터페이스를 구현하지 않으면 CloneNotSupportedException 가 발생한다고 하네요.

\n
\n

Invoking Object's clone method on an instance that does not implement the Cloneable interface results in the exception CloneNotSupportedException being thrown.

\n
","upvoteCount":1,"url":"https://github.com/orgs/Study-2-Effective-Java/discussions/42#discussioncomment-4571381"}}}
Discussion options

You must be logged in to vote

다시 보니 블로그가 있군요 👍
블로그 내용에서 궁금한 점도 있고, 알게 된 점도 있어 몇 자 적어봅니다.

  1. 초반부에서 protected 에 대한 설명을 작성해주셨습니다.

protected 는 서로 다른 패키지에서 호출할 수 없고, 서로 다른 패키지라도 상속은 가능하다.
그렇기 때문에 clone() 를 사용하려는데 Object 클래스가 필요하지만, protected 때문에 다른 패키지에 있는 Objectclone() 메서드를 호출하지 못한다. ㅠㅠ

이 문장을 여러 번 읽어봤는데 이해가 잘 안되네요. 다른 패키지라면 java.lang 패키지를 말하는 건가요~?

  1. 제가 알기로 Cloneable 인터페이스는 마커용으로 알고 있습니다.

Note that this interface does not contain the clone method. Therefore, it is not possible to clone an object merely by virtue of the fact that it implements this interface. Even if the clone method is invoked reflectively, there is no guarantee that it will succeed.

Javadoc 에 나와 있는 것처럼 Cloneable 인터페이스를 implements 하더라도 복제되는 것을 보장하지 …

Replies: 6 comments 7 replies

Comment options

You must be logged in to vote
2 replies
@YuDeokRin
Comment options

YuDeokRin Jan 1, 2023
Maintainer Author

@chikeem90
Comment options

Comment options

You must be logged in to vote
1 reply
@YuDeokRin
Comment options

YuDeokRin Jan 2, 2023
Maintainer Author

Comment options

You must be logged in to vote
1 reply
@YuDeokRin
Comment options

YuDeokRin Jan 2, 2023
Maintainer Author

Comment options

You must be logged in to vote
2 replies
@chikeem90
Comment options

@YuDeokRin
Comment options

YuDeokRin Jan 2, 2023
Maintainer Author

Answer selected by YuDeokRin
Comment options

You must be logged in to vote
1 reply
@YuDeokRin
Comment options

YuDeokRin Jan 2, 2023
Maintainer Author

Comment options

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3장 모든 객체의 공통 메서드 이펙티브 자바 3장 (모든 객체의 공통 메서드)
5 participants