Java Boxing, Unboxing, AutoBoxing
시작하기 전에,
불필요한 AutoBoxing을 줄이고,누적되는 연산이면 primitive Type을 이용한 연산을 하자
Long, Integer보다 primitive 타입을 쓰는 쪽이 훨씬 빠르다
자바에는 자료형이 2가지가 있다.
기본 타입(Primitive Type)과 레퍼런스 타입(Reference Type)
두 타입의 차이점은 이렇다
타입 | Primitive Type(기본 타입) | Reference Type(참조형 타입 ) |
---|---|---|
설명 | 원시 타입, 기본형 타입. 변수의 주소 값에 값이 그대로 저장되는 데이터 타입 | 참조형 타입. Primitive Type을 제외한 타입들을 이야기 한다. |
저장공간 | JVM Runtime Data Area중 Stack |
JVM Runtime Data Area중 Heap (실제 객체는 힙 영역에 저장되며 참조 타입 변수(인스턴스)는 스택 영역에 있는 실제 객체들의 주소를 저장하여, 객체를 사용할 때 마다 참조변수(인스턴스)에 저장된 객체의 주소를 불러와 사용한다) |
Null 저장 지원 여부 | X (Null 저장 불가 ) | O (Null 저장 가능) |
저장 타입 | 실제 값 | 주소 값 |
종류 | boolean, char, byte, short, int, long, float, double | class, interface, array, enum, string |
- 이외 다른 정보는 여기 에서 보면 된다.
Reference Type 중, 기본 타입 (primitive)의 데이터를 객체로 취급해야 할 때 도와주는 클래스 들이 있는데
그 클래스들을 Wrapper Class (래퍼 클래스)
라고 한다.
래퍼 클래스는 각각의 타입에 해당하는 데이터를 인수로 전달받아, 해당 값을 가지는 객체로 만들어 준다.
이러한 래퍼 클래스는 모두 java.lang 패키지에 포함되어 제공된다.
Wrapper Class와 기본 타입에 대응되는 클래스들
기본형 | 포장 클래스 | 생성 예 |
---|---|---|
boolean | Boolean | Boolean booleanObj = new Boolean(true); Boolean booleanObj = new Boolean(“false”); |
char | Character | Character charObj = new Character(‘a’); |
byte | Byte | Byte charObj = new Byte(10); Byte charObj = new Byte(“127”); |
short | Short | Short shortObj = new Short(1234); Short shortObj = new Short(“1234”); |
int | Integer | Integer intergerObj = new Integer(1234); Integer intergerObj = new Integer(“1234”); |
long | Long | Long longObj = new Long(1234); Long longObj = new Long(“1234”); |
float | Float | Float floatObj = new Float(12.34f); Float floatObj = new Float(“12.34f”); |
double | Double | Double doubleObj = new Double(12.34); Double doubleObj = new Double(“12.34”); |
래퍼 클래스 중에서 Integer 클래스와 Character 클래스만이 자신의 기본 타입과 이름이 다르다
박싱(Boxing)과 언박싱(UnBoxing)
박싱(Boxing)과 언박싱(UnBoxing)
래퍼 클래스(Wrapper class)는 산술 연산을 위해 정의된 클래스가 아니므로, 인스턴스에 저장된 값을 변경할 수 없다. - 불변
단지, 값을 참조하기 위해 새로운 인스턴스를 생성하고, 생성된 인스턴스의 값만을 참조할 수 있습니다.
위의 그림과 같이 기본 타입의 데이터를 래퍼 클래스의 인스턴스로 변환하는 과정을 박싱(Boxing)이라고 한다.
반면 래퍼 클래스의 인스턴스에 저장된 값을 다시 기본 타입의 데이터로 꺼내는 과정을 언박싱(UnBoxing)이라고 한다
오토 박싱(AutoBoxing)과 오토 언박싱(AutoUnBoxing)
JDK 1.5부터는 박싱과 언박싱이 필요한 상황에서 자바 컴파일러가 이를 자동으로 처리해 준다.
이렇게 자동화된 박싱과 언박싱을 오토 박싱(AutoBoxing)과 오토 언박싱(AutoUnBoxing)이라고 부른다.
다음 예제는 박싱과 언박싱, 오토 박싱과 오토 언박싱의 차이를 보여주는 예제이다.
예제
Integer num = new Integer(17); // 박싱
int n = num.intValue(); // 언박싱
System.out.println(n);
Character ch = 'X'; // Character ch = new Character('X'); : 오토박싱
char c = ch; // char c = ch.charValue(); : 오토언박싱
System.out.println(c);
오토 박싱을 이용하면 new 키워드를 사용하지 않고도 자동으로 Character 인스턴스를 생성할 수 있다.
그러나, 래퍼 클래스의 비교는 조심해야 한다 오토 박싱은 좋은 것만이 아니다
.
명시적 박싱, 언박싱 / 묵시적 박싱 언박싱
묵시적 박싱을 오토박싱(AutoBoxing) 이라고도 한다
묵시적 박싱(오토박싱)은 컴파일러가 도와줘서 자동적으로 진행된다.
명시적 박싱
: 프로그래머가 코딩하여 명시적으로 wrapper로 변환하는 것-
int intValue = 99; Integer integerObj = (Integer)intValue;
-
명시적 언박싱
: 프로그래머가 코딩하여 명시적으로 primitive로 변환하는 것-
Integer integerObj = 99; int intValue = (int)integerObj;
-
묵시적 박싱
: 프로그래머가 임의로 박싱을 해주는 것이 아니라 자동으로 박싱이 되는 것을 말한다.-
int intValue = 99; Integer integerObj = intValue;
-
묵시적인 언박싱
: 프로그래머가 임의로 언박싱을 하는 것이 아니라 자동으로 언박싱이 되는 현상을 말한다.-
Integer integerObj = 99; int intValue = integerObj;
-
래퍼 클래스의 비교 (equals, == ) ?
래퍼 클래스의 비교 연산도 오토언박싱을 통해 가능해지지만, 인스턴스에 저장된 값의 동등 여부 판단은동등 연산자(==)를 사용해서는 안 되며, equals() 메소드를 사용해야만 한다.
왜?
래퍼 클래스도 객체이므로 동등 연산자(==)를 사용하게 되면, 두 인스턴스의 값을 비교하는 것이 아니라 두 인스턴스의 주소값을 비교
하게 되기 때문이다.
따라서 서로 다른 두 인스턴스를 동등 연산자(==)로 비교하게 되면, 언제나 false 값을 반환되게 된다
오토 박싱 / 언박싱(Auto Boxing / UnBoxing)의 주의할점
오토박싱(Autoboxing)은 Java 컴파일러가 원시 타입(Primitive types)과 해당 객체 래퍼 클래스 간에 수행하는 자동 변환을 말한다.
매우 편리한 기능이지만, 남용하게 되면 성능 저하를 야기할 수 있다.
오토박싱을 반복문이나 스트림에서 남용하게 되면 성능이 낮아지게 된다. 왜?
- 컴파일러가 자동으로 코드를 넣어주지만 불필요한 객체를 생성한다.
- primitive 타입과 reference 타입의 저장 공간은 다르고 Wrapper 클래스는 불변이며, primitive를 참조한다.
1. 컴파일러가 자동으로 코드를 넣어주지만 불필요한 객체를 생성한다
다음과 같이 0부터 Integer.MAX_VALUE 까지 sum 하는 코드가 있다.
Long sum = 0L;
for(long i=0;i< Integer.MAX_VALUE;i++){
sum += i;
}
이코드는 사실 컴파일러에 의해 다음처럼 변환되어 실행된다
Long sum = Long.valueOf(0L);
for(long i = 0L; i < 0x7fffffffL; i++)
sum = Long.valueOf(sum.longValue() + i);
즉, 불필요한 객체가 생성되고 계속 메소드가 호출되는 것이다.
위의 코드를 실행하면 변수 sum을 Long으로 선언해서 불필요한 Long 객체가
2^31
개나 만들어 질것이다
요즘의 JVM에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일이 크게 부담이 아니다.
프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것은 일반적으로 좋은 일이다.
그러나,불필요하게 객체를 생성할 필요는 없다.
이 생성 파괴가 계속 반복된다면, 그 비용은 적더라도 쌓이게 된다면 만만치 않을것이다.
2. primitive 타입과 reference 타입의 저장 공간은 다르고 Wrapper 클래스는 불변이며, primitive를 참조한다.
- 자바에서 primitive 타입은 JVM의 Stack에 저장되고 reference 타입은 JVM의 Heap에 저장된다
- Wrapper 클래스들은 reference 타입이며 내부적으로 각자들의 primitive 타입인 int, double, long 을 가지고 있다.
- 즉, heap에 객체가 생성되며, 그 객체 내부에서 stack에 있는 primitive type들을 참조하고 있는 것이다.
- 자바에서 Wrapper class 에 해당하는 Integer, Character, Byte, Boolean, Long, Double, Float, Short 클래스는 모두 Immutable 이다.
- 그래서 heap 에 있는 같은 오브젝트를 레퍼런스 하고 있는 경우라도, 새로운 연산이 적용되는 순간 새로운 오브젝트가 heap 에 새롭게 할당된다.
- Integer 클래스를 까보면 내부에서 사용하는 실제 값인 private final int value 라는 변수가 있다, 즉, 생성자에 의해 생성되는 순간에만 초기화되고 변경불가능한 값이 된다.
- 이것 때문에 Wrapper class 들도 String 처럼 Immutable 한 오브젝트가 된다.
public final class Double extends Number
implements Comparable<Double>, Constable, ConstantDesc {
private final double value;
@Deprecated(since="9", forRemoval = true)
public Double(double value) {
this.value = value;
}
public static Double valueOf(String s) throws NumberFormatException {
return new Double(parseDouble(s));
}
}
// Integer
public final class Integer extends Number
implements Comparable<Integer>, Constable, ConstantDesc {
/**
* The value of the {@code Integer}.
*
* @serial
*/
private final int value;
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
}
또한, Heap 영역에 저장되고, , primitive Type을 가져오려고할 때도 Wrpper class로 인해 메모리를 추가적으로 탐색 해야하는 과정이 필요하다.
primitive타입과 wrapper 클래스 타입의 조회하는 방법을 살펴보면,
- primitive Type = Stack에서 바로 조회
- boxing된 reference Type = Stack에서 조회 + Heap 영역까지 조회
위와 같은 이유로 원시타입(primitive Type)이 성능상 이점을 가져가게된다.
또한, 차지하는 메모리의 양도 일반 primitive 타입보다 reference 타입이 훨씬 많다.
또한 오라클 자바 문서에서도 다음과 같이 설명한다.
// List adapter for primitive int array
public static List<Integer> asList(final int[] a) {
return new AbstractList<Integer>() {
public Integer get(int i) { return a[i]; }
// Throws NullPointerException if val == null
public Integer set(int i, Integer val) {
Integer oldVal = a[i]; // 박싱! -> primitiveType a[i]가 박싱된다
a[i] = val; // 언박싱! -> Integer val이 언박싱된다
return oldVal;
}
public int size() { return a.length; }
};
}
위 코드의 퍼포먼스는 썩 좋지 않습니다.
모든get
/set
작업에서 박싱과 언박싱이 일어나고 있기 때문에 성능이 열악할 수 있습니다..
가끔 중요하지 않은 부분에 쓰기에는 충분한 속도이겠지만, 퍼포먼스가 중요한 루프에서 이런 방식을 쓰는 건 멍청한 일입니다.그렇다면 도대체 언제 오토박싱과 언박싱을 사용해야 할까요?
레퍼런스 타입과 기본 타입 사이의 "임피던스 불일치"가 있는 경우에만 사용하세요(기본 타입을 쓸 수 없는 경우에만 쓰세요).
예를 들어Map
이나Set
같은 Java Collection에는 기본 타입을 못 넣으니까 이런 경우에는 레퍼런스 타입을 쓰면 됩니다.
하지만 과학 계산이나, 성능에 민감한 계산 코드에 오토박싱/언박싱을 사용하는 건 적절하지 않습니다.Integer
는int
를 완벽히 대체할 수 없습니다.
오토박싱과 언박싱은 기본 타입과 레퍼런스 타입 사이의 구분을 흐릿하게 만들어주지만, 그 차이를 완벽히 없애는 것은 아닙니다.
즉 공식 문서에서도, 잦은 연산에는 AutoBoxing은 주의하라고 나와있다.
결론
- 오토박싱으로 인한 속도 지연이 문제가 되는 상황이라면 primitive 타입으로 바꾸는 것을 검토해 볼 것.
- 박싱/ 언박싱 과정에서의 캐스팅 여부 확인 (불확실성) 및 지연 시간, 객체 생성으로 인한 메모리 생성과 복사 시간, 가비지의 발생 등, 많은 비용이 드므로, 가급적 primitive 사용으로 박싱/ 언박싱을 피하는 것이 좋다.
- 스트림(Stream) 처리에서도 주의해야 한다.
- 스트림 병렬처리의 성능이 낮게 나오는 이유 중 하나는, 기본형(원시 타입) 특화되지 않은 스트림을 초래할 때 수반되는 오토박싱, 언방식 등의 오버헤드를 수반하기 때문이다.
참조
'Java > Java' 카테고리의 다른 글
Java - 객체 비교에는 Objects.equals()를 사용하자. (0) | 2022.11.19 |
---|---|
Java - 동일성과 동등성 ( ==, equals() ) (0) | 2022.11.19 |
Java 람다표현식의 특징과 Scope, effectively final variable (0) | 2022.10.23 |
Java String Builder와 StringBuffer의 차이점 (0) | 2022.10.21 |
Java String Pool (String Constant Pool) + String.intern() (0) | 2022.10.21 |