Java — Từ Zero đến Senior/OOP cơ bản — class, object, encapsulation/Class và Object — bản thiết kế vs sản phẩm đúc ra
1/7
~18 phútOOP cơ bản — class, object, encapsulation

Class và Object — bản thiết kế vs sản phẩm đúc ra

Phân biệt class (bản thiết kế) và object (instance), từ khóa new, object sống trên heap và reference trên stack, `==` vs `.equals()` lần nữa nhìn từ góc OOP.

Module 4 khép lại với một class NumberUtils chỉ có method tĩnh — class ở đó chỉ là "cái sọt chứa hàm". Từ bài này, class thay vai trò khác: nó là bản thiết kế cho một kiểu đối tượng mới với trạng thái riêng. Bạn không chỉ gọi method — bạn tạo ra các object, mỗi object có field riêng, sống trên heap, được reference trỏ đến.

Bài này giải thích class là gì, object là gì (và hai thứ đó không giống nhau), cơ chế JVM cấp phát object trên heap, và một lần nữa nhìn == vs .equals() — nhưng lần này bạn hiểu sâu vì sao chúng khác nhau.

1. Analogy — khuôn bánh và chiếc bánh

Bạn có khuôn bánh trung thu bằng gỗ — đó là class. Khuôn mô tả hình dạng, kích thước, hoa văn. Bạn ép bột vào khuôn, cho ra 1 chiếc bánh — đó là object. Khuôn chỉ có 1; bánh có thể đúc hàng ngàn chiếc, mỗi chiếc nhân khác nhau (đậu xanh, thập cẩm), thậm chí ăn dở dang khác nhau.

Đời thườngJava
Khuôn bánhclass Cake
Một chiếc bánh đã đúcMột Cake object
Nhân bánh (đậu xanh vs thập cẩm)Field value của object
Hành động "cắn một miếng"Method

💡 💡 Cách nhớ

Class = template, object = instance đúc từ template. 1 class → N objects. Mỗi object có bản sao field riêng và có thể gọi method chung.

2. Khai báo class và tạo object

// Class khai bao 1 lan trong file
public class Book {
    String title;           // field
    String author;
    int year;
}

Dùng class — tạo object bằng new:

public class App {
    public static void main(String[] args) {
        Book b1 = new Book();
        b1.title = "Clean Code";
        b1.author = "Robert C. Martin";
        b1.year = 2008;

        Book b2 = new Book();
        b2.title = "Effective Java";
        b2.author = "Joshua Bloch";
        b2.year = 2018;

        System.out.println(b1.title);   // Clean Code
        System.out.println(b2.title);   // Effective Java
    }
}

b1b22 object khác nhau, cùng kiểu Book. Sửa b1.title không đụng b2.title — mỗi object có bản sao field riêng.

2.1 new làm gì bên dưới?

Khi bạn viết new Book(), JVM chạy 3 bước:

  1. Cấp phát memory — tìm chỗ trên heap đủ chứa tất cả field của Book. Field chưa gán lấy giá trị mặc định (null cho reference, 0 cho int, false cho boolean).
  2. Chạy constructor — method đặc biệt khởi tạo object (bài sau).
  3. Trả về reference — địa chỉ của object trên heap được gán vào biến b1.
flowchart LR
    subgraph Stack
        b1["b1 (reference)"]
        b2["b2 (reference)"]
    end
    subgraph Heap
        obj1["Book obj<br/>title = 'Clean Code'<br/>author = 'Martin'<br/>year = 2008"]
        obj2["Book obj<br/>title = 'Effective Java'<br/>author = 'Bloch'<br/>year = 2018"]
    end
    b1 -.-> obj1
    b2 -.-> obj2

Biến b1, b2 nằm trên stack frame — chúng chỉ giữ địa chỉ (reference). Object nằm trên heap. Đây là cơ chế đã học ở Module 2 — giờ nhìn lại ở tầng OOP.

3. Giá trị mặc định của field

Field không được gán nhận giá trị mặc định theo kiểu:

KiểuDefault
int, long, short, byte, char0
double, float0.0
booleanfalse
Bất kỳ reference type (String, Book, int[], ...)null

Khác với local variable — local không có default, phải gán trước khi đọc, không compiler báo error:

int x;
System.out.println(x);   // COMPILE ERROR — variable x might not have been initialized

public class Foo {
    int x;   // OK — field dong tu dong 0
}

