Java — Từ Zero đến Senior/Cú pháp Java & Kiểu dữ liệu/Kiểu tham chiếu — null, NPE, wrapper class và autoboxing
3/8
~18 phútCú pháp Java & Kiểu dữ liệu

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ómVí dụGhi chú
Class typesString, Integer, ArrayList, class tự viếtPhổ biến nhất
Interface typesComparable, Runnable, ListBiến kiểu interface
Array typesint[], String[], int[][]Mảng là reference
Enum typesDay.MONDAY, Status.ACTIVEEnum 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).
  • null có thể gán cho bất kỳ reference type nào.
  • Kích thước null trong 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:

PrimitiveWrapper classGhi chú
byteByte
shortShort
intIntegerPhổ biến nhất
longLong
floatFloat
doubleDouble
booleanBoolean
charCharacter

Vì sao cần wrapper?

  1. Collection không chứa primitiveList<int> là lỗi compile; phải dùng List<Integer>.
  2. Generic không work với primitiveMap<String, int> sai; phải Map<String, Integer>.
  3. Primitive không có method42.toString() không hợp lệ; Integer.toString(42) hoặc String.valueOf(42) thì có.
  4. 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()"| prim

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()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)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ốngDùng gìLý do
Biến local, tính toánint, doubleNhanh, không GC overhead
Collection: List, Map, SetInteger, DoubleGeneric 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✅ WrapperPhân biệt 0 với "chưa có giá trị"
Vòng lặp, accumulationintTránh boxing overhead
So sánh bằng ==❌ WrapperDù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

PitfallCode saiCode đúng
Gọi method qua nulls.length() khi s == nullif (s != null) s.length()
Unbox nullInteger x = null; int y = x;Kiểm tra x != null trước
== cho wrappera == b với Integer a, ba.equals(b) hoặc Objects.equals(a, b)
== cho Strings1 == s2 để so nội dungs1.equals(s2)
Quên String immutables.toUpperCase();s = s.toUpperCase();
Null trong collectionlist.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

14. Tự kiểm tra

  1. Vì 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ì?
  2. Đoạn code sau crash ở đâu? Vì sao?
    Integer score = null;
    boolean pass = score >= 50;
    
  3. Objects.equals(null, "hello") trả gì? Objects.equals(null, null) trả gì?
  4. Bạn có List<Integer> scores. Dùng primitive int hay wrapper Integer? Khi nào wrapper là bắt buộc?
  5. String s = "hi"; s.concat(" there"); — sau dòng này s là gì? Vì sao?
  6. 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