Kiểu tham chiếu — null, NPE, wrapper class và autoboxing
Hiểu sâu về reference types trong Java: null là gì, NullPointerException xảy ra thế nào, wrapper class và autoboxing hoạt động ra sao, Integer Cache gây surprise gì, và khi nào dùng .equals() thay vì ==.
Bài 1 giải thích reference là "địa chỉ trỏ tới object trên heap". Bài 2 đi sâu vào primitive. Bài này hoàn thành bức tranh: kiểu tham chiếu hoạt động thế nào trong thực tế — từ null đến NPE, từ wrapper class đến cái bẫy Integer Cache mà kể cả senior đôi khi quên.
1. Analogy — Thẻ gửi xe
Hãy tưởng tượng reference như thẻ gửi xe:
- Thẻ (reference) = biến bạn giữ trong code
- Xe (object) = dữ liệu thật nằm trên heap
- Bãi xe (heap) = vùng nhớ JVM quản lý
Khi bạn "copy" thẻ (gán reference), bạn có 2 thẻ cùng số hiệu → cùng lấy 1 chiếc xe. Khi bạn đánh mất thẻ cuối cùng (không còn reference nào), JVM dọn xe (Garbage Collection).
Thẻ rỗng = null. Quẹt thẻ rỗng vào máy → máy báo lỗi → NullPointerException.
2. Các loại reference type trong Java
Khác với 8 kiểu primitive cố định, số lượng reference type là vô hạn — mọi class bạn hoặc thư viện định nghĩa đều là một reference type:
| Nhóm | Ví dụ | Ghi chú |
|---|---|---|
| Class types | String, Integer, ArrayList, class tự viết | Phổ biến nhất |
| Interface types | Comparable, Runnable, List | Biến kiểu interface |
| Array types | int[], String[], int[][] | Mảng là reference |
| Enum types | Day.MONDAY, Status.ACTIVE | Enum là class đặc biệt |
| Record types (Java 16+) | record Point(int x, int y) | Immutable data class |
String ten = "An"; // class type
int[] so = {1, 2, 3}; // array type (int[] la reference)
List<String> ds = new ArrayList<>(); // interface type
3. null — reference không trỏ tới đâu
null là giá trị đặc biệt có nghĩa: "reference này không trỏ tới object nào".
String s = null; // bien s ton tai nhung khong tro vao object nao
Sự thật về null:
- Chỉ reference type mới có thể là
null. Primitive (int,double...) không bao giờnull. - Default của field reference là
null(xem bài 1 về default values). nullcó thể gán cho bất kỳ reference type nào.- Kích thước
nulltrong memory = kích thước 1 reference (4 hoặc 8 byte tuỳ JVM).
String s = null;
int n = null; // COMPILE ERROR -- primitive khong co null
4. NullPointerException — bẫy phổ biến nhất Java
Gọi bất kỳ method hoặc truy cập field qua null reference → NullPointerException (NPE):
String s = null;
int len = s.length(); // NullPointerException at runtime!
Java 14+ helpful NPE message — không còn thông báo mơ hồ:
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.length()" because "s" is null
4.1 NPE xảy ra ở đâu?
String s = null;
s.length(); // (A) goi method qua null
s.charAt(0); // (B) goi method qua null
int[] arr = null;
int x = arr[0]; // (C) truy cap phan tu mang null
int len = arr.length; // (D) doc .length cua mang null
throw null; // (E) throw null reference
Tất cả (A)–(E) đều throw NPE runtime.
5. 4 cách phòng NPE phổ biến
5.1 Kiểm tra null trước khi dùng
if (s != null) {
System.out.println(s.length());
}
Ổn nhưng verbose khi có nhiều biến. Dùng khi logic "null = không xử lý" là hợp lý.
5.2 Objects.requireNonNull — fail-fast với message rõ
import java.util.Objects;
void xuLy(String ten) {
Objects.requireNonNull(ten, "ten must not be null");
// an toan dung ten tu day tro di
System.out.println(ten.toUpperCase());
}
Khi ten == null, method ném ngay NullPointerException: ten must not be null tại dòng đó, thay vì crash ở chỗ khác khó debug. Dùng ở đầu method nhận tham số để contract rõ ràng.
5.3 Objects.equals — so sánh an toàn với null
String a = null;
String b = "hello";
// Nguy hiem: a.equals(b) nem NPE vi a la null
// An toan:
boolean bang = Objects.equals(a, b); // false, khong throw NPE
Objects.equals(a, b) trả true nếu cả hai null, trả false nếu chỉ một null, gọi a.equals(b) nếu a != null.
5.4 Optional — tránh null bằng container rõ ràng (intro)
import java.util.Optional;
Optional<String> maybeName = Optional.ofNullable(getUserName());
maybeName.ifPresent(name -> System.out.println("Xin chao, " + name));
String name = maybeName.orElse("Khach");
Optional<T> là cách hiện đại để nói "giá trị này có thể không có". Thích hợp cho return type của method, không phải field. Chi tiết ở Module 10 (Streams & Optional).
- Input validation trong method:
Objects.requireNonNull(param, "...") - So sánh có thể null:
Objects.equals(a, b) - Return type có thể trống:
Optional<T> - Logic đơn giản:
if (x != null)
6. Wrapper classes — bọc primitive thành object
Mỗi primitive có một wrapper class tương ứng:
| Primitive | Wrapper class | Ghi chú |
|---|---|---|
byte | Byte | |
short | Short | |
int | Integer | Phổ biến nhất |
long | Long | |
float | Float | |
double | Double | |
boolean | Boolean | |
char | Character |
Vì sao cần wrapper?
- Collection không chứa primitive —
List<int>là lỗi compile; phải dùngList<Integer>. - Generic không work với primitive —
Map<String, int>sai; phảiMap<String, Integer>. - Primitive không có method —
42.toString()không hợp lệ;Integer.toString(42)hoặcString.valueOf(42)thì có. - Primitive không thể
null— đôi khi cần biểu diễn "không có giá trị" (ví dụ: optional DB column).
List<Integer> soNguyen = new ArrayList<>();
soNguyen.add(1); // OK: autoboxing int -> Integer
soNguyen.add(2);
soNguyen.add(3);
Map<String, Integer> diem = new HashMap<>();
diem.put("An", 95);
Tạo wrapper đúng cách
// DUNG: valueOf -- su dung cache khi co the
Integer a = Integer.valueOf(42);
Integer b = Integer.parseInt("42"); // tu String
// TRANH: new Integer() deprecated tu Java 9, danh dau forRemoval tu Java 16
// (tinh den Java 21 van chua bi xoa, nhung dung dung)
Integer c = new Integer(42); // @Deprecated
7. Autoboxing và Unboxing
Java tự động chuyển đổi giữa primitive và wrapper — gọi là autoboxing (primitive → wrapper) và unboxing (wrapper → primitive):
// Autoboxing: int -> Integer (compiler tao Integer.valueOf(42))
Integer x = 42; // tuong duong: Integer x = Integer.valueOf(42);
// Unboxing: Integer -> int (compiler tao x.intValue())
int y = x; // tuong duong: int y = x.intValue();

