Chuyển tới nội dung chính

Wrapper Classes & Autoboxing

Wrapper classes là cầu nối giữa primitive types và object-oriented world trong Java. Hiểu wrapper classes giải thích tại sao List<int> không compile, tại sao == đôi khi cho kết quả bất ngờ với Integer, và tại sao boxing overhead ảnh hưởng performance.

Wrapper Classes là gì?

Mỗi primitive type trong Java có một wrapper class tương ứng — là object "bọc" (wrap) giá trị primitive:

PrimitiveWrapper ClassVí dụ
byteByteByte b = 42;
shortShortShort s = 100;
intIntegerInteger n = 42;
longLongLong l = 100L;
floatFloatFloat f = 3.14f;
doubleDoubleDouble d = 3.14;
charCharacterCharacter c = 'A';
booleanBooleanBoolean flag = true;

Tại sao cần Wrapper Classes?

// ❌ Không compile — Generics yêu cầu reference types
List<int> numbers = new ArrayList<>();

// ✅ Dùng wrapper class
List<Integer> numbers = new ArrayList<>();
numbers.add(42); // int 42 → tự động boxing thành Integer
Lý doGiải thích
GenericsList<T> yêu cầu object, không chấp nhận primitive
Null valuePrimitive không thể null, nhưng Integer có thể: Integer age = null;
Utility methodsInteger.parseInt("42"), Integer.MAX_VALUE, Integer.toBinaryString(42)
CollectionsMap<String, Integer> — value phải là object
API yêu cầu ObjectNhiều API nhận Object parameter, primitive không phải Object

Autoboxing & Unboxing

Từ Java 5, compiler tự động chuyển đổi giữa primitive và wrapper. Hãy tưởng tượng:

  • Autoboxing = đóng hộp: Bạn có 1 viên kẹo (primitive int), Java tự động bỏ vào hộp (Integer) để gửi qua bưu điện (Collections, APIs)
  • Unboxing = mở hộp: Bạn nhận hộp (Integer), Java tự động lấy viên kẹo ra (int) để bạn ăn (tính toán)

Autoboxing (Primitive → Wrapper)

// Compiler tự động chuyển int → Integer
Integer num = 42; // Autoboxing: int 42 → Integer.valueOf(42)
List<Integer> list = new ArrayList<>();
list.add(100); // Autoboxing: int 100 → Integer.valueOf(100)

Unboxing (Wrapper → Primitive)

Integer wrapped = Integer.valueOf(42);
int primitive = wrapped; // Unboxing: Integer → int
int sum = wrapped + 10; // Unboxing wrapped, tính toán, kết quả là int

Compiler làm gì bên dưới?

// Code bạn viết:
Integer num = 42;
int result = num + 10;

// Compiler chuyển thành:
Integer num = Integer.valueOf(42); // Autoboxing
int result = num.intValue() + 10; // Unboxing
NullPointerException khi Unboxing null
Integer num = null;
int result = num; // ❌ NullPointerException!
// Compiler chuyển thành: num.intValue() → gọi method trên null

Đây là nguồn bug rất phổ biến. Luôn kiểm tra null trước khi unbox:

Integer num = getAge();  // Có thể trả về null
if (num != null) {
int age = num; // An toàn
}

Integer Cache — Bẫy của ==

Hiện tượng bất ngờ

Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true ← OK?

Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false ← Tại sao?!

Giải thích: Integer Cache Pool

Theo JLS §5.1.7 (Boxing Conversion), JVM cache các Integer objects từ -128 đến 127:

// Integer.valueOf() implementation (simplified)
public static Integer valueOf(int i) {
if (i >= -128 && i <= 127) {
return IntegerCache.cache[i + 128]; // Trả về object đã cache
}
return new Integer(i); // Tạo object MỚI
}
Integer a = 127;  → valueOf(127) → cache[255] → Object@1
Integer b = 127; → valueOf(127) → cache[255] → Object@1 (CÙNG object)
a == b → true (so sánh reference → cùng object)

