Backend

99 Ngày Java — Ngày 22: Wrapper class & autoboxing

SSite Admin
18 tháng 08, 2026 6 phút đọc 0 lượt xem
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.

Sketchnote Ngày 22: wrapper class & autoboxing — Integer cache, bẫy unboxing null, boxing trong vòng lặp, parseInt vs valueOf

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ượngList<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 valueOf hoặc intValue mà 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: valueOf phụ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, Character có cache tương tự; Double/Float thì 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, collections
  • Unboxing 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 Long thay vì long là 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ị.

  • NumberFormatException là 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ả Integer có thể null, gán thẳng vào int — quan sát NPE, rồi sửa bằng Objects.requireNonNullElse.

  • Đo thời gian cộng dồn 10 triệu số bằng Longlong (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ý NumberFormatException là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!

S

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.

Bài viết liên quan

99 Ngày Spring — Ngày 22: Entity mapping cơ bản

Ngày 22 của 99 Ngày Spring: entity đầu tiên với @Entity/@Id và lý do bắt buộc no-arg constructor (entity là class, không phải record), chiến lược sinh khóa IDENTITY vs SEQUENCE trên PostgreSQL, @Column với ràng buộc và ánh xạ kiểu — wrapper cho cột nullable, cùng show-sql và quy ước snake_case.

18 thg 8, 20266 phút0
99 Ngày Java — Ngày 21: StringBuilder & xử lý chuỗi nâng cao

Ngày 21 của 99 Ngày Java: vì sao nối chuỗi bằng + trong vòng lặp có chi phí bình phương, StringBuilder với capacity và bộ API thường dùng, String pool — lý do == đôi khi tình cờ đúng nhưng equals mới là quy tắc, cùng String.format và text block cho chuỗi nhiều dòng dễ đọc.

17 thg 8, 20266 phút11
99 Ngày Spring — Ngày 21: JPA & Hibernate tổng quan

Ngày 21 của 99 Ngày Spring, mở đầu giai đoạn Spring Data JPA: impedance mismatch giữa đối tượng và bảng quan hệ, phân vai JPA (đặc tả) — Hibernate (cài đặt) — Spring Data JPA (tầng tiện ích), EntityManager với persistence context, và lựa chọn ddl-auto đúng cho từng môi trường.

17 thg 8, 20267 phút12