Lý do: local nằm trong stack frame được cấp phát nhanh, không có bước zero-out. Field trong object được JVM zero-out khi cấp phát — spec yêu cầu (phần của "object initialization").

4. Object sống trên heap — hệ quả

4.1 Nhiều reference cùng trỏ 1 object

Book a = new Book();
a.title = "Java";
Book b = a;           // copy reference
b.title = "Python";

System.out.println(a.title);   // "Python" — a va b tro cung object

Đây là đúng pass-by-value: b = a copy reference (địa chỉ), không copy object. Sửa object qua reference nào cũng thấy qua reference khác.

4.2 Object không còn ai reference → GC thu hồi

Book a = new Book();
a = null;     // khong ai giu reference den object cu nua
// JVM se don object cu o lan GC tiep theo

Bạn không gọi free() hay delete như C/C++. Garbage Collector (GC) tự phát hiện object không reachable và thu hồi memory. Chi tiết ở module Memory & GC.

4.3 == so địa chỉ, .equals() so nội dung

Book a = new Book(); a.title = "Java";
Book b = new Book(); b.title = "Java";

System.out.println(a == b);         // false — 2 object khac, dia chi khac
System.out.println(a.equals(b));    // false — default equals() cua Object chi so == (dia chi)

Chú ý: equals() default không so nội dung. Muốn 2 Book cùng nội dung ra true, bạn phải override equals(). Sẽ đào sâu ở bài toString / equals / hashCode.

5. Nhiều class trong 1 file

Java cho phép nhiều class trong 1 file, nhưng chỉ 1 public class, và tên file phải trùng tên public class:

// File: App.java
public class App {
    public static void main(String[] args) {
        Book b = new Book();
        b.title = "Java";
        System.out.println(b.title);
    }
}

class Book {             // package-private, khong public
    String title;
    String author;
    int year;
}

Rule này do compiler áp đặt — classpath / classloader dựa vào tên file khi tìm class bytecode. Thực tế, mỗi class quan trọng nên có file riêng (convention + dễ tìm).

6. Instance method — method thuộc về object

Module 4 dùng static hết. Giờ bạn viết method không static — gọi qua object:

public class Book {
    String title;
    int year;

    public void printInfo() {                      // instance method
        System.out.println(title + " (" + year + ")");
    }

    public int yearsOld() {
        return java.time.Year.now().getValue() - year;
    }
}

// Su dung:
Book b = new Book();
b.title = "Java";
b.year = 2010;
b.printInfo();               // goi method qua object
System.out.println(b.yearsOld());

Trong body của instance method, bạn truy cập field trực tiếp bằng tên (title, year) — Java hiểu ngầm "field của object đang gọi method này". Tương đương this.title, this.year — bài sau nói rõ this.

6.1 Instance method ≠ static method

public class Book {
    String title;

    public static void hello() { System.out.println("Hello"); }   // static
    public void printTitle()   { System.out.println(title); }      // instance
}

Book.hello();                // goi static qua class — OK
Book.printTitle();           // COMPILE ERROR — printTitle can 1 object
new Book().printTitle();     // OK — goi qua object

Book b = new Book();
b.hello();                   // OK nhung KHONG nen — goi static qua instance gay hieu lam

Gọi static qua instance vẫn compile được nhưng IDE cảnh báo: nên gọi qua class để code rõ.

7. Pitfall tổng hợp

Nhầm 1: Tưởng class là object.

Book.title = "Java";   // COMPILE ERROR — title la instance field, khong goi qua class

new Book() để tạo object trước, rồi gán qua reference: b.title = "Java".

Nhầm 2: new rồi quên gán vào biến.

new Book();   // object duoc tao roi mat luon — khong ai giu reference

✅ Lưu reference vào biến nếu cần dùng sau: Book b = new Book();.

Nhầm 3: So sánh object bằng ==.

if (user1 == user2) { ... }   // so dia chi — gan nhu luon false

✅ Override + dùng .equals(). Bài sau.

Nhầm 4: Tưởng đọc null field cho crash ngay lúc khai báo.

public class User {
    String name;   // default null
    int len = name.length();   // NPE tai runtime
}

✅ Khởi tạo field phụ thuộc nhau trong constructor hoặc lazy.

Nhầm 5: Viết nhiều public class trong 1 file.

// File: App.java
public class App { ... }
public class Book { ... }   // COMPILE ERROR

