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

Class và Object

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Hiểu sự khác biệt giữa class (blueprint) và object (instance)
  • Khai báo class với fields và methods, và tạo objects bằng từ khóa new
  • Truy cập fields và methods của object thông qua dot operator
  • Kiểm tra và xử lý null reference để tránh NullPointerException
  • Hiểu cách Java quản lý memory với stack (references) và heap (objects), và garbage collection

Bài trước: Giới thiệu Lập trình Hướng đối tượng — Đã học về 4 tính chất cơ bản của OOP. Bài này sẽ tìm hiểu chi tiết về class và object - nền tảng của OOP trong Java.

Class là gì?

Class là một blueprint (bản thiết kế) hoặc template (khuôn mẫu) dùng để tạo ra các objects.

Ẩn dụ thực tế

Hãy nghĩ về bản vẽ thiết kế nhà:

  • Class: Bản vẽ kiến trúc (chỉ có trên giấy)
  • Object: Ngôi nhà thực tế được xây dựng từ bản vẽ
  • Từ một bản vẽ, bạn có thể xây nhiều ngôi nhà giống nhau

Đặc điểm của Class

  1. Class chỉ là mô tả, không chiếm bộ nhớ cho data
  2. Class định nghĩa:
    • Fields (thuộc tính/biến): Dữ liệu mà object sẽ chứa
    • Methods (phương thức): Hành vi mà object có thể thực hiện
  3. Class là type: Bạn có thể khai báo biến với kiểu là class

Cú pháp khai báo Class

[access_modifier] class ClassName {
// Fields (instance variables)
dataType fieldName1;
dataType fieldName2;

// Methods
returnType methodName(parameters) {
// method body
}
}

Ví dụ: Class Student

public class Student {
// Fields (attributes/properties)
String name;
int age;
String studentId;
double gpa;

// Method (behavior)
void study() {
System.out.println(name + " is studying...");
}

void displayInfo() {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
System.out.println("Student ID: " + studentId);
System.out.println("GPA: " + gpa);
}
}

Object là gì?

Object là một instance (thể hiện) cụ thể được tạo ra từ class. Object có:

  1. State (trạng thái): Giá trị của các fields
  2. Behavior (hành vi): Các methods mà object có thể gọi
  3. Identity (danh tính): Object được lưu tại một vị trí duy nhất trong memory

Vòng đời Object (Object Lifecycle)

Mỗi object trong Java trải qua các giai đoạn sau:

📖 Theo JLS §12.5 (Creation of New Class Instances)

Java Language Specification định nghĩa rõ ràng quá trình tạo object:

  1. Cấp phát bộ nhớ trên heap
  2. Khởi tạo fields với giá trị mặc định
  3. Thực thi constructor
  4. Trả về reference đến object mới tạo

1. Creation (Tạo object)

Student student = new Student(); // Toán tử 'new' cấp phát bộ nhớ trên heap

2. In-Use (Đang sử dụng)

student.setName("An");
student.study();
// Object đang được sử dụng, có ít nhất 1 reference trỏ đến nó

3. Invisible (Không truy cập được)

void method() {
Student student = new Student();
student.setName("An");
} // Kết thúc method, biến 'student' ra khỏi scope
// Object vẫn còn trong heap nhưng không còn reference nào trỏ đến

4. Unreachable (Không thể truy cập - Eligible for GC)

Student s1 = new Student(); // Object A
Student s2 = new Student(); // Object B
s1 = s2; // Object A không còn reference → eligible for GC
s1 = null; // Object B có s2 trỏ đến → vẫn còn dùng
s2 = null; // Object B không còn reference → eligible for GC

5. Collected (Thu hồi bởi GC)

  • Garbage Collector tự động thu hồi bộ nhớ
  • Không thể dự đoán chính xác khi nào GC chạy

6. Finalized (Hoàn tất - deprecated)

  • Method finalize() được gọi trước khi thu hồi (deprecated từ Java 9)
  • Không nên sử dụng finalize() trong code hiện đại

7. Deallocated (Giải phóng hoàn toàn)

  • Bộ nhớ được trả lại cho heap
  • Object không còn tồn tại
