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).
💡 💡 Khi nào dùng cách nào?
- 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"| newobj⚠️ Quy tắc vàng — wrapper và ==
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
ℹ️ 📚 Deep Dive Oracle — spec chính thức
- 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
- Vì sao
Integer a = 100; Integer b = 100; a == bchotruenhưng với200chofalse? Spec Java đảm bảo điều gì? - Đoạn code sau crash ở đâu? Vì sao?
Integer score = null; boolean pass = score >= 50; Objects.equals(null, "hello")trả gì?Objects.equals(null, null)trả gì?- Bạn có
List<Integer> scores. Dùng primitiveinthay wrapperInteger? Khi nào wrapper là bắt buộc? String s = "hi"; s.concat(" there");— sau dòng nàyslà gì? Vì sao?- Vì sao
new Integer(42)bị deprecated từ Java 9? Dùng gì thay?
Bài tiếp theo: Ép kiểu — widening, narrowing và type casting