🏷️ 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 Types | Reference Types |
|---|---|
| int, double, boolean | String, Array, Object, Class |
| Lưu giá trị trực tiếp | Lưu địa chỉ đến đối tượng |
| Stack memory | Heap memory |
| Giá trị mặc định: 0, false | Giá 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! 🏷️☕