Ví dụ vòng đời đầy đủ
public class ObjectLifecycleDemo {
public static void main(String[] args) {
// 1. Creation
Student student = new Student("An", 20);
System.out.println("Object created: " + student);

// 2. In-Use
student.study();
student.displayInfo();

// 3. Unreachable - đủ điều kiện GC
student = null; // Không còn reference

// 4. Gợi ý GC chạy (không bắt buộc)
System.gc();

System.out.println("Program continues...");
}
}
Khái niệm quan trọng
  • Class: Định nghĩa CÁI GÌ (what)
  • Object: Thể hiện cụ thể (actual instance)
  • Một class có thể tạo ra vô số objects

Tạo Object với từ khóa new

ClassName objectName = new ClassName();

Giải thích:

  • ClassName: Kiểu dữ liệu (class type)
  • objectName: Tên biến tham chiếu đến object
  • new: Toán tử cấp phát bộ nhớ cho object
  • ClassName(): Constructor (sẽ học ở bài sau)

Ví dụ: Tạo Student objects

public class Main {
public static void main(String[] args) {
// Tạo object student1
Student student1 = new Student();
student1.name = "Nguyễn Văn An";
student1.age = 20;
student1.studentId = "SV001";
student1.gpa = 3.5;

// Tạo object student2
Student student2 = new Student();
student2.name = "Trần Thị Bình";
student2.age = 21;
student2.studentId = "SV002";
student2.gpa = 3.8;

// Gọi methods
student1.displayInfo();
System.out.println("---");
student2.displayInfo();
}
}

Output:

Name: Nguyễn Văn An
Age: 20
Student ID: SV001
GPA: 3.5
---
Name: Trần Thị Bình
Age: 21
Student ID: SV002
GPA: 3.8

Truy cập Fields và Methods

Dot Operator (.)

Sử dụng dấu chấm (.) để truy cập members (fields và methods) của object.

objectName.fieldName      // Truy cập field
objectName.methodName() // Gọi method

Ví dụ: BankAccount

class BankAccount {
// Fields
String accountNumber;
String ownerName;
double balance;

// Methods
void deposit(double amount) {
balance += amount;
System.out.println("Deposited: $" + amount);
}

void withdraw(double amount) {
if (amount <= balance) {
balance -= amount;
System.out.println("Withdrawn: $" + amount);
} else {
System.out.println("Insufficient funds!");
}
}

void checkBalance() {
System.out.println("Current balance: $" + balance);
}
}

public class BankingApp {
public static void main(String[] args) {
// Tạo object
BankAccount account = new BankAccount();

// Truy cập fields
account.accountNumber = "ACC123456";
account.ownerName = "Nguyễn Văn A";
account.balance = 1000.0;

// Gọi methods
account.checkBalance(); // Current balance: $1000.0
account.deposit(500.0); // Deposited: $500.0
account.withdraw(300.0); // Withdrawn: $300.0
account.checkBalance(); // Current balance: $1200.0
}
}
Lưu ý

Việc truy cập trực tiếp vào fields từ bên ngoài class là bad practice. Chúng ta sẽ học cách bảo vệ fields bằng encapsulation ở bài sau.

Null Reference

null là gì?

null là một giá trị đặc biệt trong Java, nghĩa là "không tham chiếu đến object nào".

Student student = null; // Biến student không trỏ đến object nào

NullPointerException

Nếu bạn gọi method hoặc truy cập field trên một null reference, Java sẽ ném ra NullPointerException.

Student student = null;
student.displayInfo(); // ❌ NullPointerException!

Kiểm tra null trước khi sử dụng

Student student = null;

if (student != null) {
student.displayInfo(); // ✅ An toàn
} else {
System.out.println("Student is null!");
}
Best Practice

Luôn kiểm tra null trước khi truy cập object, đặc biệt khi nhận object từ method khác hoặc từ user input.

Toán tử == và method equals() — Hiểu đúng từ gốc

Một trong những bẫy OCP phổ biến nhất là nhầm lẫn giữa ==equals(). Để hiểu đúng, cần phân biệt 3 loại so sánh mà JLS định nghĩa cho toán tử ==.

📖 Theo JLS §15.21 — Equality Operators

Java Language Specification chia toán tử ==!= thành 3 loại riêng biệt tùy theo kiểu dữ liệu:

  1. §15.21.1 — Numerical Equality: So sánh giá trị số (byte, short, int, long, float, double, char)
  2. §15.21.2 — Boolean Equality: So sánh giá trị boolean
  3. §15.21.3 — Reference Equality: So sánh tham chiếu (địa chỉ object)

Tham khảo: Oracle Java Tutorial — Equality Operators

