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:
| Primitive | Wrapper Class | Ví dụ |
|---|---|---|
byte | Byte | Byte b = 42; |
short | Short | Short s = 100; |
int | Integer | Integer n = 42; |
long | Long | Long l = 100L; |
float | Float | Float f = 3.14f; |
double | Double | Double d = 3.14; |
char | Character | Character c = 'A'; |
boolean | Boolean | Boolean 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ý do | Giải thích |
|---|---|
| Generics | List<T> yêu cầu object, không chấp nhận primitive |
| Null value | Primitive không thể null, nhưng Integer có thể: Integer age = null; |
| Utility methods | Integer.parseInt("42"), Integer.MAX_VALUE, Integer.toBinaryString(42) |
| Collections | Map<String, Integer> — value phải là object |
| API yêu cầu Object | Nhiề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
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
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.TRUE và Boolean.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ước | Primitive int | Wrapper Integer |
|---|---|---|
| Storage | Stack (4 bytes) | Heap (16+ bytes: header + value) |
+= | Phép cộng trực tiếp | Unbox → cộng → box → GC old object |
| 1M iterations | 0 objects created | ~1M Integer objects → GC pressure |
- 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,DoubleStreamthayStream<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
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 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ố
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
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ả == và .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 Classes | Object bọc primitive: Integer, Double, Boolean... |
| Autoboxing | Compiler tự chuyển primitive → wrapper (int → Integer) |
| Unboxing | Compiler tự chuyển wrapper → primitive (Integer → int) |
| Integer Cache | JVM cache Integer -128 đến 127, == chỉ đúng trong range này |
.equals() | Luôn dùng .equals() cho wrapper, KHÔNG dùng == |
| Null danger | Unboxing null → NullPointerException |
| Performance | Primitive nhanh hơn wrapper trong loops, dùng primitive streams |