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(layTenNguocDungc());
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, xoa tu Java 17
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();
flowchart LR
prim["int value"]
wrap["Integer object"]
cache{"value in range<br/>-128 to 127?"}
cached["return cached instance"]
newobj["create new Integer"]
prim -->|"autoboxing"| cache
cache -->|"yes"| cached
cache -->|"no"| newobj
cached --> wrap
newobj --> wrap
wrap -->|"unboxing<br/>intValue()"| primPitfall — 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
flowchart TD
val["Integer.valueOf(n)"]
check{"n in range<br/>-128 to 127?"}
cache["Return cached instance<br/>from Integer Cache array"]
newobj["Return new Integer(n)"]
val --> check
check -->|"yes"| cache
check -->|"no"| newobjKhô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 == b cho true nhưng với 200 cho false? Spec Java đảm bảo điều gì?▸
Integer a = 100; Integer b = 100; a == b cho true nhưng với 200 cho false? Spec Java đảm bảo điều gì?Integer cho range -128..127. Autobox Integer a = 100 gọi Integer.valueOf(100) → trả cùng instance cached → a == b so 2 reference cùng trỏ 1 object → true. Với 200 ngoài cache → mỗi lần tạo object mới → 2 reference khác nhau → false. Bài học: với wrapper, luôn dùng .equals().Q2Đoạn code sau crash ở đâu? Vì sao?Integer score = null;
boolean pass = score >= 50;
▸
Integer score = null;
boolean pass = score >= 50;NullPointerException. Toán tử >= chỉ định nghĩa trên primitive numeric → compiler phải unbox score thành int (score.intValue()). Gọi method trên null → NPE. Dòng 1 chỉ gán reference null không gây lỗi. Fix: kiểm tra if (score != null && score >= 50).Q3Objects.equals(null, "hello") trả gì? Objects.equals(null, null) trả gì?▸
Objects.equals(null, "hello") trả gì? Objects.equals(null, null) trả gì?Objects.equals(null, "hello") → false. Objects.equals(null, null) → true.
Implementation: return (a == b) || (a != null && a.equals(b)); — nếu a null thì nhánh đầu decide, không gọi a.equals() → không bao giờ NPE. Đây là lý do luôn ưu tiên Objects.equals() cho code null-safe, thay vì a.equals(b).
Q4Bạn có List<Integer> scores. Dùng primitive int hay wrapper Integer? Khi nào wrapper là bắt buộc?▸
List<Integer> scores. Dùng primitive int hay wrapper Integer? Khi nào wrapper là bắt buộc?List<Integer> buộc dùng wrapper — generic chỉ nhận reference type, không nhận primitive. Wrapper bắt buộc trong: Collections (List, Map, Set), generic type parameter, field nullable, reflection API, Optional. Với biến đơn hoặc mảng nguyên thuỷ (int[]) — dùng primitive cho hiệu năng (không có autobox/unbox, ít tốn heap).Q5String s = "hi"; s.concat(" there"); — sau dòng này s là gì? Vì sao?▸
String s = "hi"; s.concat(" there"); — sau dòng này s là gì? Vì sao?s vẫn là "hi". String là immutable — concat() không sửa object cũ, nó trả String mới "hi there". Nhưng kết quả không được gán vào biến nào → bị GC dọn. Fix: s = s.concat(" there"); hoặc String t = s.concat(" there");.Q6Vì sao new Integer(42) bị deprecated từ Java 9? Dùng gì thay?▸
new Integer(42) bị deprecated từ Java 9? Dùng gì thay?new Integer(42) bỏ qua Integer cache — luôn tạo object mới, kể cả với giá trị 0..127. Lãng phí heap + tạo object hết mức, chưa kể == giữa 2 new Integer(42) luôn false gây bug ngầm. Dùng Integer.valueOf(42) — tận dụng cache, đúng ngữ nghĩa autoboxing. Từ Java 9 new Integer(int) deprecated; Java 16+ đánh dấu forRemoval.Bài tiếp theo: Ép kiểu — widening, narrowing và type casting
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