오랜만에 이펙티브자바!
자바의 자료형 시스템은 두부분으로 나뉜다. 하나는 int, double, boolean 등의 기본 자료형, 다른 하나는 String과 List 등의 참조 자료형이다. 모든 자료형에는 대응되는 참조 자료형이 있는데 이를 객체화된 기본 자료형(boxed primitive type)이라 부른다. int, double, boolean의 객체화된 기본 자료형은 각각 Integer, Double, Boolean이다.
자바 1.5부터 자동 객체화(autoboxing)와 자동 비객체화(auto-unboxing)가 언어의 일부가 되었다. 그 둘사이에는 실직적인 차이가 있으므로 둘 가운데 무엇을 사용하고 있는지를 아는 것이 중요하며, 어떤 것을 사용할지 신중하게 결졍해야 된다.
기본 자료형과 객체화된 자료형 사이에는 세 가지 큰 차이점이 있다. 첫 번째는 기본 자료형은 값만 가지지만 객체화된 기본 자료형은 값 외에도 신원(inentity)(아이덴티티 신원?흠..)을 가진다는 것이다. 따라서 객체화된 기본 자료형 객체가 두 개 있을 때, 그 값은 같더라고 신원..은 다를 수 있다. 두번째는 기본 자료형에 저장되는 값은 전부 기능적으로 완전한 값이지만 객체화된 기본 자료형에 저장되는 값에는 그 이외에도 아무 기능도 없는 값 null이 있다는 것이다. 세번째는 기본 자료형은 시간이나 공간 요구량 측면에서 일반적으로 객체 표현형보다 효율적이라는 것이다. 주의하지 않으면 이런 차이 때문에 곤란을 겪게 될 것이다. 아래의 비교자 예제를 보자
private static final Comparator<Integer> naturalOrder = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 < o2 ? -1 : (o1 == o2 ? 0 : 1);
}
};
이 반복자는 얼핏보기엔 괜찮고 많은 테스트를 별 문제 없이 통과할 것이다. 예를 들어 위의 반복자는 Collections.sort와 함께 백만개 원소를 갖는 리스트를 (중복 여부 상관 없이) 정확히 정렬하는 용도로 사용 될 수 있다. 하지만 이 반복자에는 심각한 문제가 있다. 아래와 같이 해보자.
int compare = naturalOrder.compare(new Integer(42), new Integer(42));
System.out.println(compare);
두 객체는 42라는 동일한 값을 나타내므로 0을 반환해야 된다. 하지만 실제로 반환되는 값은 1이다. 첫번째 Integer 객체가 두 번째 보다 크다고 나온다.
눈치는 챗겠지만 두번째 o1 == 02 ? 0 : 1 삼항 연산자에 있는 ==은 객체 참조를 통해 객체인 경우 == 는 false를 반환할 것이므로 비교자는 1이라는 잘못된 값을 반환한다. 객체화된 기본 자료형에 == 연산자를 사용하는 것은 거의 항상 오류라고 봐야 한다.
이 문제를 고치는 방법으로는 int 변수에 오토박싱해서 담아 비교를 하는 것이다.
private static final Comparator<Integer> naturalOrder1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
int f = o1;
int s = o2;
return f < s ? -1 : (f == s ? 0 : 1);
}
};
위와 같이 하면 == 신원(참조) 비교를 피할 수 있다.
이제 아래의 간단한 프로그램을 보자
static Integer i;
public static void main(String[] args) {
if(i == 42){
System.out.println("Unbelievable");
}
}
이 프로그램은 Unbelievable을 출력하지 않는데 출력하지 않는 것만큼이나 이상한 짓거리를 한다. 문제는 i가 int가 아니라 Integer라는 것이다. 그리고 모든 객체 참조 필드가 그렇듯, 그 초기 값은 null이다. 위의 프로그램이 (i == 42)를 계산할 때 비교되는 것은 Integer 객체와 int 값이다. 거의 모든 경우에, 기본 자료형과 객체화된 기본 자료형을 한 연산 안에 엮어 놓으면 객체화된 기본 자료형은 자동으로 기본 자료형으로 변환된다. 위의 코드도 예외는 아니다. null인 객체 참조를 기본 자료형으로 변환하려 시도하면 NullpointException이 발생한다. 이문제는 간단하게 Integer를 int로 변환하면 잘 동작한다.
우리는 저번에 무시무시할 정도로 느린 프로그램을 봤었다.
쓸데없이 객체를 만들지 말자!
Long startTime = System.currentTimeMillis();
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
System.out.println(System.currentTimeMillis() - startTime);
이 프로그램은 우리가 예상했던거 보다 훨씬 느리다. 지역변수 sum은 long아니라 Long으로 선언 했기 때문이다. 오류도 없지만 변수가 계속해서 객체화와 비객체화를 반복하기 때문에 성능이 느려진다.
그렇다면 객체화된 기본 자료형은 언제 사용해야 하나? 첫번째는 컬렉션의 요소, 키, 값으로 사용할 때 이다. 컬렉션에는 기본 자료형은 넣을 수 없다. 다시 말해
ThreadLocal<int>
같은 변수는 선언할 수 없다. 대신
ThreadLocal<Integer>
를 써야 한다. 리플렉션을 통해 메서드를 호출 할 때도 객체화된 기본 자료형을 사용해야 한다.
요약하자만 가능하다면 기본 자료형을 사용하라는 것이다. 기본 자료형이 더 단순하고 빠르다. 객체화된 기본 자료형을 사용해야 한다면 주의하라! 자동 객체화는 번거러운 일을 줄여주긴 하지만, 객체화된 기본 자료형을 사용할 때 생길 수 있는 문제들까지 없애주진 않는다. 객체화된 기본 자료형 객체 두 개를 ==로 비교한다는 것은 그 두 객체의 신원을 비교한다는 것이며 그 것은 십중팔구 원하는 결과가 나오지 않는다. 객체화된 기본 자료형과 기본형을 한 표현식 안에 뒤섞으면 비객체화가 자동으로 일어나며, 그 과정에서 NullpointException이 발생할 수 있다. 또한 기본 ㅏㅈ료형 값을 객체화하는 과정에서 불필요한 객체들이 만들어지면 성능이 저하될 수 도 있다.