본문 바로가기
Language/Effective java

모든 객체의 공통 메서드

by y.j 2022. 6. 7.
728x90

toString을 항상 재정의하라.

toString을 잘 구현한 클래스는 사용하기에 훨씬 즐겁고, 그 클래스를 사용한 시스템은 디버깅하기 쉽다.

 

일반 규약

간결하면서 사람이 읽기 쉬운 형태의 유익한 정보를 반환하라.
모든 하위 클래스에서 이 메서드를 재정의하라.

 

실전에서 toString은 그 객체가 가진 주요 정보 모두를 반환하는 게 좋다.

 - 스스로를 완벽히 설명하는 문자열이어야 한다.

   eg. "맨해튼 거주자 전화번호부(총 1487536개)"

 

포맷을 명시하든 아니든 여러분의 의도는 명확히 밝혀야 한다.

 - 포맷을 명시하거나 주석을 작성하여 의도를 명백히 밝혀야 한다.

/*
 * 이 전화번호의 문자열 표현을 반환한다.
 * 이 문자열은 "XXX-YYY-ZZZZ"형태의 12글자로 구성된다.
 * XXX는 지역코드, YYY는 프리픽스, ZZZZ는 가입자 번호다.
 * 각각의 대문자는 10진수 숫자 하나를 나타낸다.
 * 
 * 전화번호의 각 부분의 값이 너무 작아서 자릿수를 채울 수 없다면,
 * 앞에서부터 0으로 채워나간다.
 * 예컨대 가입자 번호가 123이라면
 * 전화번호의 마지막 네 문자는 "0123"이 된다.
 */
    @Override
    public String toString() {
        return String.format("%03d-%03d-%04d",
                areaCode, prefix, lineNum);
    }
}

toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자.

 - 필드에 접근 할 수 있는 접근자를 제공해야 한다.

    접근자가 없으면 이 정보가 필요한 프로그래머는 toString의 반환 값을 파싱 할 수 밖에 없다.

    성능에 문제가 생기고 사실상 준-표준 API나 다름없어진다.

 

clone 재정의는 주의해서 진행하라.

clone 메서드를 잘 동작하게끔 해주는 구현 방법과 언제 사용해야 하는지 다른 선택지이다. Object에 clone메서드는 protected로 되어 있어 Cloneable를 구현하는 것만으로는 clone메서드를 호출 할 수 없다.

Clonable방식은 널리 쓰이고 있기 때문에 아래 3가지에 대해서 잘 알아두면 좋다.
1. clone메서드를 잘 동작하게끔 해주는 구현 방법
2. 구현해야하는 시점
3. 가능한 다른 선택지

 

Cloneble인터페이스는 대체 무슨 일이 할까? Cloneable인터페이스는 Object의 protected 메서드인 clone의 동작 방식을 결정한다. Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 필드들을 모두 복사한 객체를 반환하고 구현되지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다. 실무에서 Cloneable을 구현한 클래스는 clone메서드를 public으로 제공하며, 사용자는 당연히 복제가 제대로 이뤄지리라 기대한다.

 

clone메서드의 일반 규약

이 객체의 복사본을 생성해 반환한다. '복사'의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있다.

x.clone() != x [ 필수 참 ]

x.clone().getClass() == x.getClass() [ 필수 참 ]

x.clone().equals(x) [ 참이 필수는 아님 ]

x.clone().getClass() == x.getClass() [ 참이 필수는 아님 ]
 - 메서드가 반환하는 객체는 super.clone을 호출해 얻어야 한다. 이 클래스와 Object를 제외한 모든 클래스가 이 관례를 따른다면 참이다.

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

 

만드는 방법은 단순한다. super.clone메서드에 다운 캐스팅을 해주면 된다.