Phần 1: == với kiểu nguyên thủy (Primitive Types)

Với primitive types, == so sánh giá trị thực tế được lưu trực tiếp trên stack. Đây là cách so sánh đơn giản và trực quan nhất.

Numerical Equality (JLS §15.21.1)

// So sánh số nguyên — trực tiếp so giá trị
int a = 10;
int b = 10;
System.out.println(a == b); // true — cùng giá trị 10

// So sánh khác kiểu — Java tự động promotion (JLS §5.6.2)
int x = 5;
double y = 5.0;
System.out.println(x == y); // true — int 5 được promote thành double 5.0

// char là numeric type — so sánh bằng Unicode code point
char c = 'A';
int n = 65;
System.out.println(c == n); // true — 'A' có code point là 65

byte b1 = 10;
short s1 = 10;
System.out.println(b1 == s1); // true — cả hai promote lên int 10
🔥 Bẫy OCP: == với floatdouble (IEEE 754)

Theo JLS §15.21.1, phép so sánh == trên floating-point tuân theo chuẩn IEEE 754 — có những kết quả phản trực giác:

// 1. NaN không bằng chính nó!
double nan = Double.NaN;
System.out.println(nan == nan); // false! (JLS: NaN is unordered)
System.out.println(nan != nan); // true!
System.out.println(Double.isNaN(nan)); // true — dùng method này thay thế

// 2. Dương zero == Âm zero
System.out.println(0.0 == -0.0); // true (IEEE 754 quy định)
System.out.println(Double.compare(0.0, -0.0)); // 0 — cũng bằng nhau

// 3. Floating-point precision
System.out.println(0.1 + 0.2 == 0.3); // false! (0.30000000000000004)

// ✅ So sánh float/double đúng cách: dùng epsilon
double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 1e-10;
System.out.println(Math.abs(a - b) < epsilon); // true

Quy tắc OCP:

  • NaN == NaN luôn false — dùng Double.isNaN() để kiểm tra
  • 0.0 == -0.0 luôn true
  • Không bao giờ dùng == để so sánh kết quả tính toán float/double

Boolean Equality (JLS §15.21.2)

boolean flag1 = true;
boolean flag2 = true;
boolean flag3 = false;

System.out.println(flag1 == flag2); // true — cùng giá trị true
System.out.println(flag1 == flag3); // false — true ≠ false
System.out.println(flag1 != flag3); // true
💡 Cách nhớ: == với primitives

Primitives lưu GIÁ TRỊ trực tiếp trên stack== so sánh giá trị đó.

Ẩn dụ: Như so sánh 2 tờ tiền. Tờ 100.000đ trong túi bạn và tờ 100.000đ trong túi bạn của bạn — cùng giá trị== trả về true. Không cần quan tâm đó là "tờ tiền nào" (identity), chỉ cần giá trị bằng nhau.

Phần 2: == với kiểu tham chiếu (Reference Types) — JLS §15.21.3

Với reference types (objects), == KHÔNG so sánh nội dung — nó so sánh địa chỉ bộ nhớ (có phải cùng 1 object không).

Student s1 = new Student();
s1.name = "An";
s1.age = 20;

Student s2 = new Student();
s2.name = "An";
s2.age = 20;

Student s3 = s1; // s3 trỏ đến cùng object với s1

System.out.println(s1 == s2); // false — 2 objects khác nhau trên heap
System.out.println(s1 == s3); // true — cùng trỏ đến 1 object
💡 Cách nhớ: == với references

References lưu ĐỊA CHỈ trên stack, object thật ở heap → == so sánh địa chỉ đó.

Ẩn dụ: Như so sánh 2 địa chỉ nhà. Hai người ở cùng nội dung (cùng kiểu nhà, cùng đồ đạc) nhưng ở khác địa chỉ== trả về false. Chỉ khi hai người cùng sống 1 nhà (cùng địa chỉ) → == mới true.

Phần 3: equals() — So sánh nội dung (Value Equality)

equals() là method của Object class, được thiết kế để so sánh nội dung (nếu override đúng cách).

Mặc định: Object.equals() hoạt động giống hệt == (so sánh reference). Chỉ khi bạn override nó thì mới so sánh nội dung.

// String đã override equals() → so sánh nội dung
String str1 = new String("Hello");
String str2 = new String("Hello");
String str3 = str1;

System.out.println(str1 == str2); // false — khác object
System.out.println(str1 == str3); // true — cùng object
System.out.println(str1.equals(str2)); // true — cùng nội dung "Hello"