Integer c = 128; → valueOf(128) → new Integer(128) → Object@2
Integer d = 128; → valueOf(128) → new Integer(128) → Object@3 (KHÁC object)
c == d → false (so sánh reference → khác object)

Luôn dùng .equals() cho Wrapper Objects

Integer x = 200;
Integer y = 200;

// ❌ Không đáng tin — phụ thuộc vào cache range
System.out.println(x == y); // false

// ✅ Luôn đúng — so sánh giá trị
System.out.println(x.equals(y)); // true
Quy tắc vàng

Luôn dùng .equals() khi so sánh wrapper objects. == với wrapper objects so sánh reference (địa chỉ bộ nhớ), không phải value (giá trị).

Duy nhất ngoại lệ: Boolean.TRUEBoolean.FALSE là constants, == luôn hoạt động cho Boolean.

Utility Methods

Wrapper classes cung cấp nhiều methods hữu ích:

Parsing — Chuyển String sang số

int num = Integer.parseInt("42");           // "42" → 42
double pi = Double.parseDouble("3.14"); // "3.14" → 3.14
long big = Long.parseLong("1000000"); // "1000000" → 1000000
boolean flag = Boolean.parseBoolean("true"); // "true" → true

// ❌ NumberFormatException nếu string không hợp lệ
int bad = Integer.parseInt("hello"); // NumberFormatException
int bad2 = Integer.parseInt("3.14"); // NumberFormatException (int không có thập phân)

Constants — Giá trị giới hạn

System.out.println(Integer.MAX_VALUE);   // 2147483647 (2^31 - 1)
System.out.println(Integer.MIN_VALUE); // -2147483648 (-2^31)
System.out.println(Long.MAX_VALUE); // 9223372036854775807
System.out.println(Double.POSITIVE_INFINITY);
System.out.println(Double.NaN); // Not a Number

Conversion — Chuyển đổi hệ cơ số

System.out.println(Integer.toBinaryString(42));   // "101010"
System.out.println(Integer.toOctalString(42)); // "52"
System.out.println(Integer.toHexString(42)); // "2a"
System.out.println(Integer.parseInt("101010", 2)); // 42 (binary → decimal)

Compare — So sánh

int result = Integer.compare(10, 20);  // -1 (10 < 20)
int result2 = Integer.compare(20, 10); // 1 (20 > 10)
int result3 = Integer.compare(10, 10); // 0 (bằng nhau)

int max = Integer.max(10, 20); // 20
int min = Integer.min(10, 20); // 10
int sum = Integer.sum(10, 20); // 30

Performance: Boxing Overhead

Vấn đề trong vòng lặp

// ❌ Tốn performance — mỗi += tạo Integer object mới
Integer sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += i; // Unbox sum → tính → Autobox kết quả → gán lại
// Tương đương: sum = Integer.valueOf(sum.intValue() + i);
}

// ✅ Dùng primitive — nhanh hơn nhiều
int sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += i; // Chỉ phép cộng trên stack, không tạo object
}

Tại sao chậm?

BướcPrimitive intWrapper Integer
StorageStack (4 bytes)Heap (16+ bytes: header + value)
+=Phép cộng trực tiếpUnbox → cộng → box → GC old object
1M iterations0 objects created~1M Integer objects → GC pressure
Best Practice
  • Dùng primitive cho local variables, loops, calculations
  • Dùng wrapper khi cần: Collections, null values, API yêu cầu Object
  • Java 8+: Dùng IntStream, LongStream, DoubleStream thay Stream<Integer> để tránh boxing
// ❌ Boxing mỗi element
int sum = list.stream()
.mapToInt(Integer::intValue) // Unbox
.sum();

// ✅ Primitive stream — không boxing
int sum = IntStream.rangeClosed(1, 1_000_000).sum();

