본문 바로가기
Java/Effective Java

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

by 쁘니쁘나 2022. 7. 3.

 

자바의 데이터 타입은 int, double, boolean 같은 기본타입과 이에 대응하는 Integer, Double, Boolean 같은 박싱된 기본 타입이 있다.

 

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

 

기본타입과 박싱된 기본타입의 차이

1. 박싱된 기본 타입은 값 뿐만아니라 식별성 이라는 속성도 갖는다.

2. 박싱된 기본 타입은 유효하지 않은 값, 즉 null을 가질 수 있다.

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

 

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

 

Integer 값을 오름차순으로 정렬하는 예시를 보자.

(첫 번째 원소가 두 번째 원소보다 작으면 음수, 같으면 0, 크면 양수를 반환한다.)

 

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

➡️ 별다른 문제없이 테스트가 잘 통과한다.

 

위의 예제에서 심각한 결함은??

아래 값을 출력해 보면 알 수 있다.

 

naturalOrder.compare(new Integer(42), new Integer(42)) // ➡️ 1

➡️ 두 인스턴스의 값이 42로 같으므로 0 을 출력해야 할 것 같지만, 실제로는 1 을 출력한다. 즉, 첫번째 Integer 가 두 번째보다 크다고 나온다.

 

왜 이런 결과가 나올까??

 

naturalOrder 의 첫 번째 검사 (i < j)를 보자.

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

 

🐣 오토박싱과 오토 언박싱
오토박싱 : 컴파일러에 의해 기본 타입이 Wrapper 클래스 (박싱된 기본타입) 로 자동 변환 되는것.
오토 언박싱 : 컬렉션에서 값을 가져와 기본타입에 넣을때 자동으로 기본 타입으로 형변환되어 기본 값에 저장하는 것.

 

하지만 두 번째 검사 (i == j)에서는 두 객체 참조의 식별성을 검사하게 된다.

i 와 j 가 서로 다른 Integer 인스턴스라면 결과는 false가 되고, 비교자는 1 을 반환한다.

 

이처럼 같은 객체를 비교하는게 아니라면 박싱된 기본 타입에 == 연산자를 사용하면 오류가 일어난다.

 

이 문제를 해결하려면??

 

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

➡️ 지역 변수 2개를 두어 각각 박싱된 Integer 매개변수의 값을 기본 타입 정수로 저장한 다음, 모든 비교를 이 기본 타입 변수로 수행해야 한다. 이렇게 하면 오류의 원인인 식별성 검사가 이뤄지지 않는다.

 

위의 예제로 식별성검사를 피하는 방법을 알아봤다. 하지만 이번엔 NullPointerException 을 발생시키는 예제를 알아보자.

 

NullPointerException 이 발생하는 예제

public class Unbelievable {
	static Integer i;
	
	public static void main(String [] args) {
	
		if (i == 42)
		System.out.println("Oh my god");

	}

}

➡️ Oh my god 도 출력되지 않을 뿐더러 i == 42 를 검사할 때 NullPointerException 을 던진다!

 

위의 결과가 나온이유는 i 가 int가 아닌 Integer 이며 다른 참조 타입 필드와 마찬가지로 i 의 초기값이 null 이기 때문이다.

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

 

위의 예제를 통해 알 수 있듯이 기본 타입과 박싱된 기본 타입을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 풀린다.

 

해결법은 i를 int 로 선언해주면 된다.

 

실수로 지역변수 sum 을 박싱된 기본 타입으로 선언하여 느려진 예제

public static void main(String [) args) {
	Long sum = 0L;

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

	System.out.println(sum);
}

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

그럼 박싱된 기본 타입은 언제 써야 하나?

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

 

 

핵심정리

  • 기본 타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 기본 타입을 사용하라. 간단하고 빠르다.
  • 박싱된 두개의 기본 타입을 == 연산자로 비교한다면 식별성 비교가 이뤄지는데 우리가 원한 결과가 아닐 가능성이 크다.
  • 같은 연산에서 기본타입과 박싱된 기본타입을 혼용하면 언박싱이 이뤄지며, NullPointerException 을 던질 수 있다.
  • 기본타입을 박싱하는 작업은 필요없는 객체를 생성하는 부작용을 나을 수 있다.

 

 

 

 

 

참고


도서 - Effective Java Item61

댓글