Chuyển tới nội dung chính

🏷️ Kiểu Tham Chiếu - Reference Types

🎯 Món Ăn Hôm Nay

Khi quản lý quán café, bạn không chỉ làm việc với số nguyên (số ly) hay chuỗi (tên món) đơn thuần. Bạn cần quản lý đối tượng phức tạp: đơn hàng có nhiều thông tin, khách hàng có tên-địa chỉ-điện thoại, menu có danh sách món... Hôm nay chúng ta học về Kiểu Tham Chiếu - cách Java lưu trữ dữ liệu phức tạp!

☕ Reference Types Là Gì?

Reference Type là kiểu dữ liệu lưu trữ địa chỉ tham chiếu đến đối tượng trong bộ nhớ, thay vì lưu giá trị trực tiếp.

Sự khác biệt:

Primitive TypesReference Types
int, double, booleanString, Array, Object, Class
Lưu giá trị trực tiếpLưu địa chỉ đến đối tượng
Stack memoryHeap memory
Giá trị mặc định: 0, falseGiá trị mặc định: null

Tưởng tượng:

  • Primitive: Viết giá trị trực tiếp lên giấy: gia = 50000
  • Reference: Ghi địa chỉ kho hàng: donHang -> [Kho số 3, ngăn 12]

👨‍🍳 Câu Chuyện Trong Quán

Tình huống: Barista nhận đơn hàng

Primitive Type (giá trị đơn giản):

int soLuong = 2;  // Ghi trực tiếp: 2 ly

Reference Type (đối tượng phức tạp):

DonHang donHang = new DonHang();  // Tạo đơn hàng trong "kho"
donHang.tenMon = "Latte"; // Truy cập thông tin qua tham chiếu
donHang.soLuong = 2;

Biến donHang không lưu toàn bộ thông tin đơn hàng, mà chỉ lưu địa chỉ nơi đơn hàng được lưu trong bộ nhớ!

📝 Các Loại Reference Types

Ví Dụ 1: String - Kiểu Chuỗi

String là reference type đặc biệt:

public class QuanCafe {
public static void main(String[] args) {
// ☕ String là reference type
String tenMon1 = "Latte";
String tenMon2 = "Latte";
String tenMon3 = new String("Latte");

// So sánh địa chỉ (==)
System.out.println("tenMon1 == tenMon2: " + (tenMon1 == tenMon2)); // true
System.out.println("tenMon1 == tenMon3: " + (tenMon1 == tenMon3)); // false

// So sánh giá trị (.equals())
System.out.println("tenMon1.equals(tenMon3): " + tenMon1.equals(tenMon3)); // true

// Default value
String tenKhach = null;
System.out.println("Tên khách: " + tenKhach); // null
}
}

Output:

tenMon1 == tenMon2: true
tenMon1 == tenMon3: false
tenMon1.equals(tenMon3): true
Tên khách: null

Ví Dụ 2: Array - Mảng

Array là reference type:

public class QuanCafe {
public static void main(String[] args) {
// 📦 Tạo mảng - reference type
int[] gia = {45000, 50000, 55000};
String[] menu = {"Espresso", "Latte", "Cappuccino"};

// Gán tham chiếu
int[] giaBackup = gia; // Cùng tham chiếu đến một mảng

// Thay đổi qua giaBackup
giaBackup[0] = 40000;

// gia cũng thay đổi!
System.out.println("gia[0]: " + gia[0]); // 40000

// Default value
String[] danhSachCho = new String[3];
System.out.println("Khách 1: " + danhSachCho[0]); // null
}
}

Output:

gia[0]: 40000
Khách 1: null

Ví Dụ 3: Class - Lớp Tự Định Nghĩa

Tạo class riêng:

// 📋 Định nghĩa class DonHang
class DonHang {
String tenMon;
int soLuong;
int donGia;

void hienThi() {
System.out.printf("☕ %s x%d = %,d VND%n",
tenMon, soLuong, soLuong * donGia);
}
}

public class QuanCafe {
public static void main(String[] args) {
// Tạo đối tượng - reference type
DonHang don1 = new DonHang();
don1.tenMon = "Latte";
don1.soLuong = 2;
don1.donGia = 50000;

// Gán tham chiếu
DonHang don2 = don1; // Cùng tham chiếu

// Thay đổi qua don2
don2.soLuong = 3;

// don1 cũng thay đổi!
don1.hienThi(); // 3 ly
don2.hienThi(); // 3 ly
}
}

Output:

☕ Latte x3 = 150,000 VND
☕ Latte x3 = 150,000 VND

Ví Dụ 4: Wrapper Classes