@Override
public PhoneNumber clone() {
    try {
        return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

 

clone메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.

Stack을 봤을 때 Stack 내부는 메모리를 스스로 관리할 수 있는 Object[] elements가 존재한다. Stack.clone()을 통해 복사객체는 만들 수 있지만 Object[] elements는 공유하게 될 것이다. 그래서 elements도 복제해주어야 한다.

@Override
public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

* 배열을 복제할 때는 컴파일타임 타입과 런타임 타입이 모두 원본 배열과 같은 타입을 반환하기 때문에 타입변환이 필요 없다.

 

Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌한다( 원본과 필드를 공유해도 되는 경우 제외 ). 복제 할 수 있는 클래스를 만들기 위해 일부 필드에서 final 한정자를 제거해야 할 수도 있다.

 

HashTable같은 경우에는 clone을 재귀적으로 호출하는 것만으로는 충분하지 않는다. HashTable은 Key와 value를 가지고 있는 한쌍의 Entry로 구성되어 있다. 아래 코드같은 경우에는 자신만의 buckets을 가지게 되지만, 참조 객체의 경우는 복제가 되지 않아 원본과 복제본 둘 다 예기치 않게 동작할 가능성이 있다.

@Override
public HashTable clone() {
    try {
        HashTable result = (HashTable) super.clone();
        result.buckets = buckets.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

 

Entry배열을 따로 만들고, buckets에 있는 모든 객체를 복사해주어야 한다.

@Override
public HashTable clone() {
    try {
        HashTable result = (HashTable) super.clone();
        result.buckets = new Entry[buckets.length];
        for(int i = 0; i < buckets.length; i++)
            if(buckets[i] != null)
                result.buckets[i] = buckets[i].deepCopy();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

 

고수준의 API를 사용하는 방법도 존재한다. HashTable에 예처럼 다 복사를 하지 않고, put메서드를 만들어 put(key, value)로 내용을 똑같이 만들어준다. 하지만, 이 방법은 처리가 느리고 Cloneable아키텍처의 기초인 필드 단위 객체 복사를 우회하기 때문에 어울리지 않는 방식이다. 또, put메서드가 하위 클래스에서 재정의 될 경우 원본과 복사본이 달라질 가능성이 있으므로 private이나 final로 정의해야 한다.

 

재정의한 clone은 CloneNotSupportedException을 하지 않고 사용해야 편하다.

 

상속용 클래스는 Cloneable을 구현해서는 안된다. Object의 clone처럼 제대로 된 clone메서드를 구현해 protected로 두고 CloneNotSupportedException던질 수 있도록 선언하거나 clone을 동작하지 않게 구현해놓고 하위 클래스에서 재정의 못하게 할 수도 있다.

 

Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화해줘야 한다.

 

요약

Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다.
제한자는 public, 자신 클래스로 타입을 변경한다.
super.clone을 먼저 호출한 후 필요한 필드 전부를 적절히 수정한다.
객체의 있는 모든 가변 객체 및 참조객체를 모두 복사한다.

 

다른 방법

복사 생성자와 복사 팩터리를 만들자.

 

복사 생성자 ( 변환 생성자 )

public Yum(Yum yum) {
    ...
}

 

복사 팩터리 ( 변환 팩토리 )

public static Yum newInstance(Yum yum) {
    ...
}

 

장점

  • 언어 모순적이고 위험천만한 객체 생성 메커니즘을 사용하지 않는다.
  • 엉성하게 문서화된 규약에 기대지 않는다.
  • 정상적인 final 필드 용법과도 충돌하지 않는다.
  • 불필요한 검사 예외를 던지지도 않는다.
  • 형변환도 필요치 않다.
  • 인터페이스 타입으로 인수를 받을 수 있다.
728x90

'Language > Effective java' 카테고리의 다른 글

클래스와 인터페이스  (0) 2022.06.12
모든 객체의 공통 메서드  (0) 2022.06.11
모든 객체의 공통 메서드  (0) 2022.06.06
모든 객체의 공통 메서드  (0) 2022.06.06
객체 생성과 파괴  (0) 2022.06.05

댓글