이 글에서는 java의 primitive wrapper class에 대해 알아본다.
Primitive Wrapper Class
Primitive Wrapper Class는 primitive type을 object로 다뤄야 할 때 사용한다. 대표적으로 collection이나 generic에 들어가야 하는 값은 모두 object여야 하는데 primitive type의 collection이 필요할 수도 있는데 이 때 primitive wrapper class를 사용한다.
참고로 primitive type는 byte, char, short, int, long, float, double, boolean 이렇게 8종류밖에 없으며 다음과 같이 wrapping한다.
- byte - Byte
- char - Character
- short - Short
- int - Integer
- long - Long
- float - Float
- double - Double
- boolean - Boolean
boxing, unboxing
primitive type을 boxing해 primitive wrapper class를 만들고, primitive wrapper class를 unboxing해 primitive type 값을 가져올 수 있다. 그렇지만 명시적으로 boxing과 unboxing하지 않아도 되고, primitive type을 쓰는 것과 동일하게 쓸 수 있다.
또한 primitive wrapper class는 immutable이기 때문에 arithmetic operation이 일어날 때 implicitly unboxing과 boxing이 이루어진다. 따라서 많은 데이터들을 처리할 때는 boxing과 unboxing 과정으로 인해 성능이 떨어질 수 있으므로 유의해야 한다.
public class App {
public static void main(String[] args) throws Exception {
Integer number = new Integer(10); // boxing: deprecated
Integer number2 = 10; // auto boxing
int n = number.intValue(); // unboxing
System.out.println(n);
int n2 = number2; // auto unboxing
System.out.println(n2);
}
}
Primitive Wrapper Class의 특징
- immutable이다.
- primitive type의 경우 null이 될 수 없지만 wrapper class는 class인 만큼 null이 허용된다.
- wrapper class의 비교는 ==가 아니라 equals() method를 사용해야 한다.
- type에 따라 constant pool에 몇 개의 값을 caching해 둔다.
- explicitly boxing하는 경우 새로운 object를 생성한다.
Java Data Types 포스팅에서 String이 immutable이었기 때문에 reference type이지만 primitive type처럼 작동했던 것처럼, Primitive Wrapper Class도 이와 유사하게 굴러간다. 다음 예시를 보자.
public class App {
private static void process(Integer integer){
System.out.println("----- in function -----");
System.out.println("before processing : " + integer); // 1
integer = 10;
System.out.println("after processing : " + integer); // 10
System.out.println("----- leaving function -----");
}
public static void main(String[] args) throws Exception {
Integer someInteger = 1;
System.out.println("value before function call : " + someInteger); // 1
process(someInteger);
System.out.println("value after function call : " + someInteger); // 1
}
}
someInteger를 parameter로 줬다. 이 때 Integer는 class이니 reference type이 parameter로 들어갔다. 그러나 process method에서 integer를 10으로 바꾼다. process 내부에서는 값이 바뀌어 있으나 process 밖으로 나오면 다시 원래 값인 1로 바뀐다. 즉 새로운 값을 primitive wrapper class에 넣으면 원래 memory에 있는 값이 바뀐 게 아니라, 새로운 address를 참조한다는 뜻이다.
String과 마찬가지로 이는 primitive wrapper class가 immutable이기 때문이다. 아래는 int의 wrapper class인 Integer class의 내부 코드이다.
// Failed to get sources. Instead, stub sources have been generated by the disassembler.
// Implementation of methods is unavailable.
package java.lang;
public final class Integer extends java.lang.Number implements java.lang.Comparable {
// ...
private final int value;
// ...
}
값이 저장되는 숫자 값에 final로 선언되어 있다. final로 선언된 attribute는 다시 새로운 값을 할당받을 수 없기 때문에, ref = n와 같이 다른 값을 넣으면 Integer class 내부의 value를 바꾸는 것이 아니라 boxing된 n이 ref에 들어가는 것이다.
그러나 재미있는 사례가 있다. 아래 예시를 보자.
public class App {
public static void main(String[] args) throws Exception {
Integer someInteger = 1;
Integer ref = 1;
Integer otherInteger = new Integer(1);
System.out.println(someInteger == ref); // true : same address
System.out.println(someInteger.equals(ref)); // true
System.out.println(someInteger.intValue() == otherInteger.intValue()); // true
System.out.println(someInteger == otherInteger); // false : different address;
System.out.println(someInteger.equals(otherInteger)); // true
someInteger = 128;
ref = 128;
System.out.println(someInteger == ref); // false : different address
System.out.println(someInteger.equals(ref)); // true : same value
}
}
포인트는 2개이다.
첫째. someInteger와 ref를 각각 1로 저장했을 때는 address 값이 같게 나오지만 128로 저장했을 때는 다른 address를 가리킨다. 127까지는 true가 나온다! 그 이유는 아래에 기술하듯 Integer 자료형은 -128부터 127까지의 값을 constant pool에 미리 caching해 두고 해당 값을 가져오는데, 127까지는 constant pool에 있는 값을 가져오므로 같은 address이지만 128부터는 constant pool에 없어 새로운 object를 할당하기 때문에 address가 달라지기 때문이다.
둘째. otherInteger를 explicitly new Integer()로 선언하면 constant pool을 참조하지 않고 새로운 object가 선언되기 때문에 someInteger와 다른 address를 가진다.
이렇듯 값의 범위와 선언 방법에 따라 object address 값이 달라지기 때문에 primitive wrapper class의 비교는 꼭 equals() method를 사용해야 한다!
앞선 포스팅에서 기술했듯 ==는 참조 비교(object address 비교), equals는 내용 비교(logical equality)이다.
정확하게는
Boolean의 경우 true, false
Character의 경우 ASCII code인 \u0000 ~ \u007f
Byte, Short, Int, Long은 -128부터 127까지
constant pool에 caching되어 있다.
immutable의 중요성
immutable object는 어떠한 경우에도 값이 변하지 않기 때문에 multi-thread 환경에서 사용하기 좋으며 값 변경을 대비한 복사본이 필요 없다. 그렇지만 immutable object의 값이 바뀌는 경우 새 object를 할당해야 한다는 overhead가 있고, 즉슨 더 많은 garbage(사용하지 않는 object)가 발생한다는 것이다.
따라서 object를 read하는지, write하는지, 또는 둘 다 사용하는지에 따라 immutable object를 사용할지 mutable object를 사용할지 결정해야만 한다. (그러나 대부분의 경우 정확성이 더 중요하기 때문에 많은 책들이 immutable object를 사용할 것을 권장한다.)
'Development > Java' 카테고리의 다른 글
[Java] Multi Thread (0) | 2023.03.04 |
---|---|
[Java] String vs StringBuffer vs StringBuilder (0) | 2023.03.04 |
[Java] Generic (0) | 2023.03.02 |
[Java] Java Virtual Machine (0) | 2023.03.01 |
[Java] Exception Handling (0) | 2023.03.01 |