본문으로 건너뛰기

9.3 래퍼 클래스 (Wrapper Classes)

자바에는 객체가 아닌 기본 자료형(Primitive Type: int, char, double, boolean 등) 8가지가 존재합니다. 가볍고 빠르기 때문에 많이 쓰이지만, 객체지향 프로그래밍을 하다 보면 이 단순한 숫자나 문자도 마치 객체(Object)처럼 다루어야 할 때 가 반드시 옵니다.

예를 들어 ArrayList<int>는 불가능하지만 ArrayList<Integer>는 가능합니다. 이럴 때 기본형 변수를 객체의 포장지로 감싸주는 클래스가 바로 래퍼(Wrapper) 클래스 입니다.

1. 8가지 래퍼 클래스

기본형과 래퍼 클래스의 매핑은 다음과 같습니다. 대부분 첫 글자를 대문자로 바꾸면 되지만 intchar는 이름이 다릅니다.

기본형래퍼 클래스크기범위
byteByte1 byte-128 ~ 127
shortShort2 bytes-32,768 ~ 32,767
intInteger4 bytes-2,147,483,648 ~ 2,147,483,647
longLong8 bytes-9.2 * 10^18 ~ 9.2 * 10^18
floatFloat4 bytes약 ±3.4 * 10^38
doubleDouble8 bytes약 ±1.8 * 10^308
charCharacter2 bytes0 ~ 65,535 (유니코드)
booleanBoolean1 bittrue / false

2. 오토박싱(Autoboxing)과 언박싱(Unboxing)

JDK 5부터는 기본형 ↔ 래퍼 객체 변환을 자바 컴파일러가 자동으로 처리합니다.

  • 박싱(Boxing): 기본형 → 래퍼 객체 (포장)
  • 언박싱(Unboxing): 래퍼 객체 → 기본형 (포장 제거)
  • 오토박싱/오토언박싱: 위 두 과정을 컴파일러가 자동으로 처리
public class AutoboxingDemo {
public static void main(String[] args) {
// === 수동 박싱 (Java 9 이후 deprecated) ===
// Integer deprecated = new Integer(10); // 비권장

// === 오토박싱: 기본형 → 래퍼 객체 (자동) ===
Integer num1 = 42; // int → Integer 자동 변환
Double d1 = 3.14; // double → Double 자동 변환
Boolean b1 = true; // boolean → Boolean 자동 변환
Character c1 = 'A'; // char → Character 자동 변환

// === 오토언박싱: 래퍼 객체 → 기본형 (자동) ===
int i1 = num1; // Integer → int 자동 변환
double dval = d1; // Double → double 자동 변환

// === 혼용 연산 (오토언박싱 후 연산) ===
Integer a = 10;
Integer b = 20;
int sum = a + b; // 내부적으로: a.intValue() + b.intValue()
System.out.println("합계: " + sum); // 30

// Integer와 int를 자연스럽게 섞어서 연산
Integer counter = 0;
for (int i = 0; i < 5; i++) {
counter++; // 오토언박싱 → 증가 → 오토박싱
}
System.out.println("카운터: " + counter); // 5
}
}
오토박싱 성능 주의

반복문 안에서 Integer 등 래퍼 타입을 사용하면 오토박싱/언박싱이 반복되어 성능이 저하됩니다. 성능이 중요한 반복문에서는 기본형 int를 사용하세요.

public class AutoboxingPerformance {
public static void main(String[] args) {
int N = 10_000_000;

// 나쁜 예: 반복문 안에서 Integer 사용 (오토박싱 N번 발생!)
long start1 = System.currentTimeMillis();
Integer sum1 = 0;
for (int i = 0; i < N; i++) {
sum1 += i; // sum1 = sum1 + i → 언박싱 + 덧셈 + 오토박싱
}
long time1 = System.currentTimeMillis() - start1;

// 좋은 예: int 사용 (오토박싱 없음)
long start2 = System.currentTimeMillis();
int sum2 = 0;
for (int i = 0; i < N; i++) {
sum2 += i;
}
long time2 = System.currentTimeMillis() - start2;

System.out.println("Integer 사용: " + time1 + "ms, 결과: " + sum1);
System.out.println("int 사용: " + time2 + "ms, 결과: " + sum2);
// Integer가 훨씬 느립니다 (약 5~10배 차이)
}
}

3. Integer Cache: == 비교 함정

Integer 등 래퍼 클래스는 -128 ~ 127 범위의 값은 객체를 캐싱합니다. 이 범위에서는 ==true이지만, 범위를 벗어나면 false가 됩니다.

