Java Foundations/Kiểu tham chiếu — null, NPE, wrapper class và autoboxing
10/35
Bài 10 / 35~18 phútCú pháp Java & Kiểu dữ liệuMiễn phí lượt xem

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

Tự kiểm tra
Q1
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ì?
JLS §5.1.7 yêu cầu JVM cache instance 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;
Crash ở dòng 2 với 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).
Q3
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).

Q4
Bạn 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).
Q5
String s = "hi"; s.concat(" there"); — sau dòng này s là gì? Vì sao?
s vẫn là "hi". Stringimmutableconcat() 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");.
Q6
Vì sao 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

Đặt 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