peaches-book-study / effective-java

이펙티브 자바 3/E
0 stars 2 forks source link

Item 61. 박싱된 기본 타입보다는 기본 타입을 사용하라 #61

Open youngkimi opened 2 months ago

youngkimi commented 2 months ago

Chapter : 9. 일반적인 프로그래밍 원칙

Item : 61. 박싱된 기본 타입보다는 기본 타입을 사용하라

Assignee : youngkimi


🍑 서론

자바의 데이터 타입은 크게 기본 타입과 참조 타입으로 나눌 수 있다.

기본 타입에는 대응하는 참조 타입이 하나씩 있으며, 이를 박싱된 기본 타입이라고 한다.

오토박싱과 언박싱 덕분에 두 타입을 크게 구분하지 않고 사용할 수는 있으나, 차이가 사라지는 것은 아니다.

둘 사이에는 분명한 차이가 있으며, 이를 주의하여 사용해야 한다.

🍑 본론

차이 1. 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성(identify)이란 속성이 있다.

박싱된 기본 타입의 인스턴스는 값이 같아도 다르다고 식별될 수 있다.

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

Integer A = new Integer(3);
Integer B = new Integer(2);

System.out.println("A is Bigger than B: " + naturalOrder.compare(A, B));

A is Bigger than B: 1

아주 정상적으로 작동한다. 하지만 값이 같다면?

Integer A = new Integer(2);
Integer B = new Integer(2);

System.out.println("A is Bigger than B: " + naturalOrder.compare(A, B));

A is Bigger than B: 1

값이 같아도 결과로 1(A 보다 B가 크다)이 출력된다.

참고로 Integer 생성자에 int를 삽입하는 것은 java 9부터 deprecated 되었다. 또한, new Integer(int) 와 같은 형식은 캐싱된 값을 불러오는 것이 아니라 새 객체를 생성한다. 후술 예정.

여기서 첫 번째 검사 i < j ? -1 는 잘 작동한다. 두 수를 비교하는 과정에서 자연스럽게 오토 언박싱이 발생한다. 하지만 그 다음, i == j에서 제대로 작동하지 않는다. == 연산을 통해 두 객체의 식별성을 검사하는데, 둘이 다른 인스턴스이므로 값이 같더라고 비교의 결과가 거짓이 되어 1을 반환하게 된다. 이처럼 박싱된 기본 타입에 == 연산자를 사용하면 오류가 일어난다.

실무에서는 이처럼 기본 타입을 비교하는 비교자가 필요한 경우에는 지역 변수 두개를 두어 각각 박싱된 기본 타입을 기본 타입 정수로 저장한 뒤, 비교를 수행해야 한다.

Comparator<Integer> naturalOrder =
    (iBox, jBox) -> {
        int i = iBox, j = jBox;
        return (i < j) ? -1 : (i == j ? 0 : 1);
    }

==, != 는 값을 비교하는 연산이 아니라 주소를 비교하는 연산이다. 때문에 박싱된 기본 타입 비교에는 equals 등을 사용하여 값을 비교해야 한다.

하지만 예외도 존재한다. 효율적 연산을 위해 아래 범위 값을 갖는 포장 객체는 서로 공유하여 사용된다.

타입 내용
boolean true, false
char \u0000 ~ \u007f
int -128 ~ 127
public class Main {
    static Boolean T1 = true, T2 = true;
    static Integer I1 = 10, I2 = 10;
    static Integer I3 = 128, I4 = 128;

    public static void main(String[] args) {
        System.out.println("T1 == T2: " + (T1 == T2));
        System.out.println("I1 == I2: " + (I1 == I2));
        System.out.println("I3 == I4: " + (I3 == I4));
    }
}

T1 == T2: true I1 == I2: true I3 == I4: false

차이 2. 기본 타입의 값은 언제나 유효하다. 하지만 박싱된 기본 타입은 유효하지 않은 값(null)을 가질 수 있다.

static Integer i;

public static void main(String[] args) {
    if (i == 42) {
        System.out.println("Unbelievable!");
    }
}

이 프로그램은 i == 42 를 조회하는 과정에서 NPE를 던진다. 원인은 i가 int가 아닌 Integer이고, i의 초기값이 null 이기 때문이다. i == 42 연산은 Integer와 int를 비교하는 연산이고, 이러한 경우에서는 거의 예외 없이 Integer의 박싱이 풀린다. 해법은 간단하다. i를 int로 선언하면 된다.

차이 3. 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용 면에서 더 효율적이다.

static Long sum = 0L;

public static void main(String[] args) {
    long startTime = System.currentTimeMillis();

    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }

    System.out.println(sum);

    long endTime = System.currentTimeMillis();

    System.out.printf("소요 시간: %d.%d초 ", (endTime - startTime)/1000, (endTime - startTime)%1000);
}

2305843005992468481 소요 시간: 5.805초

여기에서 sum의 타입을 Long 에서 long으로 바꿔보자.

2305843005992468481 소요 시간: 0.616초

Long을 사용해도 오류, 경고 없이 컴파일 되지만, 박싱과 언박싱이 반복해서 발생하므로 체감될 정도로 성능이 느려진다.

🍑 결론

그렇다면 언제 써야하는가?

1. 컬렉션의 원소, 키, 값으로 사용하는 경우.

2. 리플렉션을 통해 메서드를 호출하는 경우.

기본 타입과 박싱된 기본 타입 중 하나를 사용해야 한다면 가능하면 기본 타입을 사용하라. 기본 타입은 간단하고 빠르다.

두 박싱된 기본 타입을 비교하는 과정에서 에러가 발생할 수도, 초기화 문제로 NPE가 발생할 수도, 기본 타입을 박싱하는 작업이 불필요한 객체를 생성하는 부작용을 나을 수도 있다.

3. Null 값의 표현이 필요한 경우.


Referenced by