Wrapper 클래스는 기본형 값을 객체로 Wrapping 하는 것이다.
자바에서는 8개의 기본형을 객체로 다루지 않은데 이것이 바로 자바가 완전한 객체지향 언어가 아니라는 얘기를 듣는 이유이다.
대신 기본형을 사용함 으로써 더 높은 성능을 얻을 수 있었다.
* 실제 기본형 타입과 double 과 객체형 타입인 Double을 각각의 배열에 만들어 넣는 성능 테스트를 돌렸을때
아래와 같은 결과를 얻을 수 있었다.
그렇다면 Wrapper class 를 언제 사용하고 왜 기본형을 감싸는가?
자바는 객체지향 언어이다 보니 매개 변수로 객체를 요구할 때, 기본형 값이 아닌 객체로 저장할때, 객체간의 비교가 필요할때 등등의 기본형이 아닌 데이터를 객체로서의 형태로 경우가 많다. 이 경우에 기본형 값들을 객체로 변환하여 필요한 작업을 수행한다.
wrapper 클래스의 기본 개념, 오토박싱 & 언박싱 (autoBoxing & unboxing) 이 있다.
기본형 값을 래퍼클래스의 객체로 자동 변환 하는것을 '오토박싱(autoboxing)'
반대로 기본형으로 변환 하는것을 '언박싱(unboxing)' 이라고 한다.
autoBoxing & unBoxing 예시
unBoxing(기본형 변환) | autoBoxing(wrapper 변환) |
int i = Integer.parseInt("100"); double d = Double.parseDouble("3.14") byte b = Byte.parseByte("100"); short b = Short.parseByte("100"); ... |
Integer i = Integer.ValueOf(100); double d = Double.ValueOf(3.14); byte b = Byte.ValueOf(100); short s = Short.ValueOf(100); ... |
기본형 | 래퍼클래스 | 생성자 | 활용예 |
boolean | Boolean | Boolean (boolean value) Boolean (String s) |
Boolean b = new Boolean(true) Boolean b2 = new Boolean("true") |
char | Character | Character(char value) | Character c = new Character('a') |
byte | Byte | Byte(byte value) Byte(String s) |
Byte b = new Byte(10); Byte b2 = new Byte("10"); |
short | Short | short(short value) short (String s) |
Short s = new Short(10); Short s = new Short("10"); |
int | Integer | Integer(int value) Integer(String s) |
Integer i = new Integer(100); Integer i2 = new Integer("100"); |
long | Long | Long(long value) Long(String s) |
Long l = new Long(100); Long l2 = new Long("100"); |
float | Float | Float (double value) Float (float value) Float (String s) |
Float f = new Float(1.0); Float f2 = new Float(1.0f); Float f3 = new Float("1.0f") |
double | Double | Double (double value) Double (String s) |
Double d = new Double(1.0); Double b2 = new Double("1.0"); |
equals() 와 = =
래퍼 클래스 들은 모두 equals() 가 오버라이딩 되어있어 주소 값이 아닌 객체가 가지고 있는 값을 비교한다.
기본형이 '=' 로 값을 비교하는 것과는 차이가 있다.
기본형과 참조형간의 연산
JDK 1.5 이전에는 기본형과 참조형 간의 연산이 가능했기 때문에, 래퍼 클래스로 기본형을 객체로 만들어서 연산해야 했다.
int i = 5;
Integer iObj = new Integer(7);
int sum = i + iObj; // 에러. 기본형과 참조형 간의 덧셈 불가
그러나 이제는 기본형과 참조형 간의 덧셈이 가능하다. 자바 언어의 규칙이 바뀌 것은 아니고, 컴파일러가 자동으로 변환하는 코드를 넣어주기 때문이다. 아래의 경우, 컴파일러가 Integer 객체를 int 타입의 값으로 변환해주는 intValue() 를 추가 해 준다.
JDK 1.5 부터 도입된 '오토박싱(autoboxing)' 기능 때문에 반환값이 기본형일 때와 래퍼 클래스 일 때의 차이가 없어졌다.
컴파일 전의 코드 | 컴파일 후의 코드 |
int i = 5; Integer iObj = new Integer(7); int sum = i + iObj; |
int i = 5; Integer iObj = new Integer(7); int sum = i + iObj.intValue(); |
Boxing 작업이 어떤 방식으로 일어나는지 코드로 확인해 보자
AutoBoxing
Integer 클래스를 기준으로 살펴 보았을때,
String 객체 클래스의 char 타입의 value 변수와 마찬가지로 Integer의 멤버 변수 value에 final이 붙어있다.
불변(immutable) 이다.
10 으로 auto boxing
바이트 코드로 valueOf 를 호출 하는 것을 알 수 있다.
return new 연산자로 새로운 객체를 생성하는것을 알 수 있다.
UnBoxing
integer.parseInt()
public static int parseInt(CharSequence s, int beginIndex, int endIndex, int radix)
throws NumberFormatException {
s = Objects.requireNonNull(s);
if (beginIndex < 0 || beginIndex > endIndex || endIndex > s.length()) {
throw new IndexOutOfBoundsException();
}
if (radix < Character.MIN_RADIX) {
throw new NumberFormatException("radix " + radix +
" less than Character.MIN_RADIX");
}
if (radix > Character.MAX_RADIX) {
throw new NumberFormatException("radix " + radix +
" greater than Character.MAX_RADIX");
}
boolean negative = false;
int i = beginIndex;
int limit = -Integer.MAX_VALUE;
if (i < endIndex) {
char firstChar = s.charAt(i);
if (firstChar < '0') { // Possible leading "+" or "-"
if (firstChar == '-') {
negative = true;
limit = Integer.MIN_VALUE;
} else if (firstChar != '+') {
throw NumberFormatException.forCharSequence(s, beginIndex,
endIndex, i);
}
i++;
if (i == endIndex) { // Cannot have lone "+" or "-"
throw NumberFormatException.forCharSequence(s, beginIndex,
endIndex, i);
}
}
int multmin = limit / radix;
int result = 0;
while (i < endIndex) {
// Accumulating negatively avoids surprises near MAX_VALUE
int digit = Character.digit(s.charAt(i), radix);
if (digit < 0 || result < multmin) {
throw NumberFormatException.forCharSequence(s, beginIndex,
endIndex, i);
}
result *= radix;
if (result < limit + digit) {
throw NumberFormatException.forCharSequence(s, beginIndex,
endIndex, i);
}
i++;
result -= digit;
}
return negative ? result : -result;
} else {
throw NumberFormatException.forInputString("");
}
}
1. format 이 안맞을시 발생하는 exception 을 가지고 있다.
2. charsequence 로 값을 받는다.
3. negative 양수 음수를 직접 확인한다.
Boxing Type + Primitive Type
byte code
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 5 L0
BIPUSH 10
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ASTORE 1
L1
LINENUMBER 6 L1
ALOAD 1
INVOKEVIRTUAL java/lang/Integer.intValue ()I
ICONST_1
IADD
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ASTORE 1
L2
LINENUMBER 7 L2
RETURN
L3
LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
LOCALVARIABLE integer Ljava/lang/Integer; L1 L3 1
MAXSTACK = 2
MAXLOCALS = 2
- INVOKESTATIC java/lang/Integer.valueOf, boxing 생성
- INVOKEVIRTUAL java/lang/Integer.intValue , unBoxing
- ICONST_1 , IADD = 1 상수를 더한다.
- INVOKESTATIC java/lang/Integer.valueOf, boxing 생성
박싱된 기본 타입보다는 기본 타입을 사용 해야 하는 이유
1. Wrapper class 사용시 NullPointer 가 발생 할수 있는 경우
public class NullPointerTest {
static Integer i;
public static void main(String[] args) {
if (i == 0) {
System.out.println("nullPointer");
}
}
}
- == 연산시 unboxing 발생
- 기본형에 대해서는 오류가 나타나지 않는다.
- 원인은 i 가 literal 값인 42와 비교하는 과정에서 i는 Auto UnBoxing을 수행한다.
하지만 i는 null이기 때문에 Auto UnBoxing을 수행하는 과정에서 NPE를 발생시키게 된다
2. 의도하지 않은 Auto Boxing으로 인한 성능저하
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
- 박싱과 언박싱이 반복해서 일어나 체감될 정도로 성능이 느려진다.
- 기본 타입의 값은 JVM내의 Stack 메모리에 저장된다.
참조 타입의 값은 객체 내의 상수에 저장된다. 따라서 JVM 내의 Heap 메모리에 저장된다.
박싱된 기본타입(Wrapper class) 는 언제 사용 해야 하나?
- 컬렉션의 원소, 키, 값으로 쓴다.
- 컬렉션은 기본타입을 담을 수 없으므로 어쩔 수 없이 박싱된 기본타입을 사용해야 한다.
- 제네릭(Generics) 타입을 이용하는 경우에도 박싱된 기본타입을 사용한다.
- 제네릭 타입에서는 int, double과 같은 기본타입을 지원하지 않기 때문이다.
- 리플렉션(Reflection)을 통해 메서드를 호출할 때에도 박싱된 기본타입을 사용한
Reference
자바의 정석
https://studymake.tistory.com/399