Pitfall — unbox null → NPE
Integer x = null;
int y = x; // NullPointerException! unbox null khong the lay intValue()
Đây là NPE ngầm khó debug: dòng int y = x; trông vô hại nhưng thực ra gọi x.intValue() mà x đang null.
// Ví dụ thực tế: diem co the null (hoc sinh chua thi)
Integer diem = layDiemTuDB(hocSinhId); // co the tra null
if (diem != null && diem > 50) { // unbox o day neu diem != null
System.out.println("Dat");
}
// DUNG -- khong unbox null
Autoboxing trong vòng lặp — performance trap
// CHAM: moi lan += tao Integer moi (unbox + tinh + autobox)
Integer sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += i; // unbox sum, cong i, autobox lai
}
// NHANH: dung int thuan
int sum2 = 0;
for (int i = 0; i < 1_000_000; i++) {
sum2 += i;
}
8. Integer Cache — bẫy ==
Đây là một trong những câu hỏi phỏng vấn Java kinh điển nhất:
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true -- bat ngo?
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false -- tai sao khac?
Vì sao lại thế?
JLS §5.1.7 quy định: Integer.valueOf(n) phải cache các instance cho giá trị từ −128 đến 127. Ngoài range đó, mỗi lần valueOf tạo object mới.
Integer a = 100; // Integer.valueOf(100) -- tra cached instance
Integer b = 100; // Integer.valueOf(100) -- tra CUNG cached instance
// a va b tro cung 1 object -> a == b la true
Integer c = 200; // Integer.valueOf(200) -- tao object moi
Integer d = 200; // Integer.valueOf(200) -- tao object moi KHAC
// c va d la 2 object khac nhau -> c == d la false