public class IntegerCacheDemo {
public static void main(String[] args) {
// -128 ~ 127 범위: 캐시된 객체 재사용
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true (같은 캐시 객체)
System.out.println(a.equals(b)); // true

// 127 초과: 매번 새 객체 생성
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false (다른 객체!)
System.out.println(c.equals(d)); // true (내용은 같음)

// 결론: Integer 비교는 항상 equals()를 사용하세요!
Integer x = 1000;
Integer y = 1000;
System.out.println(x.equals(y)); // true (올바른 비교)
}
}
Integer 비교 규칙

래퍼 클래스 객체 비교는 항상 equals()를 사용 합니다. ==는 객체의 주소를 비교하므로 캐시 범위 밖에서는 예상과 다른 결과가 나옵니다.

4. 래퍼 클래스의 유틸리티 메서드

래퍼 클래스는 단순한 포장 이상으로 강력한 정적 유틸리티 메서드를 제공합니다.

Integer 유틸리티

public class IntegerUtils {
public static void main(String[] args) {
// 문자열 ↔ 숫자 변환
int parsed = Integer.parseInt("42"); // "42" → 42
int parsedHex = Integer.parseInt("FF", 16); // "FF"(16진수) → 255
int parsedBin = Integer.parseInt("1010", 2); // "1010"(2진수) → 10
System.out.printf("10진수: %d, 16진수: %d, 2진수: %d%n", parsed, parsedHex, parsedBin);

// 숫자 → 다양한 진수 문자열
System.out.println(Integer.toBinaryString(255)); // 11111111
System.out.println(Integer.toHexString(255)); // ff
System.out.println(Integer.toOctalString(255)); // 377

// 최솟값, 최댓값 상수
System.out.println(Integer.MAX_VALUE); // 2147483647
System.out.println(Integer.MIN_VALUE); // -2147483648
System.out.println(Long.MAX_VALUE); // 9223372036854775807

// 비트 조작
System.out.println(Integer.bitCount(255)); // 8 (1인 비트 개수)
System.out.println(Integer.highestOneBit(100)); // 64
System.out.println(Integer.numberOfLeadingZeros(1)); // 31
System.out.println(Integer.reverse(1)); // 비트 역전

// 비교
System.out.println(Integer.compare(10, 20)); // -1 (10 < 20)
System.out.println(Integer.max(10, 20)); // 20
System.out.println(Integer.min(10, 20)); // 10
System.out.println(Integer.sum(10, 20)); // 30
}
}

Double, Character 유틸리티

public class WrapperUtils {
public static void main(String[] args) {
// Double
System.out.println(Double.parseDouble("3.14")); // 3.14
System.out.println(Double.isNaN(0.0 / 0.0)); // true (Not a Number)
System.out.println(Double.isInfinite(1.0 / 0)); // true (무한대)
System.out.println(Double.MAX_VALUE); // 1.7976931348623157E308
System.out.println(Double.MIN_VALUE); // 4.9E-324 (양수 최솟값)

// Character
System.out.println(Character.isLetter('A')); // true
System.out.println(Character.isDigit('5')); // true
System.out.println(Character.isWhitespace(' ')); // true
System.out.println(Character.isUpperCase('A')); // true
System.out.println(Character.isLowerCase('a')); // true
System.out.println(Character.toUpperCase('a')); // A
System.out.println(Character.toLowerCase('Z')); // z
System.out.println(Character.isLetterOrDigit('9')); // true

// Boolean
System.out.println(Boolean.parseBoolean("true")); // true
System.out.println(Boolean.parseBoolean("TRUE")); // true
System.out.println(Boolean.parseBoolean("yes")); // false (true만 인식)
System.out.println(Boolean.toString(false)); // "false"
}
}

5. Number 클래스 계층

모든 숫자형 래퍼 클래스(Integer, Long, Double, Float, Short, Byte)는 추상 클래스 Number를 상속받습니다.

public class NumberHierarchy {
public static void printNumber(Number n) {
// Number 타입으로 받아서 다양한 기본형으로 추출 가능
System.out.printf("intValue: %d, doubleValue: %.2f, longValue: %d%n",
n.intValue(), n.doubleValue(), n.longValue());
}

public static void main(String[] args) {
printNumber(42); // int → Integer → Number
printNumber(3.14); // double → Double → Number
printNumber(100L); // long → Long → Number
printNumber((short)5); // short → Short → Number
}
}