✅ Chỉ 1 public class trùng tên file. Các class khác bỏ public hoặc tách file.

8. 📚 Deep Dive Oracle

ℹ️ 📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: JLS §15.9 mô tả chi tiết chuỗi bước của new: evaluate arguments → allocate → initialize fields to default → call constructor. Hiểu chuỗi này giúp bạn debug các bug kiểu "field == null trong constructor" (field được set default trước khi constructor chạy).

9. Tóm tắt

  • Class = template. Object = instance đúc từ class. 1 class → N objects, mỗi object field riêng.
  • new ClassName() cấp phát object trên heap, trả reference về biến trên stack.
  • Field không gán lấy giá trị mặc định (0, false, null). Local variable thì bắt buộc gán trước khi đọc.
  • Biến kiểu reference giữ địa chỉ, không giữ object. Gán b = a copy reference → 2 biến trỏ cùng object.
  • == so địa chỉ. .equals() default cũng so địa chỉ — cần override để so nội dung.
  • GC tự thu hồi object không còn reference. Bạn không gọi free().
  • Instance method gọi qua object (obj.method()); static method gọi qua class (Class.method()).
  • 1 file Java chỉ có 1 public class và tên file phải trùng.

10. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau in gì?
Book a = new Book();
a.title = "A";
Book b = a;
b.title = "B";
System.out.println(a.title);

In B.

Book b = a copy referenceab cùng trỏ đến một object trên heap. Sửa b.title là sửa object chung → a.title cũng thay đổi.

Nếu muốn 2 object độc lập, phải new Book() lần nữa hoặc tự copy field.

Q2
Đoạn sau compile không?
public class Book {
    int year;
}

public class App {
    public static void main(String[] args) {
        int x;
        System.out.println(x);

        Book b = new Book();
        System.out.println(b.year);
    }
}

Không compile — int x local variable bị dùng trước khi gán → compile error "variable x might not have been initialized".

Còn b.year thì OK và in 0. Field của object được JVM zero-out khi cấp phát — int field mặc định 0. Local variable không có default.

Lý do khác biệt: field khởi tạo trong quá trình new (JVM chịu trách nhiệm zero memory); local nằm trong stack frame không có bước zero hóa → Java yêu cầu gán trước khi đọc để tránh đọc rác.

Q3
Đoạn sau in gì và tại sao?
Book a = new Book();
Book b = new Book();
System.out.println(a == b);
System.out.println(a.equals(b));

Cả hai đều in false.

  • a == b so reference (địa chỉ). ab là 2 lần new riêng → 2 object trên heap khác chỗ → địa chỉ khác → false.
  • a.equals(b) gọi phiên bản equals default của Object — bản này chỉ thực hiện this == other, không so nội dung. Vẫn ra false.

Muốn so nội dung (vd cùng title/author/year → bằng nhau), phải override equals() trong class Book. Đi kèm phải override hashCode() — contract Java. Bài sau sẽ đào sâu.

Q4
Đoạn sau có gì khác thường? Mỗi khi chạy main tạo bao nhiêu object?
new Book();
new Book();
Book b = new Book();

Tạo 3 object trên heap. Hai dòng đầu không lưu reference vào biến → object vừa tạo đã không còn ai trỏ đến → eligible GC ngay lập tức.

Dòng thứ 3 gán vào b → object đó sống cho đến khi b ra khỏi scope hoặc bị gán reference khác.

Pattern "new rồi vứt" hợp lệ khi new chỉ vì side-effect (vd trong constructor có ghi log, register listener) — hiếm và thường là design kém. Phổ biến hơn: gọi method trả về mới dùng kết quả, vd new StringBuilder().append("a").append("b").toString().

Q5
Vì sao file App.java không thể chứa 2 class đều public?

Java compiler + classloader dùng tên file để tìm class bytecode. Rule: trong 1 file, chỉ được 1 class public, và tên file phải trùng tên public class đó. Có 2 public class → compiler không biết đặt file nào → refuse compile.

Nhiều class non-public (package-private) trong 1 file thì được:

// App.java
public class App { ... }
class Book { ... }      // OK — khong public
class User { ... }      // OK

Bytecode vẫn sinh ra App.class, Book.class, User.class (mỗi class 1 file .class). Convention: mỗi class quan trọng nên có file .java riêng cho dễ tìm và tôn trọng IDE/tool.


Bài tiếp theo: Constructor và this — khởi tạo object đúng cách