Wrapper class cho primitive types:

public class QuanCafe {
public static void main(String[] args) {
// Primitive vs Wrapper
int soLuong = 5; // Primitive
Integer soLuongObj = 5; // Reference (Wrapper)

// Wrapper có methods hữu ích
String giaTxt = "50000";
int gia = Integer.parseInt(giaTxt); // String -> int
System.out.println("Giá: " + gia + " VND");

// Autoboxing & Unboxing
Integer a = 10; // Autoboxing: int -> Integer
int b = a; // Unboxing: Integer -> int

// null chỉ dùng cho reference types
Integer soLy = null;
// int x = null; // ❌ Lỗi biên dịch!

System.out.println("Số ly: " + soLy); // null
}
}

Output:

Giá: 50000 VND
Số ly: null

Ví Dụ 5: ArrayList - Danh Sách Động

ArrayList là reference type:

import java.util.ArrayList;

public class QuanCafe {
public static void main(String[] args) {
// 📋 Tạo danh sách động
ArrayList<String> danhSachCho = new ArrayList<>();

// Thêm khách
danhSachCho.add("Anh Minh");
danhSachCho.add("Chị Lan");
danhSachCho.add("Anh Tuấn");

// Gán tham chiếu
ArrayList<String> backup = danhSachCho;

// Thay đổi qua backup
backup.add("Chị Hoa");

// danhSachCho cũng thay đổi!
System.out.println("Số khách: " + danhSachCho.size()); // 4

// Hiển thị danh sách
for (String khach : danhSachCho) {
System.out.println("👤 " + khach);
}
}
}

Output:

Số khách: 4
👤 Anh Minh
👤 Chị Lan
👤 Anh Tuấn
👤 Chị Hoa

Ví Dụ 6: null - Giá Trị Đặc Biệt

null chỉ dùng cho reference types:

public class QuanCafe {
public static void main(String[] args) {
// ✅ Reference types có thể là null
String tenKhach = null;
DonHang donHang = null;
int[] gia = null;

// ❌ NullPointerException nếu truy cập
try {
System.out.println(tenKhach.length()); // ❌ Lỗi!
} catch (NullPointerException e) {
System.out.println("❌ Lỗi: Tên khách là null!");
}

// ✅ Kiểm tra null trước khi dùng
if (tenKhach != null) {
System.out.println("Tên: " + tenKhach);
} else {
System.out.println("💡 Chưa có tên khách");
}

// Primitive KHÔNG thể null
// int soLuong = null; // ❌ Lỗi biên dịch!
Integer soLuong = null; // ✅ OK
}
}

Output:

❌ Lỗi: Tên khách là null!
💡 Chưa có tên khách

🔥 Thực Hành Trong Quán

Bài Tập 1: So Sánh Đúng Cách

Viết code so sánh 2 String đúng cách.

String mon1 = "Latte";
String mon2 = new String("Latte");

// ❌ SAI - So sánh địa chỉ
if (mon1 == mon2) {
System.out.println("Giống nhau");
}

// ✅ ĐÚNG - So sánh giá trị
if (mon1.equals(mon2)) {
System.out.println("✅ Cùng món!");
}

Bài Tập 2: Clone Array

Tạo bản sao thực sự của mảng.

int[] giaGoc = {45000, 50000, 55000};

// ❌ SAI - Chỉ copy tham chiếu
int[] giaCopy1 = giaGoc;
giaCopy1[0] = 40000;
System.out.println(giaGoc[0]); // 40000 - Thay đổi cả gốc!

// ✅ ĐÚNG - Clone thực sự
int[] giaCopy2 = giaGoc.clone();
giaCopy2[0] = 35000;
System.out.println(giaGoc[0]); // 45000 - Không ảnh hưởng gốc

Bài Tập 3: Kiểm Tra null An Toàn

Viết phương thức in tên món an toàn.

public static void inTenMon(String tenMon) {
// ✅ Kiểm tra null
if (tenMon == null) {
System.out.println("⚠️ Chưa có tên món");
return;
}

// ✅ Kiểm tra chuỗi rỗng
if (tenMon.trim().isEmpty()) {
System.out.println("⚠️ Tên món trống");
return;
}

System.out.println("☕ Món: " + tenMon);
}

// Test
inTenMon(null); // ⚠️ Chưa có tên món
inTenMon(""); // ⚠️ Tên món trống
inTenMon("Latte"); // ☕ Món: Latte

⚠️ Những Lỗi Đầu Bếp Thường Gặp

Lỗi 1: Dùng == Thay Vì equals()

SAI:

