개요
박싱된 기본 타입과 기본타입의 차이를 알아보고, 왜 후자의 사용을 지향하는지 알아보자.
기본 타입이란?
자바에서의 타입은 '데이터 타입'을 말한다. 이는 해당 데이터가 메모리에 어떻게 저장되고, 어떻게 처리되어야 하는지를 명시적으로 알려주는 역할을 한다.
자바에서는 여러 형태의 '타입'을 미리 정의하여 제공하는데, 이것을 자바의 기본타입(==Primitive type) 이라고 한다. 참고로 기본 타입 외에도 String, List 와 같은 참조 타입이 있다.
기본 타입은 8 종류가 있으며, 크게는 정수형, 실수형, 문자형, 논리형 타입으로 나뉜다.
정수형 타입 (4종류)
정수란 부호를 가지고 있으며, 소수 부분이 없는 수를 의미한다. int, long, short, byte 타입이 있다.
정수형 타입 | 메모리의 크기 | 데이터의 표현 범위 |
byte | 1바이트 | -128 .. 127 |
short | 2바이트 | -32,768 ~ 32,767 |
int | 4바이트 | -2,147,483,648 ~ 2,147,483,647 |
long | 8바이트 | -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 |
실수형 타입 (2종류)
실수란 소수부나 지수부가 있는 수를 가리키며, 정수보다 훨씬 더 넓은 표현 범위를 가진다. float, double 타입이 있다.
실수형 타입 | 메모리의 크기 | 데이터의 표현 범위 |
float | 4바이트 | 3.4 * 10의 -38승 ... 3.4 * 10의 38승 |
double | 8바이트 | 1.7 * 10의 -308승 ... 1.7 * 10의 308승 |
문자형 타입 (1종류)
컴퓨터는 2진수밖에 인식하지 못하므로 어떤 문자를 어떤 숫자에 대응시키는 약속을 했다. 대표적으로 C언어는 아스키 코드를 사용해 문자를 표현한다. 아스키 코드는 영문 대소문자 및 몇가지 기호를 사용하는 7비트 문자 인코딩 방식이다. 문자 하나를 7비트로 표현하므로 총 128개의 문자를 표현할 수 있다.
자바에서는 유니코드를 사용하여 문자를 표현한다. 문자 하나를 16비트로 표현하므로, 총 65,536 개의 문자를 표현할 수 있기에 각 나라의 모든 언어를 표현할 수 있다.
문자형 타입 | 메모리의 크기 | 데이터의 표현 범위 |
char | 2 바이트 | 0 ... 65,536 |
논리형 타입 (1종류)
논리형은 참이나 거짓 중 한 가지 값만을 가질 수 있는 타입을 의미한다. boolean 타입이 있다.
boolean 형의 기본값은 false 이며, 1 바이트의 크기를 가진다.
논리형 타입 | 메모리의 크기 | 데이터의 표현 범위 |
boolean | 1바이트 | true 또는 false |
박싱된 기본 타입이란?
자바에서는 앞서 말한 기본 타입에 대응하는 참조 타입이 하나씩 있는데, 이를 박싱된 기본타입이라고 한다.
예를들어 int, double, boolean 에 대응하는 박싱된 기본 타입은 Integer, Double, Boolean이다.
기본 타입과 박싱된 기본 타입의 차이
첫째, 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 식별성이란 속성을 갖는다.
박싱된 기본 타입의 두 인스턴스는 값이 같아도 서로 다르다고 식별될 수 있다. 메모리의 주소가 다르기 때문이다.
둘째, 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 null을 가질 수 있다.
null 에 대한 체크가 이루어지지 않는다면 NPE가 발생할 수 있다.
셋째, 기본 타입이 박싱된 기본 타입보다 처리 시간, 메모리 사용면에서 효율적이다.
박싱된 기본 타입이 메모리를 더 잡아먹기 때문이다.
이 세가지를 무시하고 생각없이 사용했다가는 문제가 발생할 수 있다. 이를 무시했을 때 발생하는 문제들을 예제로 알아보자.
예제 1. 오름차순 정렬
Comparator<Integer> naturalOrder = (i, j) -> (i<j) ? -1 : (i == j ? 0 : 1);
System.out.println(naturalOrder.compare(new Integer(1),new Integer(3)));
함수형 인터페이스인 Comparator 의 구현체를 람다식으로 구현한 후, 호출하는 예제이다. i 보다 j 가 클 경우 -1, 같을경우 0, i 가 클 경우 1을 리턴한다. naturalOrder.compare 메서드의 매개변수로 값을 넣어 테스트했을 때는 문제는 발생하지 않는 것처럼 보인다. new Integer(1), new Integer(3)을 넣었을 땐 -1이, 반대로 넣었을땐 1이 리턴되기 때문이다. 그렇다면 위 코드의 문제가 뭘까?
바로 같은 값에 대해서는 0을 리턴하지 않는 점이다.
첫번째 검사 (i<j) 는 잘 동작한다. i, j가 참조하는 오토박싱된 Integer 인스턴스는 비교를 위해 기본 타입 값으로 변환된 후 비교 연산을 한다. 그런데 두 번째 검사인 (i == j) 에서는 '객체 참조'의 식별성을 검사하게 된다. i와 j 가 같은 값을 갖고있다고 할지라도 인스턴스가 다르기에 false 가 출력되고 결국 1을 반환한다. 즉, 박싱된 기본 타입에 == 연산자를 사용하게 될 경우 문제가 발생하는 것이다.
예제 2. 기이하게 동작하는 프로그램 예제
public class UseCase {
static Integer i;
public static void main(String[] args) {
if (i == 42)
System.out.println("믿을 수 없군!");
}
}
이 프로그램의 실행 결과는 어떨까?
이 프로그램의 결과는 "믿을 수 없군!"을 출력하지 않지만, 그 전에 기이한 결과를 보여준다. i == 42를 검사할 때 NullPointerException을 던지는 것이다. 원인은 (i == 42) 코드에서 null을 참조하고 있는 참조 타입 i와 42라는 기본 타입을 비교하기 위해 i를 언박싱하게 되는데, 이때 null 참조를 언박싱하므로 NPE 예외가 발생한다. 기본 타입과 박싱된 타입을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 언박싱되기 때문이다.
예제 3.느린 반복문 예제
Long sum = 0L;
for(long i = 0; i <= Integer.MAX_VALUE; i++){
sum += 1;
}
이 프로그램은 지역변수 sum을 박싱된 기본 타입으로 선언하여 느려졌다. sum += 1 부분에서 언박싱과 박싱이 반복해서 일어나기 때문이다. 체감될 정도로 성능이 느려진다.
박싱된 기본 타입은 언제 쓰는게 좋을까?
그렇다면 박싱된 기본 타입은 아래와 같은 상황에서 써야한다.
첫째, 컬렉션의 원소, 키, 값으로 쓴다. 컬렉션은 기본 타입을 담을 수 없기때문이다.
둘째, 타입 매개변수로 사용한다. 타입 매개변수로 기본 타입을 지원하지 않기 때문이다.
셋째, 리플렉션을 통해 메서드를 호출할 때도 박싱된 기본 타입을 사용해야 한다.
정리
기본 타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 기본 타입을 사용하자. 간단하고 빠르며, 앞서 말했던 언박싱으로 인한 버그, 성능 이슈를 예방할 수 있기 때문이다. 또한 기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용을 나을 수 있다. 박싱된 기본 타입을 꼭 사용해야한다면 위와 같은 문제 상황들을 이해하고 적절히 사용해야 한다.
'공부 > Effective Java' 카테고리의 다른 글
[Effective Java] Item 69. 예외는 진짜 예외 상황에만 사용하라 (0) | 2024.06.20 |
---|---|
[Effective Java] Item 66. 네이티브 메서드는 신중히 사용하라 (2) | 2024.06.19 |
[Effective Java] Item 59. 라이브러리를 익히고 사용하라 (0) | 2024.05.23 |
[Effective Java] Item 57. 지역변수의 범위를 최소화하라 (0) | 2024.05.07 |
[Effective Java] Item 55. 옵셔널 반환은 신중히 하라 (0) | 2024.04.18 |