// Java 7+: Objects.equals() — null-safe comparison
import java.util.Objects;

String a = null;
String b = "Hello";

// a.equals(b) → NullPointerException!
Objects.equals(a, b); // false (no exception!)
Objects.equals(null, null); // true

Phần 4: Bẫy OCP kinh điển

🔥 Bẫy OCP: String Literal vs new String()
String s1 = "Hello";                   // String pool (intern)
String s2 = "Hello"; // Tái sử dụng từ pool → cùng object!
String s3 = new String("Hello"); // Heap (object mới, bỏ qua pool)
String s4 = s3.intern(); // Trả về reference từ pool

System.out.println(s1 == s2); // true — cùng object trong pool
System.out.println(s1 == s3); // false — s3 là object mới trên heap
System.out.println(s1 == s4); // true — intern() trả về pool reference
System.out.println(s1.equals(s3)); // true — cùng nội dung "Hello"
🔥 Bẫy OCP: Integer Cache (-128 đến 127)

Đây là bẫy nguy hiểm nhất vì kết quả thay đổi tùy theo giá trị số:

// Autoboxing: Integer.valueOf() được gọi ngầm
Integer a = 100; // Integer.valueOf(100) → lấy từ cache
Integer b = 100; // Integer.valueOf(100) → cùng object từ cache!
Integer c = 200; // Integer.valueOf(200) → tạo object MỚI (ngoài cache)
Integer d = 200; // Integer.valueOf(200) → tạo object MỚI khác

System.out.println(a == b); // true! — cùng cached object (100 trong -128..127)
System.out.println(c == d); // false! — khác object (200 ngoài cache range)

// ✅ LUÔN dùng equals() cho wrapper types
System.out.println(a.equals(b)); // true
System.out.println(c.equals(d)); // true

Tại sao? Integer.valueOf() cache các giá trị từ -128 đến 127 (JLS §5.1.7). Trong range này, autoboxing trả về cùng object → ==true. Ngoài range → tạo object mới → ==false.

Áp dụng tương tự cho: Byte, Short, Long (cache -128 đến 127), Character (cache 0 đến 127), Boolean (cache TRUEFALSE).

🔥 Bẫy OCP: Autoboxing + Unboxing + ==
// Trộn primitive và wrapper — Java auto-unbox wrapper thành primitive
int primitive = 100;
Integer wrapper = 100; // autoboxed

System.out.println(primitive == wrapper); // true! — wrapper unbox thành int, so sánh giá trị
System.out.println(wrapper.equals(primitive)); // true — primitive autobox, so sánh nội dung

// ⚠️ NGUY HIỂM: Unboxing null → NullPointerException!
Integer nullWrapper = null;
// System.out.println(nullWrapper == 0); // ❌ NullPointerException! (unbox null)
// System.out.println(nullWrapper == primitive); // ❌ NullPointerException!

// ✅ An toàn: null check trước
if (nullWrapper != null && nullWrapper == 0) {
System.out.println("Zero");
}

Bảng tổng hợp: Khi nào dùng ==, khi nào dùng equals()

Trường hợpDùng ==Dùng equals()Giải thích
int, double, char... (primitives)✅ Luôn dùng❌ Không cóPrimitives so sánh giá trị
boolean (primitive)✅ Luôn dùng❌ Không cóSo sánh true/false
Kiểm tra null✅ Luôn dùng❌ NPE nếu nullobj == null an toàn
String❌ Tránh dùng✅ Luôn dùngPool có thể gây nhầm
Integer, Long... (wrappers)❌ Tránh dùng✅ Luôn dùngCache gây kết quả bất ngờ
Custom objects❌ Trừ khi cố ý✅ Sau khi overrideCần override equals + hashCode
Enum✅ Dùng được✅ Cũng OKEnum là singleton, == an toàn
💡 Cách nhớ: Quy tắc vàng

"Primitive dùng ==, Object dùng equals()"

Ngoại lệ duy nhất: enum — vì mỗi enum constant là singleton (chỉ có 1 instance), nên ==equals() cho cùng kết quả. Nhiều style guides khuyến khích dùng == cho enum vì nhanh hơn và null-safe.

enum Color { RED, GREEN, BLUE }