6. Optional을 이용한 null 안전 처리

Java 8부터 Optional<T>를 사용하면 null을 직접 다루는 것보다 안전하게 처리할 수 있습니다.

import java.util.Optional;

public class OptionalExample {
// null을 반환할 수 있는 메서드 (안전하지 않음)
static Integer findScoreUnsafe(String name) {
if ("Alice".equals(name)) return 95;
return null; // 없으면 null 반환 → NullPointerException 위험!
}

// Optional을 반환하는 메서드 (안전)
static Optional<Integer> findScore(String name) {
if ("Alice".equals(name)) return Optional.of(95);
return Optional.empty(); // null 대신 빈 Optional 반환
}

public static void main(String[] args) {
// Optional 기본 사용
Optional<Integer> result = findScore("Alice");
if (result.isPresent()) {
System.out.println("점수: " + result.get()); // 점수: 95
}

// orElse: 없으면 기본값
int score = findScore("Bob").orElse(0);
System.out.println("Bob 점수: " + score); // 0

// orElseGet: 없으면 람다 실행
int score2 = findScore("Charlie").orElseGet(() -> -1);
System.out.println("Charlie 점수: " + score2); // -1

// orElseThrow: 없으면 예외
try {
int score3 = findScore("Dave").orElseThrow(
() -> new RuntimeException("점수 없음")
);
} catch (RuntimeException e) {
System.out.println("오류: " + e.getMessage()); // 오류: 점수 없음
}

// ifPresent: 값이 있을 때만 실행
findScore("Alice").ifPresent(s -> System.out.println("Alice의 점수: " + s));

// map: 값이 있으면 변환
Optional<String> grade = findScore("Alice").map(s -> s >= 90 ? "A" : "B");
System.out.println("등급: " + grade.orElse("없음")); // 등급: A
}
}

7. 실전 예제: 문자열 배열을 정수 배열로 안전하게 변환

import java.util.ArrayList;
import java.util.List;
import java.util.OptionalInt;

public class SafeIntegerConversion {

// 안전한 문자열 → 정수 변환 (실패 시 null 반환)
static Integer safeParseInt(String s) {
if (s == null || s.isBlank()) return null;
try {
return Integer.parseInt(s.trim());
} catch (NumberFormatException e) {
return null;
}
}

// 문자열 배열 → 유효한 정수 리스트 변환
static List<Integer> parseIntegers(String[] strings) {
List<Integer> result = new ArrayList<>();
for (String s : strings) {
Integer value = safeParseInt(s);
if (value != null) {
result.add(value);
} else {
System.out.println("변환 실패 (건너뜀): '" + s + "'");
}
}
return result;
}

public static void main(String[] args) {
String[] inputs = {"10", "25", "abc", null, " 30 ", "", "15", "xyz", "100"};

System.out.println("=== 문자열 배열 → 정수 변환 ===");
List<Integer> numbers = parseIntegers(inputs);

System.out.println("\n변환 성공: " + numbers);
System.out.println("개수: " + numbers.size());

// 통계 계산
int sum = 0;
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;

for (Integer n : numbers) {
sum += n; // 오토언박싱
if (n > max) max = n; // 오토언박싱
if (n < min) min = n; // 오토언박싱
}

double avg = (double) sum / numbers.size();
System.out.printf("합계: %d, 평균: %.1f, 최대: %d, 최소: %d%n",
sum, avg, max, min);
}
}

실행 결과:

=== 문자열 배열 → 정수 변환 ===
변환 실패 (건너뜀): 'abc'
변환 실패 (건너뜀): 'null'
변환 실패 (건너뜀): ''
변환 실패 (건너뜀): 'xyz'

변환 성공: [10, 25, 30, 15, 100]
개수: 5
합계: 180, 평균: 36.0, 최대: 100, 최소: 10
고수 팁: 래퍼 클래스 사용 지침
  1. 컬렉션, 제네릭: List<Integer>, Map<String, Boolean> 등 객체만 허용하는 곳에서 사용
  2. null 허용 필요: DB에서 null이 올 수 있는 필드는 Integer 타입으로 선언
  3. 유틸리티 메서드: Integer.parseInt(), Double.isNaN() 등 정적 메서드 활용
  4. 성능 중요 시: 반복 연산에서는 기본형 int, double을 사용
  5. 비교: 래퍼 객체 간 값 비교는 항상 equals() 사용, == 금지

다음 장에서는 모든 클래스의 최상위 부모인 Object 클래스 에 대해 배워보겠습니다.