String mon1 = new String("Latte");
String mon2 = new String("Latte");

if (mon1 == mon2) { // ❌ So sánh địa chỉ - false!
System.out.println("Giống nhau");
}

ĐÚNG:

if (mon1.equals(mon2)) {  // ✅ So sánh giá trị - true!
System.out.println("✅ Giống nhau");
}

Lỗi 2: Không Kiểm Tra null

SAI:

String tenKhach = null;
System.out.println(tenKhach.toUpperCase()); // ❌ NullPointerException!

ĐÚNG:

String tenKhach = null;
if (tenKhach != null) {
System.out.println(tenKhach.toUpperCase());
} else {
System.out.println("💡 Khách vãng lai");
}

Lỗi 3: Nghĩ Rằng Gán = Copy

SAI:

int[] gia1 = {50000};
int[] gia2 = gia1; // ❌ Không phải copy!

gia2[0] = 40000;
System.out.println(gia1[0]); // 40000 - gia1 cũng đổi!

ĐÚNG:

int[] gia1 = {50000};
int[] gia2 = gia1.clone(); // ✅ Clone thật

gia2[0] = 40000;
System.out.println(gia1[0]); // 50000 - Không ảnh hưởng

Lỗi 4: Gán null Cho Primitive

SAI:

int soLuong = null;  // ❌ Lỗi biên dịch!

ĐÚNG:

Integer soLuong = null;  // ✅ Dùng wrapper class

Lỗi 5: So Sánh null Với equals()

SAI:

String ten = null;
if (ten.equals("Latte")) { // ❌ NullPointerException!
// ...
}

ĐÚNG:

String ten = null;
if ("Latte".equals(ten)) { // ✅ An toàn với null
// ...
}
// Hoặc
if (ten != null && ten.equals("Latte")) {
// ...
}

💡 Bí Quyết Của Barista

1. So Sánh Reference Types

// ❌ Dùng == - So sánh địa chỉ
if (str1 == str2) { }

// ✅ Dùng equals() - So sánh giá trị
if (str1.equals(str2)) { }

// ✅ An toàn với null
if ("Latte".equals(tenMon)) { }

2. Kiểm Tra null

// Pattern 1: if-null
if (obj != null) {
obj.method();
}

// Pattern 2: Early return
if (obj == null) {
return;
}
obj.method();

// Pattern 3: Default value
String ten = (tenKhach != null) ? tenKhach : "Khách vãng lai";

3. Clone vs Reference

// Reference - Cùng đối tượng
int[] arr2 = arr1;

// Shallow copy - Copy mảng cấp 1
int[] arr2 = arr1.clone();

// Deep copy - Copy đệ quy (cho object)
DonHang don2 = new DonHang(don1.tenMon, don1.soLuong);

4. Wrapper Classes

// Primitive -> Wrapper
int x = 5;
Integer xObj = Integer.valueOf(x); // Tường minh
Integer yObj = x; // Autoboxing

// Wrapper -> Primitive
Integer aObj = 10;
int a = aObj.intValue(); // Tường minh
int b = aObj; // Unboxing

// Parse String
int gia = Integer.parseInt("50000");
double giaDouble = Double.parseDouble("49.99");

5. Immutable vs Mutable

// String - Immutable (không thay đổi được)
String s1 = "Latte";
String s2 = s1.toUpperCase(); // Tạo String mới
System.out.println(s1); // "Latte" - không đổi

// StringBuilder - Mutable (thay đổi được)
StringBuilder sb = new StringBuilder("Latte");
sb.append(" Grande"); // Thay đổi trực tiếp
System.out.println(sb); // "Latte Grande"

🎓 Bạn Đã Học Được

  • Reference Type: Lưu địa chỉ, không phải giá trị
  • Các loại: String, Array, Class, ArrayList, Wrapper
  • null: Giá trị đặc biệt chỉ cho reference types
  • So sánh: Dùng equals() thay vì ==
  • Clone: arr.clone() để copy mảng
  • NullPointerException: Kiểm tra null trước khi dùng
  • Wrapper Classes: Integer, Double, Boolean...

☕ Món Tiếp Theo

Bài tiếp theo: 📝 Xử Lý Chuỗi - String Methods

Học cách xử lý tên món, ghi chú, địa chỉ khách hàng!


💡 Lời Khuyên Cuối: Reference types là nền tảng của lập trình hướng đối tượng. Hiểu rõ sự khác biệt giữa primitive và reference sẽ giúp bạn tránh được rất nhiều bug khó hiểu. Luôn nhớ: so sánh bằng equals(), kiểm tra null, và clone khi cần! 🏷️☕