Color c1 = Color.RED;
Color c2 = Color.RED;
System.out.println(c1 == c2); // true — singleton
System.out.println(c1.equals(c2)); // true — cũng OK
System.out.println(c1 == null); // false — an toàn, không NPE
// c1.equals(null); // false — cũng OK nhưng thừa

Ví dụ: Override equals() cho custom class

class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

// Override equals() để so sánh nội dung
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // Cùng reference
if (obj == null || getClass() != obj.getClass()) return false;

Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}

@Override
public int hashCode() {
// Java 7+: Objects.hash() — tiện lợi hơn manual calculation
return Objects.hash(name, age); // Thay vì tính thủ công
}
}

// Test
Person p1 = new Person("An", 20);
Person p2 = new Person("An", 20);
Person p3 = p1;

System.out.println(p1 == p2); // false - khác object
System.out.println(p1.equals(p2)); // true - cùng nội dung (sau khi override)
System.out.println(p1 == p3); // true - cùng reference
🔥 Bẫy OCP: null reference behavior
String str1 = null;
String str2 = "Hello";

// ❌ NullPointerException
// System.out.println(str1.equals(str2));

// ✅ An toàn - không throw exception
System.out.println(str2.equals(str1)); // false

// ✅ An toàn với == (nhưng chỉ kiểm tra reference)
System.out.println(str1 == str2); // false
System.out.println(str1 == null); // true

// ✅ Best practice - kiểm tra null trước
if (str1 != null && str1.equals(str2)) {
System.out.println("Equal");
}

// ✅ Java 7+ - Objects.equals() tự động xử lý null
System.out.println(Objects.equals(str1, str2)); // false (không throw exception)

Ví dụ thực tế: Tìm kiếm sinh viên

class StudentDatabase {
Student findStudentById(String id) {
// Giả sử không tìm thấy
return null;
}
}

public class Main {
public static void main(String[] args) {
StudentDatabase db = new StudentDatabase();
Student student = db.findStudentById("SV999");

if (student != null) {
student.displayInfo();
} else {
System.out.println("Student not found!");
}
}
}

Garbage Collection

Garbage Collection là gì?

Garbage Collection (GC) là quá trình tự động thu hồi bộ nhớ của các objects không còn được sử dụng nữa.

Lợi ích
  • Tự động: Lập trình viên không cần quản lý bộ nhớ thủ công
  • Tránh memory leaks: Bộ nhớ được giải phóng tự động
  • An toàn hơn C/C++: Không có dangling pointers

Khi nào object bị thu hồi?

Object sẽ được GC thu hồi khi:

  1. Không còn reference nào trỏ đến object
  2. Reference được gán bằng null
  3. Reference trỏ đến object khác
public class GCExample {
public static void main(String[] args) {
Student s1 = new Student(); // Object 1 được tạo
s1.name = "An";

Student s2 = new Student(); // Object 2 được tạo
s2.name = "Bình";

s1 = s2; // Object 1 không còn reference → GC sẽ thu hồi

s2 = null; // Object 2 vẫn có reference s1
s1 = null; // Object 2 không còn reference → GC sẽ thu hồi
}
}

Gợi ý GC chạy

Bạn có thể gợi ý JVM chạy GC (không bắt buộc):

System.gc(); // Gợi ý JVM chạy garbage collection
Cảnh báo

System.gc() chỉ là gợi ý, JVM có thể bỏ qua. Đừng phụ thuộc vào nó!

Memory: Stack vs Heap

Java chia bộ nhớ runtime thành 2 vùng chính: StackHeap.

Stack Memory (Bộ nhớ ngăn xếp)

Đặc điểm:

  • Lưu trữ local variables (biến cục bộ) và references (tham chiếu) đến objects
  • Hoạt động theo nguyên tắc LIFO (Last In, First Out) - vào sau ra trước
  • Tự động giải phóng khi method kết thúc (không cần GC)
  • Nhanh (truy cập trực tiếp) nhưng kích thước nhỏ (thường 1MB - 2MB)
  • Mỗi thread có stack riêng (thread-safe)

Stack lưu gì:

  • Primitive variables: int, double, boolean, char...
  • Object references (địa chỉ): Student student (chỉ địa chỉ, không phải object)
  • Method call frames: Thông tin về method đang thực thi

Heap Memory (Bộ nhớ đống)

Đặc điểm:

  • Lưu trữ actual objects (đối tượng thực tế) và arrays
  • Lớn hơn stack nhiều (có thể hàng GB) nhưng chậm hơn
  • Quản lý bởi Garbage Collector (GC)
  • Objects tồn tại cho đến khi GC thu hồi (khi không còn reference)
  • Chia sẻ giữa tất cả threads (cần đồng bộ khi truy cập)

Heap lưu gì:

  • Tất cả objects được tạo bằng new
  • Instance variables (fields của object)
  • Arrays (cả primitive arrays và object arrays)

So sánh chi tiết Stack vs Heap

Tiêu chíStackHeap
Lưu trữLocal variables, referencesActual objects, arrays
Kích thướcNhỏ (1-2 MB)Lớn (có thể GB)
Tốc độNhanh (truy cập trực tiếp)Chậm hơn
Quản lýTự động (LIFO)Garbage Collector
LifetimeĐến khi method kết thúcĐến khi GC thu hồi
ThreadMỗi thread có stack riêngChia sẻ giữa threads
LỗiStackOverflowErrorOutOfMemoryError: Java heap space
Truy cậpChỉ thread hiện tạiTất cả threads

Minh họa

public class MemoryExample {
public static void main(String[] args) {
// Stack: lưu reference 'student'
// Heap: lưu object Student
Student student = new Student();
student.name = "An"; // "An" cũng lưu trong heap (String pool)
student.age = 20; // primitive value lưu trong object

processStudent(student);
}

static void processStudent(Student s) {
// Stack: lưu reference 's' (trỏ đến cùng object với 'student')
s.age = 21; // Thay đổi object trong heap
}
}

Sơ đồ bộ nhớ:

Stack                   Heap
+----------------+ +------------------------+
| main() | | Student object |
| student ---------->| name = "An" |
| | | age = 21 |
+----------------+ | studentId = null |
| processStudent()| | gpa = 0.0 |
| s ----------------→| (cùng object) |
+----------------+ +------------------------+
💡 Cách nhớ Stack vs Heap
  • Stack: Như danh bạ điện thoại - chỉ lưu số điện thoại (reference), không lưu người thật
  • Heap: Như thế giới thực - người thật (object) sống ở đây
  • Nhiều người có thể lưu cùng 1 số điện thoại (multiple references to same object)

Ví dụ chi tiết: Stack và Heap hoạt động như thế nào

public class MemoryDemo {
public static void main(String[] args) { // Frame 1: main()
int x = 10; // Stack: x = 10
Student s1 = new Student("An", 20); // Stack: s1 = địa chỉ 0x100
// Heap: object Student tại 0x100
changeValue(x, s1); // Frame 2: changeValue()

System.out.println("x = " + x); // 10 (không đổi)
System.out.println("s1.age = " + s1.age); // 25 (đã đổi!)
} // Frame 1 kết thúc → giải phóng

static void changeValue(int num, Student student) { // Frame 2
num = 100; // Stack: num = 100 (copy của x, không ảnh hưởng x)
student.age = 25; // Heap: thay đổi object tại 0x100 (ảnh hưởng s1)
student = null; // Stack: student = null (chỉ ảnh hưởng local variable)
} // Frame 2 kết thúc → num, student (reference) bị xóa
}

Sơ đồ bộ nhớ chi tiết:

STACK (LIFO)                                 HEAP
+---------------------------+ +--------------------------------+
| Frame 2: changeValue() | | |
| num = 100 | | Object Student @ 0x100 |
| student = null ----X | | +--------------------------+ |
+---------------------------+ | | name = "An" | |
| Frame 1: main() | | | age = 25 (đã đổi!) | |
| x = 10 | | | studentId = null | |
| s1 = 0x100 ---------------------+-------| +--------------------------+ |
| args = 0x200 ---------+ | | |
+---------------------------+ | | Array String @ 0x200 |
| | +--------------------------+ |
| | | (empty array) | |
+------>| +--------------------------+ |
+--------------------------------+

Giải thích:

  1. x = 10 lưu trong stack (primitive)
  2. s1 lưu địa chỉ 0x100 trong stack, object thật ở heap
  3. Gọi changeValue(): tạo frame mới trên stack
  4. num = 100: chỉ thay đổi copy trong stack, không ảnh hưởng x
  5. student.age = 25: thay đổi object trong heap → ảnh hưởng s1
  6. student = null: chỉ thay đổi reference local, không ảnh hưởng s1
  7. Kết thúc changeValue(): frame 2 bị xóa khỏi stack

GC Roots và Reachability

GC Roots là các điểm bắt đầu mà Garbage Collector sử dụng để xác định object nào còn đang được sử dụng.

GC Roots bao gồm:

  1. Local variables trong stack của threads đang chạy
  2. Static variables của các classes đã load
  3. Active threads (thread objects)
  4. JNI references (từ native code)

Cách GC hoạt động:

  1. Bắt đầu từ GC Roots
  2. Đánh dấu (mark) tất cả objects có thể reach được từ roots
  3. Quét (sweep) và thu hồi objects không được đánh dấu
public class GCRootsDemo {
private static Student staticStudent; // GC Root: static variable

public static void main(String[] args) {
// GC Root: local variable trong main thread
Student s1 = new Student("An", 20);

Student s2 = new Student("Bình", 21);
s2.friend = new Student("Cường", 22); // Friend chỉ reachable qua s2

staticStudent = s1; // s1 reachable qua static variable

s1 = null; // s1 vẫn reachable qua staticStudent
s2 = null; // s2 và friend KHÔNG reachable → eligible for GC

System.gc(); // Gợi ý GC chạy
}
}

Các loại References trong Java

🔬 Nâng cao

Phần này giới thiệu các loại references đặc biệt (Soft, Weak, Phantom). Nếu bạn mới bắt đầu, có thể bỏ qua và quay lại ở Module JVM Internals.

Java có 4 loại references với mức độ "mạnh" khác nhau:

1. Strong Reference (Tham chiếu mạnh)

Reference thông thường, GC không bao giờ thu hồi object nếu còn strong reference.

Student student = new Student("An", 20); // Strong reference
// Object không bao giờ bị GC thu hồi cho đến khi student = null

2. Soft Reference (Tham chiếu mềm)

GC có thể thu hồi khi hết bộ nhớ (trước khi throw OutOfMemoryError).

import java.lang.ref.SoftReference;

SoftReference<Student> softRef = new SoftReference<>(new Student("An", 20));
Student student = softRef.get(); // Lấy object (có thể null nếu đã GC)

// Use case: Caching - giữ trong cache nhưng có thể xóa khi hết RAM

3. Weak Reference (Tham chiếu yếu)

GC sẽ thu hồi ngay khi chạy, dù còn bộ nhớ hay không.

import java.lang.ref.WeakReference;

WeakReference<Student> weakRef = new WeakReference<>(new Student("An", 20));
Student student = weakRef.get(); // Có thể null sau GC

// Use case: WeakHashMap - key tự động xóa khi không còn dùng

4. Phantom Reference (Tham chiếu ảo)

Không thể lấy object qua get(), chỉ dùng để nhận thông báo khi object sắp bị GC.

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

ReferenceQueue<Student> queue = new ReferenceQueue<>();
PhantomReference<Student> phantomRef = new PhantomReference<>(new Student("An", 20), queue);

// phantomRef.get() luôn trả về null
// Use case: Cleanup resources trước khi object bị deallocate
Tóm tắt References
  • Strong: Normal reference, không bao giờ GC (khi còn reference)
  • Soft: GC khi hết RAM - dùng cho cache
  • Weak: GC ngay khi chạy - dùng cho WeakHashMap
  • Phantom: Không access được object - dùng cho cleanup notification
🔥 Bẫy OCP: NullPointerException scenarios
// Scenario 1: Uninitialized reference
Student student; // Chưa khởi tạo
// System.out.println(student.name); // ❌ Compile error: variable might not have been initialized

// Scenario 2: Explicit null assignment
Student student = null;
// System.out.println(student.name); // ❌ NullPointerException

// Scenario 3: Method return null
Student student = findStudent("999"); // Method trả về null
// student.displayInfo(); // ❌ NullPointerException nếu không kiểm tra

// Scenario 4: Array elements default to null
Student[] students = new Student[5]; // Array có 5 phần tử, tất cả đều null
// students[0].displayInfo(); // ❌ NullPointerException

// Scenario 5: Chaining calls
// student.getFriend().getName(); // ❌ NPE nếu getFriend() trả về null

// ✅ Best practice: Null-safe code
Student student = findStudent("001");
if (student != null) {
student.displayInfo();
} else {
System.out.println("Student not found");
}

// ✅ Java 8+: Optional
Optional<Student> optStudent = Optional.ofNullable(findStudent("001"));
optStudent.ifPresent(s -> s.displayInfo());

// ✅ Java 14+: NPE with helpful message
// NullPointerException: Cannot invoke "Student.getName()" because "student" is null

Ví dụ tổng hợp: Quản lý Xe hơi

class Car {
// Fields (attributes)
String brand;
String model;
String color;
int year;
double price;
int mileage; // km đã đi

// Methods (behaviors)
void displayInfo() {
System.out.println("=== Car Information ===");
System.out.println("Brand: " + brand);
System.out.println("Model: " + model);
System.out.println("Color: " + color);
System.out.println("Year: " + year);
System.out.println("Price: $" + price);
System.out.println("Mileage: " + mileage + " km");
}

void drive(int distance) {
mileage += distance;
System.out.println("Drove " + distance + " km");
System.out.println("Total mileage: " + mileage + " km");
}

void repaint(String newColor) {
System.out.println("Repainting from " + color + " to " + newColor);
color = newColor;
}

double calculateDepreciation() {
int age = 2024 - year;
double depreciationRate = 0.15; // 15% per year
double currentValue = price * Math.pow(1 - depreciationRate, age);
return currentValue;
}
}

public class CarDealership {
public static void main(String[] args) {
// Tạo object car1
Car car1 = new Car();
car1.brand = "Toyota";
car1.model = "Camry";
car1.color = "Black";
car1.year = 2020;
car1.price = 30000;
car1.mileage = 15000;

// Tạo object car2
Car car2 = new Car();
car2.brand = "Honda";
car2.model = "Civic";
car2.color = "White";
car2.year = 2022;
car2.price = 25000;
car2.mileage = 5000;

// Sử dụng car1
car1.displayInfo();
car1.drive(200);
car1.repaint("Red");
double value1 = car1.calculateDepreciation();
System.out.println("Depreciated value: $" + String.format("%.2f", value1));

System.out.println("\n" + "=".repeat(40) + "\n");

// Sử dụng car2
car2.displayInfo();
car2.drive(100);

// Ví dụ null reference
Car car3 = null;
if (car3 != null) {
car3.displayInfo();
} else {
System.out.println("\ncar3 is null - no car assigned");
}

// Gán reference mới
car3 = car1; // car3 và car1 cùng trỏ đến một object
car3.repaint("Blue"); // Thay đổi qua car3
car1.displayInfo(); // car1 cũng thấy thay đổi
}
}

Bài tập thực hành

Bài 1: Tạo Class Book

Tạo class Book với:

  • Fields: title, author, isbn, price, pageCount
  • Methods:
    • displayInfo(): In thông tin sách
    • applyDiscount(double percent): Giảm giá
    • isPricey(): Trả về true nếu giá > $50

Tạo 3 objects Book và test các methods.

Bài 2: Tạo Class Rectangle

Tạo class Rectangle với:

  • Fields: width, height
  • Methods:
    • calculateArea(): Tính diện tích
    • calculatePerimeter(): Tính chu vi
    • isSquare(): Kiểm tra có phải hình vuông không
    • displayInfo(): Hiển thị thông tin

Bài 3: Null Safety

Viết method printStudentInfo(Student student) có kiểm tra null trước khi in. Test với cả student null và non-null.

Bài 4: Multiple References

Tạo 2 references trỏ đến cùng một BankAccount object. Thực hiện deposit qua reference thứ nhất, rồi kiểm tra balance qua reference thứ hai. Giải thích kết quả.

Tổng kết

Key Takeaways

  • Class: Blueprint để tạo objects, chứa fields và methods
  • Object: Instance cụ thể của class, lưu trong heap memory
  • new keyword: Tạo object mới và cấp phát bộ nhớ
  • Dot operator (.): Truy cập fields và methods của object
  • null: Giá trị đặc biệt nghĩa là "không tham chiếu object nào"
  • NullPointerException: Lỗi khi truy cập member trên null reference
  • Garbage Collection: Tự động thu hồi bộ nhớ của objects không dùng
  • Stack vs Heap:
    • Stack: Lưu local variables và references
    • Heap: Lưu actual objects

Bài tiếp theo

Trong bài tiếp theo, chúng ta sẽ tìm hiểu về Constructors và Methods - cách khởi tạo objects một cách chuyên nghiệp và định nghĩa behaviors phức tạp hơn.

Bài 3: Constructor và Methods

Đọc thêm


"Procedural programming is about writing procedures or methods that perform operations on the data, while object-oriented programming is about creating objects that contain both data and methods." — Microsoft Documentation