Java

[Effective Java] 아이템12. toString을 항상 재정의하라

jun9.com 2022. 3. 31. 22:57

요약

  • toString을 재정의해야하는 이유
  • toString을 재정의할 때 유의할점
  • format을 지정할 때 주의할 점
  • toString을 재정의 할 필요가 없는 경우

 


 

 

1. toString을 재정의해야하는 이유

 

toString 일반 규약

간결하면서 사람이 읽기 쉬운 형태의 정보를 제공해야 한다.

 

 

Object의 toString

Object에서 제공하는 toString은 클래스이름@16진수_해시코드를 반환한다.

public String toString() {
    return this.getClass().getName() + "@" + Integer.toHexString(this.hashCode());
}

toString을 재정의하지 않는다면 로그메시지에 16진수 해시코드만 잔뜩 남을 것!

로그는 정보성/디버깅 용도로 남기는데,  의미있는 정보를 얻기 힘들다.

 

 

[예시] toString을 재정의하지 않고 PhoneNumber 객체를 출력할 때 

{PhoneNumber@adbbd}

 

-> 대신 PhoneNumber에서 중요한 필드인 핸드폰 번호를 출력하도록 재정의하면, 

{Jenny=707-867-5309} 와 같이 전화번호를 반환하는 형태가 되어 디버깅에 훨씬 유용하다.

 

 

[참고] 출력문에서 toString()이 어떻게 호출되는가?

public void println(Object x) {
        String s = String.valueOf(x); // 출력시 출력값을 valueOf로 생성한다.
        synchronized (this) {
            print(s);
            newLine();
        }
    }
 public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString(); // toString()을 사용한다.
    }

 


 

2. toString()을 재정의 할 때 유의할 점

  1. 객체가 가진 주요정보를 모두 반환하는게 좋다. 
  2. 객체 스스로를 완벽히 설명하는 문자열이여야 한다.
  3. toString이 반환한 정보를 얻어올 수 있는 API를 제공해야 한다. (없다면 개발자가 파싱해서 가져와야 하므로 성능 나빠진다.)

 


 

3. format을 지정할 때 주의할 점

1. 값 클래스(ex: 전화번호, 행렬)라면 formatting을 해주는 것이 좋다.

- 값 만으로는 자신의 정보를 충분히 설명하지 못하는 경우가 많다.

- 포맷팅한다면  그 값을 그대로 입출력에 사용하거나 csv파일로 사람이 읽을 수 있는 데이터 객체로 저장할 수 있다.

 

 

 

2. 포맷문자열과 객체를 상호전환할 수 있는 정적 팩터리나 생성자를 함께 제공하면 좋다.

ex) BigInteger, BigDecimal

의 toString(): 지정한 radix(진수)로 변환하여 출력한다.

지정한 radix 범위를 넘어가면 (2~36) 10진수를 디폴트로 변환한다. 

BigInteger bigInteger1 = new BigInteger(321456);
bigInteger1.toString(2); // 1001110011110110000

 

10진수로 다시 변환 가능

public BigInteger(String val) {
        this(val, 10);
    }

 

3. 한 번 포맷을 지정하면 형태를 바꾸는 것이 어려우므로 초기에 문제 없는 값으로 포맷팅 해야 한다.

바꾸면 기존 데이터, 코드들에 영향을 끼친다.

새 포맷으로 적용한 것들이 기존 코드에 적용되지 못하고, 새롭게 만든 코드에는 기존 데이터가 적용되지 못한다.

실제로 운영중인 서비스라면, 수정이 누락된 경우가 흔하게 발생할 수 있고 -> 이는 사용자가 체감하는 심한 에러를 발생시킬 수 있다.

 

 

4. 명세에 아주 자세하게 적어야한다.

[포맷을 명시하는 경우]

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

 

[포맷을 명시하지 않는 경우]

/**
* 이 약물에 관한 대략적인 설명을 반환한다.
* 다음은 이 설명의 일반적인 형태이나,
* 상세 형식은 정해지지 않았으며 향후 변경될 수 있다.
* 
* "[약물 #9: 유형-사랑, 냄새=테러빈유, 겉모습=먹물]"
*/
@Override
public String toString() { ... }

4. toString()을 재정의할 필요가 없는 경우

1.  유틸성 정적 클래스

- toString()의 니즈가 없다.

 

2. ENUM

- 이미 적절한 toString을 제공한다.

    - values()로 모든 value값을 가져올 수 있다. 

    - name, value 원하는 값을 가져올 수 있다.

+) 하위클래스들이 공유할 문자열이 있는 추상클래스라면 재정의 해야한다.