Ví dụ thực tế: Xử lý dữ liệu nullable

UserService.java
public class UserService {

// Integer (không phải int) vì age có thể null trong database
public String formatAge(Integer age) {
if (age == null) {
return "Chưa cập nhật";
}
// Unboxing an toàn — đã check null
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Tuổi không hợp lệ: " + age);
}
return age + " tuổi";
}

// Chuyển đổi input từ form (String → Integer)
public Integer parseAge(String input) {
if (input == null || input.trim().isEmpty()) {
return null; // Wrapper cho phép null
}
try {
int age = Integer.parseInt(input.trim());
return age; // Autoboxing
} catch (NumberFormatException e) {
return null;
}
}
}

Lỗi thường gặp

Lỗi thường gặp

Lỗi 1: Dùng == so sánh wrapper objects

// ❌ Hoạt động với 127, SAI với 128
Integer a = 128;
Integer b = 128;
if (a == b) { } // false!

// ✅ Luôn dùng .equals()
if (a.equals(b)) { } // true

Tại sao: == so sánh reference, không phải giá trị. Integer cache chỉ từ -128 đến 127.

Lỗi 2: Unboxing null → NullPointerException

// ❌ NPE nếu map không chứa key
Map<String, Integer> scores = new HashMap<>();
int score = scores.get("math"); // NPE! get() trả về null

// ✅ Dùng getOrDefault hoặc check null
int score = scores.getOrDefault("math", 0);

Lỗi 3: Boxing trong performance-critical loop

// ❌ Tạo hàng triệu Integer objects
Long total = 0L;
for (long i = 0; i < 10_000_000; i++) {
total += i;
}

// ✅ Dùng primitive
long total = 0L;
for (long i = 0; i < 10_000_000; i++) {
total += i;
}

Boolean Cache và Character Utility

Boolean Cache

Boolean.valueOf() luôn trả về 1 trong 2 cached instances — không bao giờ tạo object mới:

Boolean a = Boolean.valueOf(true);
Boolean b = Boolean.valueOf(true);
System.out.println(a == b); // true — luôn là Boolean.TRUE
System.out.println(a == Boolean.TRUE); // true

Boolean c = Boolean.valueOf(false);
System.out.println(c == Boolean.FALSE); // true

Vì Boolean chỉ có 2 giá trị nên == luôn hoạt động đúng (khác với Integer chỉ cache -128 đến 127).

Character Utility Methods

Character class cung cấp nhiều methods hữu ích để kiểm tra và chuyển đổi ký tự:

// Kiểm tra loại ký tự
System.out.println(Character.isDigit('5')); // true
System.out.println(Character.isDigit('A')); // false
System.out.println(Character.isLetter('A')); // true
System.out.println(Character.isWhitespace(' ')); // true
System.out.println(Character.isUpperCase('A')); // true
System.out.println(Character.isLowerCase('a')); // true

// Chuyển đổi
System.out.println(Character.toUpperCase('a')); // 'A'
System.out.println(Character.toLowerCase('Z')); // 'z'
System.out.println(Character.getNumericValue('7')); // 7

Ví dụ thực tế — validate input:

public static boolean isValidUsername(String username) {
if (username == null || username.isEmpty()) return false;

// Ký tự đầu phải là chữ cái
if (!Character.isLetter(username.charAt(0))) return false;

// Các ký tự còn lại: chữ cái, số, hoặc underscore
for (char c : username.toCharArray()) {
if (!Character.isLetterOrDigit(c) && c != '_') {
return false;
}
}
return true;
}

Integer Overflow — Tràn số

OCP Trap — Integer Overflow

Khi giá trị vượt quá Integer.MAX_VALUE, nó quay vòng (wrap around) về Integer.MIN_VALUE. Java không throw exception!

