Java Foundations
12/39
Bài 12 / 39~13 phútCú pháp Java & Kiểu dữ liệuLộ trình · chặng 02/21Miễn phí… lượt xem

Kiểu tham chiếu — null, NPE, wrapper class và autoboxing

Khám phá kiểu tham chiếu trong Java, cơ chế null, NullPointerException, wrapper class, autoboxing và giải mã cạm bẫy Integer Cache kinh điển.

TL;DR: Kiểu tham chiếu (reference type) trong Java lưu trữ địa chỉ trỏ tới đối tượng trên Heap. Biến mang giá trị null khi không trỏ tới đối tượng nào và sẽ ném NullPointerException (NPE) nếu truy cập phương thức/thuộc tính qua null. Wrapper class bọc primitive để dùng trong Collection/Generic, đi kèm cơ chế Autoboxing/Unboxing tự động. Luôn cảnh giác với Integer Cache (-128 đến 127) và luôn dùng .equals() để so sánh giá trị thay vì toán tử ==.

Bài 1: Biến và khai báo 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.

Vì sao thẻ gửi xe là ẩn dụ chính xác nhất cho kiểu tham chiếu?

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.

Java hỗ trợ những nhóm kiểu tham chiếu phổ biến nào?

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
Java
String ten = "An";           // class type
int[] so   = {1, 2, 3};     // array type (int[] is reference)
List<String> ds = new ArrayList<>();  // interface type

Giá trị null mang bản chất gì và kích thước ra sao trong bộ nhớ?

null là giá trị đặc biệt có nghĩa: "reference này không trỏ tới object nào".

Java
String s = null;  // variable s exists but points to no object

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).
Java
String s = null;
int n = null;   // COMPILE ERROR -- primitive cannot be null

NullPointerException phát sinh từ những tình huống kinh điển nào?

Gọi bất kỳ method hoặc truy cập field qua null reference → NullPointerException (NPE):

Java
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?

Java
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.

Thử ngẫmnhớ lại NullPointerException gần nhất bạn tự debug — nó xảy ra ở việc gọi method qua null, hay đọc phần tử một mảng null? Phân biệt được hai ca đó giúp gì khi đọc stack trace lần sau?

Bốn chiến lược hiệu quả nào giúp phòng ngừa lỗi NullPointerException?

5.1 Kiểm tra null trước khi dùng

Java
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õ

Java
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

Java
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)

Java
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).

💡 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)

Tại sao Java cần wrapper class để bọc các kiểu nguyên thủy?

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 primitive — List<int> là lỗi compile; phải dùng List<Integer>.
  2. Generic không work với primitive — Map<String, int> sai; phải Map<String, Integer>.
  3. Primitive không có method — 42.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).
Java
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

Java
// 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

Cơ chế autoboxing và unboxing hoạt động ra sao và tiềm ẩn rủi ro gì?

Java tự động chuyển đổi giữa primitive và wrapper — gọi là autoboxing (primitive → wrapper) và unboxing (wrapper → primitive):

Java
// 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();

Hai dòng gán Integer và int được compiler dịch thành lời gọi Integer.valueOf và intValue, rồi rẽ nhánh theo việc biến có trỏ tới object không: nếu null thì intValue ném NullPointerException

Pitfall — unbox null → NPE

Java
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.

Java
// Vi du thuc te: 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

Java
// 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;
}

Thử ngẫmbạn thấy một vòng lặp cộng dồn hàng triệu lần dùng biến Integer thay vì int — theo cơ chế autobox vừa học, bạn giải thích cho đồng nghiệp chỗ chậm nằm ở đâu thế nào?

Cơ chế Integer Cache tạo ra cạm bẫy so sánh bằng toán tử == như thế nào?

Đây là một trong những câu hỏi phỏng vấn Java kinh điển nhất:

Java
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.

Java
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

Integer.valueOf rẽ hai nhánh theo range cache: trong khoảng âm 128 tới 127 thì trả cùng một instance nên == cho true, ngoài khoảng thì tạo object mới nên == cho false, cả hai nhánh đều dẫn tới kết luận dùng equals

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().

Java
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.

Khi nào nên dùng kiểu nguyên thủy và khi nào bắt buộc dùng wrapper class?

Tình huốngDùng gìLý do
Biến local, tính toán✅ int, doubleNhanh, không GC overhead
Collection: List, Map, Set✅ Integer, 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, accumulation✅ intTránh boxing overhead
So sánh bằng ==❌ WrapperDùng .equals()

Tại sao String là kiểu tham chiếu đặc biệt có tính bất biến?

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):

Java
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ũ:

Java
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

Thử ngẫmmột hàm xử lý hàng loạt chuỗi giống nhau đọc từ file log — gọi .intern() trên từng chuỗi trước khi lưu vào danh sách có lợi gì về memory, và trả giá gì đổi lại?

Những lỗi sai phổ biến nào thường gặp khi thao tác với biến tham chiếu?

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

Mã nguồn mẫu minh họa tổng hợp các tình huống tham chiếu hoạt động ra sao?

Java
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

Tài liệu đặc tả Java nào định nghĩa cơ chế wrapper và Integer Cache?

📚 Deep Dive Oracle — spec chính thức

Câu hỏi tự kiểm tra giúp củng cố kiến thức như thế nào?

Tự kiểm tra
0/6 câu đã trả lời
  1. 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ì?
  2. Q2
    Đoạn code sau crash ở đâu? Vì sao?
    Integer score = null;
    boolean pass = score >= 50;
  3. Q3
    Objects.equals(null, "hello") trả gì? Objects.equals(null, null) trả gì?
  4. Q4
    Bạn có List<Integer> scores. Dùng primitive int hay wrapper Integer? Khi nào wrapper là bắt buộc?
  5. Q5
    String s = "hi"; s.concat(" there"); — sau dòng này s là gì? Vì sao?
  6. Q6
    Vì 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 có giúp bạn hiểu bản chất không?

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.

Lộ trình Junior Java Backend Developer

Chặng 2/21 · Lập trình từ số 0 với Java — bài 12/32 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

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 ==