99 Ngày Java — Ngày 22: Wrapper class & autoboxing
Ngày 03 giới thiệu 8 kiểu nguyên thủy; hôm nay ta gặp 8 người anh em đối tượng của chúng: wrapper class. Cầu nối giữa hai thế giới là autoboxing — compiler tự chuyển đổi qua lại, tiện đến mức gần như vô hình. Nhưng chính sự vô hình đó sinh ra ba cái bẫy kinh điển: Integer so sánh == cho kết quả thay đổi theo giá trị, unboxing null ném NullPointerException, và boxing thầm lặng trong vòng lặp tạo hàng triệu đối tượng rác.

Wrapper & autoboxing — cầu nối hai thế giới
// 8 kiểu nguyên thủy (Ngày 03) — 8 lớp wrapper tương ứng:// int → Integer · long → Long · double → Double// boolean → Boolean · char → Character ...
// Vì sao cần wrapper? Generics & collections chỉ nhận ĐỐI TƯỢNG:
List<int> khongHopLe; // ✗ không biên dịch
List<Integer> hopLe = new ArrayList<>(); // ✓
// AUTOBOXING / UNBOXING — compiler chuyển đổi tự động hai chiều:Integer boxed = 42; // autoboxing: int → Integer.valueOf(42)int primitive = boxed; // unboxing: Integer → boxed.intValue()
hopLe.add(7); // autoboxing khi thêm vào collectionint dau = hopLe.get(0); // unboxing khi lấy ra
// Tiện lợi — nhưng chính sự "tự động" này sinh ra các bẫy của hôm nay.Wrapper tồn tại vì hệ thống generics (Ngày 39) và collections (Giai đoạn 4) chỉ làm việc với đối tượng —
List<int>không hợp lệ,List<Integer>là cách biểu diễn đúng.Autoboxing/unboxing là đường chuyển đổi tự động hai chiều — mỗi lần chuyển đều có chi phí thật: một lời gọi
valueOfhoặcintValuemà code không hiển thị.Wrapper là bất biến (Ngày 20) — nền tảng cho cơ chế cache ở mục kế tiếp.
Integer cache — vì sao == lúc đúng lúc sai
Integer a = 127, b = 127;
a == b; // true — ???
Integer c = 128, d = 128;
c == d; // false — ?!?
// INTEGER CACHE: valueOf tái sử dụng đối tượng trong khoảng -128..127 —// ngoài khoảng đó, mỗi lần boxing là một đối tượng MỚI.// Cùng nguyên lý String pool (Ngày 21): chia sẻ an toàn nhờ bất biến.
// Quy tắc không đổi: so sánh wrapper LUÔN dùng equals:
c.equals(d); // true — nội dung như nhau
// new Integer(...) đã deprecated từ Java 9 —// luôn để autoboxing / valueOf đảm nhận việc tạo wrapper.Hiện tượng 127 đúng / 128 sai không phải ngẫu nhiên:
valueOfphục vụ khoảng -128..127 từ cache dùng chung — cùng đối tượng nên=="tình cờ đúng", hệt như String pool của Ngày 21.Đây là câu hỏi phỏng vấn kinh điển — và câu trả lời chuẩn luôn kết thúc bằng quy tắc: so sánh giá trị của wrapper luôn dùng
equals, không bao giờ dựa vào cache.Long,Short,Byte,Charactercó cache tương tự;Double/Floatthì không — thêm một lý do để không bao giờ tin==với wrapper.
Hai bẫy vận hành: null và vòng lặp
// Bẫy 1 — unboxing null: NullPointerExceptionInteger diem = timDiem(maSV); // method có thể trả nullint d = diem; // ✗ null.intValue() → NPE lúc runtime
// An toàn: kiểm tra null trước, hoặc cung cấp mặc định:int anToan = Objects.requireNonNullElse(diem, 0);
// Bẫy 2 — boxing thầm lặng trong vòng lặp:Long tong = 0L; // wrapper!for (long i = 0; i < 10_000_000; i++) {
tong += i; // mỗi vòng: unbox + cộng + BOX đối tượng MỚI
} // → hàng triệu đối tượng rác
long tongNhanh = 0L; // primitive: không một lần boxing nào
// Nguyên tắc chọn kiểu:// PRIMITIVE cho field & biến tính toán (mặc định)// WRAPPER khi cần "có thể null" (cột DB nullable — bài Spring hôm nay!)// hoặc khi bắt buộc: generics, collectionsUnboxing null là nguồn NPE khó thấy nhất vì dòng code trông hoàn toàn vô hại — mọi chỗ nhận wrapper từ nguồn có thể null (DB, Map, method trả
Integer) đều cần phương án mặc định.Vòng lặp cộng dồn trên
Longthay vìlonglà lỗi hiệu năng kinh điển từ Effective Java: mỗi vòng sinh một đối tượng mới — cùng họ với bài học nối chuỗi bằng+của Ngày 21.Nguyên tắc chọn kiểu ba dòng trong code trên đáng ghi nhớ — đặc biệt vế giữa: wrapper là lựa chọn đúng khi giá trị cần biểu diễn
null, như cột nullable trong DB (bài Spring hôm nay dùng đúng nguyên tắc này).
parseInt vs valueOf
// Chuỗi → số: hai method, hai kiểu trả vềint a = Integer.parseInt("42"); // trả PRIMITIVE intInteger b = Integer.valueOf("42"); // trả WRAPPER (đi qua cache)
// Chọn theo đích đến:// tính toán tiếp → parseInt (không boxing thừa)// đưa vào collection / cần nullable → valueOf
// Cả hai ném NumberFormatException với đầu vào không hợp lệ:
Integer.parseInt("42a"); // ✗ NumberFormatException
Integer.parseInt(""); // ✗ NumberFormatException
// Đầu vào từ người dùng: validate trước khi parse —// đúng vai trò Bean Validation của bài Spring Ngày 14.Khác biệt nằm ở kiểu trả về:
parseInt→ primitive (tính toán tiếp không boxing),valueOf→ wrapper (đi qua cache, hợp với collection) — chọn theo đích đến của giá trị.NumberFormatExceptionlà unchecked exception (phân loại đầy đủ ở Ngày 41) — với đầu vào người dùng, hàng rào đúng là validate trước khi parse.
Bài tập nhỏ
Dự đoán rồi kiểm chứng bộ tứ:
Integer 127 == 127,128 == 128,-128 == -128,-129 == -129— giải thích từng kết quả bằng khoảng cache.Viết method trả
Integercó thể null, gán thẳng vàoint— quan sát NPE, rồi sửa bằngObjects.requireNonNullElse.Đo thời gian cộng dồn 10 triệu số bằng
Longvàlong(System.nanoTime()— như bài đo của Ngày 21) — ghi lại chênh lệch.Viết đoạn đọc input người dùng: validate bằng regex đơn giản trước, rồi mới
parseInt— xử lýNumberFormatExceptionlàm lưới cuối.
Kết luận
Wrapper và autoboxing gói gọn trong bốn ý: cầu nối primitive – đối tượng là tự động nhưng có chi phí, Integer cache khiến == thất thường nên equals là quy tắc duy nhất, null và vòng lặp là hai bẫy vận hành cần nhận diện từ xa, và parseInt/valueOf chọn theo đích đến. Ngày 23 ta gặp kiểu dữ liệu mô hình hóa tập giá trị cố định: enum — không chỉ là hằng số, mà là lớp đầy đủ với field và method. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