System.out.println(Integer.MAX_VALUE);       // 2147483647
System.out.println(Integer.MAX_VALUE + 1); // -2147483648 (MIN_VALUE!)
System.out.println(Integer.MIN_VALUE - 1); // 2147483647 (MAX_VALUE!)

// Ví dụ thực tế: tính tiền sai
int price = 2_000_000_000; // 2 tỷ
int quantity = 2;
int total = price * quantity; // -294967296 ← SAI! Overflow!

// ✅ Fix: dùng long
long totalSafe = (long) price * quantity; // 4000000000 ← ĐÚNG
Phát hiện overflow

Java 8+ có Math.addExact(), Math.multiplyExact() — throw ArithmeticException nếu overflow:

int a = Integer.MAX_VALUE;
int b = 1;
int sum = Math.addExact(a, b); // ArithmeticException: integer overflow

Bài tập

Bài 1: Wrapper Basics [Cơ bản]

Viết method convertAndPrint(String[] args) nhận mảng String, chuyển mỗi phần tử sang Integer (bỏ qua phần tử không hợp lệ), in ra tổng, min, max.

Xem lời giải
public static void convertAndPrint(String[] args) {
List<Integer> numbers = new ArrayList<>();

for (String s : args) {
try {
numbers.add(Integer.parseInt(s.trim()));
} catch (NumberFormatException e) {
System.out.println("Bỏ qua: " + s);
}
}

if (numbers.isEmpty()) {
System.out.println("Không có số hợp lệ");
return;
}

int sum = 0, min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
for (int num : numbers) { // Unboxing
sum += num;
min = Math.min(min, num);
max = Math.max(max, num);
}

System.out.println("Tổng: " + sum);
System.out.println("Min: " + min);
System.out.println("Max: " + max);
}

Bài 2: Integer Cache Test [Trung bình]

Viết chương trình kiểm chứng Integer cache: tạo 2 Integer với cùng giá trị, thử các giá trị -129, -128, 0, 127, 128. In kết quả ==.equals() cho mỗi cặp. Giải thích output.

Xem lời giải
public class IntegerCacheTest {
public static void main(String[] args) {
int[] testValues = {-129, -128, 0, 127, 128};

for (int val : testValues) {
Integer a = val; // Autoboxing
Integer b = val; // Autoboxing

System.out.printf("Value: %4d | == : %-5b | equals: %-5b%n",
val, (a == b), a.equals(b));
}
}
}

Output:

Value: -129 | == : false | equals: true    ← Ngoài cache range
Value: -128 | == : true | equals: true ← Trong cache [-128, 127]
Value: 0 | == : true | equals: true ← Trong cache
Value: 127 | == : true | equals: true ← Biên trên cache
Value: 128 | == : false | equals: true ← Ngoài cache range

Bài 3: Performance Benchmark [Thách thức]

So sánh thời gian tính tổng 1 triệu số sử dụng: (a) Integer wrapper, (b) int primitive, (c) IntStream. Dùng System.nanoTime() để đo. Chạy 5 lần, tính trung bình. Giải thích kết quả.

Gợi ý
long start = System.nanoTime();
// ... code cần đo
long elapsed = System.nanoTime() - start;
System.out.printf("Time: %.2f ms%n", elapsed / 1_000_000.0);

Lưu ý: JVM warmup ảnh hưởng kết quả — chạy thử vài lần trước khi đo chính thức.

Tóm tắt

Khái niệmĐiểm chính
Wrapper ClassesObject bọc primitive: Integer, Double, Boolean...
AutoboxingCompiler tự chuyển primitive → wrapper (int → Integer)
UnboxingCompiler tự chuyển wrapper → primitive (Integer → int)
Integer CacheJVM cache Integer -128 đến 127, == chỉ đúng trong range này
.equals()Luôn dùng .equals() cho wrapper, KHÔNG dùng ==
Null dangerUnboxing nullNullPointerException
PerformancePrimitive nhanh hơn wrapper trong loops, dùng primitive streams

Đọc thêm