아이템12는 모든 객체의 공통 메서드 중 toString()
의 재정의에 대해 설명하고 있다.
toString
메서드에 대해 간단히 알아보자면,Object
클래스에 정의된 메서드이다.Object
를 상속받기 때문에 toString
메서드를 가지고 있다.하지만 Object
의 기본 toString
메서드가 우리가 작성한 클래스에 적합한 문자열을 반환하는 경우는 거의 없다.
이 메서드는 PhoneNumber@adbdb 처럼 단순히 클래스_이름@16진수로 표시한 해시코드를 반환할 뿐이다.
toString
의 일반 규약에 따르면,
'간결하면서 사람이 읽기 쉬운 형태의 유익한 정보' 를 반환해야 한다.
예를 들면, PhoneNumber@adbdb 보다는 010-1234-5678 처럼 전화번호로 직접 표현하는게 유익하다.
또 다른 규약으로는,
'모든 하위 클래스에서 이 메서드를 재정의하라' 이다.
저자는 해당 규약을 "정말 새겨들어야 할 조언이다!" 라고 말한다.
. . .
* 개인적인 생각으로는 객체의 내부 상태를 외부로 노출시키고 싶지 않은 경우나 문자열 표현이 큰 의미를 갖지 않는 경우는 toString을 재정의할 필요가 없다고 생각한다.
toString
메서드는 아래 상황에서 자동으로 호출한다.
좋은 toString
은 이 인스턴스를 포함하는 객체에서 유용하게 쓰인다.
예를 들어 map 객체를 출력했을 때 {Jenny = 010-1234-5678} 처럼 말이다.
다음으로는 toString
에 주요 정보가 담기지 않았을 때 문제가 되는 대표적인 예인 테스트 실패 메시지이다.
❌ Assertions failure: expected {abc, 123}, but was {abe, 123}
// 단언 실패: 예상 값 {abc, 123}, 실젯값 {abc, 123}
아래와 같은 경우에 위와 같은 메시지가 나올 수 있다.
다른 예시로 보겠다.
public class Wizard {
private String lastName;
private String firstName;
@Override
public String toString() {
return "Wizard{" +
"성 ='" + lastName + '\'' +
'}';
} // toString 에 lastName 정보만 담겨져 있다.
}
class WizardTest {
@Test
void toString_테스트() {
Wizard w1 = new Wizard("포터", "제임스");
Wizard w2 = new Wizard("포터", "해리");
Assertions.assertEquals(w1, w2); // 둘의 equals 비교
}
}
❌ toString_테스트() // 서로 다르다!
Expected :Wizard{성 ='포터'}
Actual :Wizard{성 ='포터'} // 그러나 보기에는 같아보임.
toString
을 구현할 때면 반환값의 포맷을 문서화할지 정해야 한다.전화번호나 행렬 같은 값 클래스는 문서화하길 권장한다.
포맷을 명시하면('010-1234-5678' 와 같이) 사람이 읽기 쉽다.
따라서 이 값 그대로 입출력에 사용하거나, 사람이 읽을 수 있는 데이터 객체로 저장할 수도 있다.
포맷을 명시하기로 했다면, 명시한 포맷에 맞는 문자열과 객체를 서로 전환할 수 있는 정적 팩터리나 생성자를 함께 제공하면 좋다.
// BigInteger, BigDecimal과 대부분의 기본 타입 클래스가 여기 해당한다.
String s = "10000000000000";
BigInteger b1 = new BigInteger(s);
포맷을 한 번 명시 후에, 프로그래머들이 해당 포맷에 맞춰 의존적인 코드를 작성한다면
포맷을 바꿨을 때 그 코드들은 엉망이 돼버린다.
반대로 포맷을 명시하지 않는다면, 이 부분에서는 유연성을 얻게 된다.
이펙티브 자바에서 가져온 코드다.
/**
* 이 전화번호의 문자열 표현을 반환한다.
* 이 문자열은 "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() { ... }
이러한 설명이 있어야 프로그래머들이 이 포맷에 맞춰 코드를 작성할 것이다.
아래 예시를 보면 상세 형식은 정해지지 않았으며 향후 변경될 수 있다.
라고 적혀있다.
그럼에도 불구하고 해당 포맷의 값을 가공한 코드를 작성하면 자신을 탓할 수 밖에 없다.
이건 getter
와 같은 메서드를 제공하라는 의미이다.
어차피 toString
으로 공개된 데이터라면, toString
을 구성하는 각각의 데이터를 따로따로 받을 수 있는 메서드들을 제공하자는 것이다.
만약 PhoneNumber.class
의 각 필드에 대한 getter
가 없다면,
각 정보가 필요한 프로그래머는 toString
의 반환값을 파싱할 수 밖에 없다. 상당히 비효율적이고 필요없는 작업이다.
equals()
, hashCode()
메서드들과 마찬가지로 toString()
도 구글의 AutoValue 프레임워크나 IDE에서 생성해주는 기능이 있다. 하지만 '클래스의 특성' 까지는 파악하지 못한다. 예컨대 앞서의 PhoneNumber 클래스가 이런 경우이다. 하지만, 이런 경우라도 Object의 toString
보다는 훨씬 유용하다.