티스토리 뷰

자바의 데이터 타입은 크게 두 가지로 나눌 수 있습니다.

바로 int, double, boolean 같은 기본 타입과 String, List와 같은 참조 타입입니다.

그리고 각각의 기본 타입에는 대응하는 참조 타입이 하나씩 있으며, 이를 박싱된 기본 타입이라고 합니다.

예컨대 int, double, boolean에 대응하는 박싱된 기본 타입은 Integer, Double, Boolean 입니다.

아이템 6에서 이야기했듯, 오토박싱과 오토언박싱 덕분에 두 타입을 크게 구분하지 않고 사용할 수는 있지만, 그렇다고 차이가 사라지는 것은 아닙니다.

둘 사이에는 분명한 차이가 있으니 어떤 타입을 사용하는지는 상당히 중요합니다.

즉, 주의해서 선택해야 한다는 의미입니다.

기본 타입과 박싱된 기본 타입의 주된 차이는 크게 세 가지입니다.

  1. 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성이란 속성을 갖는다.
  2. 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 null을 가질 수 있다.
  3. 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.

이상의 세 가지 차이 때문에 주의하지 않고 사용하면 진짜로 문제가 발생할 수 있습니다.

다음은 Integer 값을 오름차순으로 정렬하는 비교자입니다.

Integer는 그 자체로 순서가 있으니 이 비교자가 실질적인 의미는 없지만, 아주 흥미로운 점을 하나 보여줍니다.

Comparator<Integer> naturalOrder =
    (i, j) -> (i < j) ? -1 : (i == j ? 0: 1);

리스트에 중복이 있어도 상관없습니다.

하지만 심각한 결함이 숨어 있습니다.

이 결함을 눈으로 확인하고 싶다면 naturalOrder.compare(new Integer(42), new Integer(42))의 값을 출력해봅시다.

두 Integer 인스턴스의 값이 42로 같으므로 0을 출력해야 하지만, 실제로는 1을 출력합니다.

즉, 첫 번째 Integer가 두 번째보다 크다고 주장하고 있습니다.

🤔
대체 원인이 뭘까?

 

naturalOrder의 첫 번째 검사(i < j)는 잘 작동합니다.

여기서 i와 j가 참조하는 오토박싱된 Integer 인스턴스는 기본 타입 값으로 변환됩니다.

그런 다음 첫 번째 정숫값이 두 번째 값보다 작은지를 평가합니다.

만약 작지 않다면 두 번째 검사(i == j)가 이뤄집니다.

그런데 이 두 번째 검사에서는 두 '객체 참조'의 식별성을 검사하게 됩니다.

i와 j가 서로 다른 Integer 인스턴스라면 (비록 값은 같더라도) 이 비교의 결과는 false가 되고, 비교자는 (잘못된 결과인) 1을 반환합니다.

즉, 첫 번째 Integer 값이 두 번째보다 크다고 결과가 나타나집니다.

이처럼 박싱된 기본 타입에 == 연산자를 사용하면 오류가 일어납니다.

실무에서 이와 같이 기본 타입을 다루는 비교자가 필요하다면 Comparator.naturalOrder()를 사용합시다.

비교자를 직접 만들면 비교자 생성 메서드나 기본 타입을 받는 정적 compare 메서드를 사용해야 합니다. (아이템 14)

그렇더라도 이 문제를 고치려면 지역변수 2개를 두어 각각 박싱된 Integer 매개변수의 값을 기본 타입 정수로 저장한 다음, 모든 비교를 이 기본 타입 변수로 수행해야 합니다.

이렇게 하면 오류의 원인인 식별성 검사가 이뤄지지 않습니다.

Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
    int i = iBoxed, j = jBoxed; // 오토박싱
    return i < j ? -1 : (i == j ? 0 : 1);
};

이제 다음의 간단한 프로그램을 살펴봅시다.

public class Unbelievable {
    static Integer i;

    public static void main(String[] args) {
        if (i == 42)
            System.out.println("믿을 수 없군!");
    }
}

이 프로그램은 물론 "믿을 수 없군!"을 출력하지 않지만 그만큼 기이한 결과를 보여줍니다.

i == 42를 검사할 때 NullPointerException을 던집니다.

원인은 i가 int가 아닌 Integer이며, 다른 참조 타입 필드와 마찬가지로 i의 초깃값도 Null이라는 데 있습니다.

즉, i == 42는 Integer와 int를 비교하는 것입니다.

거의 예외 없이 기본 타입과 박싱된 기본 타입을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 풀립니다.

그리고 null 참조를 언박싱하면 NullPointerException이 발생합니다.

이 예에서 보듯, 이런 일은 어디서든 벌어질 수 있습니다.

다행히 해법은 간단합니다.

i를 int로 선언해주면 끝납니다.

이번에는 아이템 6의 코드를 다시 살펴봅시다.

private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <=Integer.MAX_VALUE; i++) {
        sum += i;
    }
    return sum;
}

이 프로그램은 실수로 지역변수 sum을 박싱된 기본 타입으로 선언하여 느려졌습니다.

오류나 경고 없이 컴파일되지만, 박싱과 언박싱이 반복해서 일어나 체감될 정도로 성능이 느려집니다.

이번 아이템에서 다룬 세 프로그램 모두 문제의 원인은 하나입니다.

프로그래머가 기본 타입과 박싱된 기본 타입의 차이를 무시한 대가를 치른 것이죠.

처음 두 프로그램은 뼈아픈 실패로 이어졌고, 마지막은 심각한 성능 문제가 발생했습니다.

그렇다면 박싱된 기본 타입은 언제 써야 하는가?

첫 번째, 컬렉션의 원소, 키, 값으로 씁니다.

컬렉션은 기본 타입을 담을 수 없으므로 어쩔 수 없이 박싱된 기본 타입을 써야만 합니다.

더 일반화해 말하면, 매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로는 박싱된 기본 타입을 써야 합니다.

자바 언어가 타입 매개변수로 기본 타입을 지원하지 않기 때문입니다.

예컨대 변수를 TheradLocal<int> 타입으로 선언하는 건 불가능하며, 대신 TreadLocal<Integer>로 써야 합니다.

마지막으로, 리플렉션(아이템 65)을 통해 메서드를 호출할 때도 박싱된 기본 타입을 사용해야 합니다.

정리

  • 기본타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 가능하면 기본 타입을 사용하라.
  • 기본 타입은 간단하고 빠르다.
  • 박싱된 기본 타입을 써야 한다면 주의를 기울이자.
  • 오토박싱이 박싱된 기본 타입을 사용할 때의 번거로움을 줄여주지만, 그 위험까지 없애주지는 않는다.
  • 두 박싱된 기본 타입을 == 연산자로 비교한다면 식별성 비교가 이뤄지는데, 이는 여러분이 원한게 아닐 가능성이 높다.
  • 같은 연산에서 기본 타입과 박싱된 기본 타입을 혼용하면 언박싱이 이뤄지며, 언박싱 과정에서 NullPointerException을 던질 수 있다.
  • 마지막으로, 기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용을 낳을 수 있다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함