Không bao giờ dùng == để so sánh giá trị wrapper class.
Dùng .equals() hoặc Objects.equals().
Integer a = 200;
Integer b = 200;
a == b // false (behavior phu thuoc cache range)
a.equals(b) // true (so sanh gia tri)
Objects.equals(a, b) // true (null-safe)
Quy tắc này áp dụng cho tất cả wrapper: Long, Double, Boolean...
Boolean cũng cache
Boolean.valueOf(true) và Boolean.valueOf(false) luôn trả cached instance (Boolean.TRUE, Boolean.FALSE). Tuy nhiên vẫn nên dùng .equals() để code rõ ý định.
9. ✅/❌ Khi nào dùng wrapper vs primitive?
| Tình huống | Dùng gì | Lý do |
|---|---|---|
| Biến local, tính toán | ✅ int, double | Nhanh, không GC overhead |
Collection: List, Map, Set | ✅ Integer, Double | Generic yêu cầu object |
| Generic class/method | ✅ Wrapper | <T> không nhận primitive |
| Giá trị có thể "không có" (nullable) | ✅ Integer (hoặc Optional<Integer>) | Primitive không null |
| Field trong entity/DTO DB column nullable | ✅ Wrapper | Phân biệt 0 với "chưa có giá trị" |
| Vòng lặp, accumulation | ✅ int | Tránh boxing overhead |
So sánh bằng == | ❌ Wrapper | Dùng .equals() |
10. String — reference type đặc biệt
String là class, là reference type, nhưng có những đặc điểm riêng (bài 1 đã giới thiệu ở mức tổng quan):
String s1 = "hello"; // literal -> String Pool
String s2 = "hello"; // cung literal -> cung object trong Pool
String s3 = new String("hello"); // buoc pool, tao object rieng
String s4 = s3.intern(); // dua vao pool, tra reference cua pooled object
System.out.println(s1 == s2); // true (cung object trong pool)
System.out.println(s1 == s3); // false (khac object)
System.out.println(s1 == s4); // true (s4 = pooled reference)
System.out.println(s1.equals(s3)); // true (cung noi dung)
String là immutable — mọi method trả chuỗi mới, không sửa chuỗi cũ:
String s = "hello";
s.toUpperCase(); // khong co tac dung -- tra String moi bi bo di
s = s.toUpperCase(); // dung: gan ket qua lai
System.out.println(s); // HELLO
11. Bảng pitfall phổ biến với reference
| Pitfall | Code sai | Code đúng |
|---|---|---|
| Gọi method qua null | s.length() khi s == null | if (s != null) s.length() |
| Unbox null | Integer x = null; int y = x; | Kiểm tra x != null trước |
== cho wrapper | a == b với Integer a, b | a.equals(b) hoặc Objects.equals(a, b) |
== cho String | s1 == s2 để so nội dung | s1.equals(s2) |
| Quên String immutable | s.toUpperCase(); | s = s.toUpperCase(); |
| Null trong collection | list.add(null); list.get(0).length(); | Dùng Objects.requireNonNull hoặc kiểm tra null sau get |
12. Code example — tổng hợp
import java.util.*;
public class ReferenceDemo {
public static void main(String[] args) {
// --- Integer Cache demo ---
Integer a = 100, b = 100;
Integer c = 200, d = 200;
System.out.println("a == b (100): " + (a == b)); // true (cached)
System.out.println("c == d (200): " + (c == d)); // false (new objects)
System.out.println("c.equals(d): " + c.equals(d)); // true (value equal)
// --- NPE khi unbox null ---
Integer diemNull = null;
try {
int x = diemNull; // unbox null -> NPE
} catch (NullPointerException e) {
System.out.println("NPE khi unbox null: " + e.getMessage());
}
// --- Objects.requireNonNull ---
try {
String ten = null;
Objects.requireNonNull(ten, "ten khong duoc null");
} catch (NullPointerException e) {
System.out.println("requireNonNull: " + e.getMessage());
}
// --- Objects.equals null-safe ---
String s1 = null;
String s2 = "hello";
System.out.println("Objects.equals: " + Objects.equals(s1, s2)); // false, no NPE
}
}
Kết quả:
a == b (100): true
c == d (200): false
c.equals(d): true
NPE khi unbox null: Cannot invoke "java.lang.Integer.intValue()" because "diemNull" is null
requireNonNull: ten khong duoc null
Objects.equals: false
13. Deep Dive Oracle
- JLS §4.3 — Reference Types and Values: định nghĩa reference type, null type.
- JLS §5.1.7 — Boxing Conversion: Integer cache spec — yêu cầu cache −128..127, cho phép JVM cache rộng hơn.
- JLS §5.1.8 — Unboxing Conversion: unboxing null → NPE được spec hoá ở đây.
- Objects.requireNonNull API: fail-fast null check.
- Optional API: alternative to null for return types.
- JEP 358 — Helpful NullPointerExceptions: NPE message cải tiến từ Java 14.
14. Tự kiểm tra
- Q1Vì sao
Integer a = 100; Integer b = 100; a == bchotruenhưng với200chofalse? Spec Java đảm bảo điều gì? - Q2Đoạn code sau crash ở đâu? Vì sao?
Integer score = null; boolean pass = score >= 50; - Q3
Objects.equals(null, "hello")trả gì?Objects.equals(null, null)trả gì? - Q4Bạn có
List<Integer> scores. Dùng primitiveinthay wrapperInteger? Khi nào wrapper là bắt buộc? - Q5
String s = "hi"; s.concat(" there");— sau dòng nàyslà gì? Vì sao? - Q6Vì sao
new Integer(42)bị deprecated từ Java 9? Dùng gì thay?
Bài tiếp theo: Toán tử số học và so sánh — phép chia nguyên, modulo âm và bẫy ==
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên