Class và Object
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.
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
- Class chỉ là mô tả, không chiếm bộ nhớ cho data
- 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
- 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ó:
- State (trạng thái): Giá trị của các fields
- Behavior (hành vi): Các methods mà object có thể gọi
- 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:
Java Language Specification định nghĩa rõ ràng quá trình tạo object:
- Cấp phát bộ nhớ trên heap
- Khởi tạo fields với giá trị mặc định
- Thực thi constructor
- 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
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...");
}
}
- 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 objectnew: Toán tử cấp phát bộ nhớ cho objectClassName(): 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
}
}
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!");
}
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 == và 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ử ==.
Java Language Specification chia toán tử == và != thành 3 loại riêng biệt tùy theo kiểu dữ liệu:
- §15.21.1 — Numerical Equality: So sánh giá trị số (
byte,short,int,long,float,double,char) - §15.21.2 — Boolean Equality: So sánh giá trị boolean
- §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
== với float và double (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 == NaNluônfalse— dùngDouble.isNaN()để kiểm tra0.0 == -0.0luôntrue- 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
== với primitivesPrimitives 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
== với referencesReferences 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
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"
Đâ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 → == là true. Ngoài range → tạo object mới → == là false.
Áp dụng tương tự cho: Byte, Short, Long (cache -128 đến 127), Character (cache 0 đến 127), Boolean (cache TRUE và FALSE).
==// 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ợp | Dù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 null | obj == null an toàn |
String | ❌ Tránh dùng | ✅ Luôn dùng | Pool có thể gây nhầm |
Integer, Long... (wrappers) | ❌ Tránh dùng | ✅ Luôn dùng | Cache gây kết quả bất ngờ |
| Custom objects | ❌ Trừ khi cố ý | ✅ Sau khi override | Cần override equals + hashCode |
| Enum | ✅ Dùng được | ✅ Cũng OK | Enum là singleton, == an toàn |
"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 == và 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
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.
- 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:
- Không còn reference nào trỏ đến object
- Reference được gán bằng null
- 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
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: Stack và Heap.
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í | Stack | Heap |
|---|---|---|
| Lưu trữ | Local variables, references | Actual objects, arrays |
| Kích thước | Nhỏ (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 |
| Thread | Mỗi thread có stack riêng | Chia sẻ giữa threads |
| Lỗi | StackOverflowError | OutOfMemoryError: Java heap space |
| Truy cập | Chỉ thread hiện tại | Tấ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) |
+----------------+ +------------------------+
- 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:
x = 10lưu trong stack (primitive)s1lưu địa chỉ0x100trong stack, object thật ở heap- Gọi
changeValue(): tạo frame mới trên stack num = 100: chỉ thay đổi copy trong stack, không ảnh hưởngxstudent.age = 25: thay đổi object trong heap → ảnh hưởngs1student = null: chỉ thay đổi reference local, không ảnh hưởngs1- 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:
- Local variables trong stack của threads đang chạy
- Static variables của các classes đã load
- Active threads (thread objects)
- JNI references (từ native code)
Cách GC hoạt động:
- Bắt đầu từ GC Roots
- Đánh dấu (mark) tất cả objects có thể reach được từ roots
- 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
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
- 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
// 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áchapplyDiscount(double percent): Giảm giáisPricey(): Trả vềtruenế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íchcalculatePerimeter(): Tính chu viisSquare(): Kiểm tra có phải hình vuông khôngdisplayInfo(): 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