Bỏ qua

OOP — Class và Object

1. Khái niệm

Class là bản thiết kế (blueprint) mô tả một loại đối tượng: nó có những dữ liệu gì (fields) và có thể làm gì (methods).

Object là một thực thể cụ thể được tạo ra từ class đó. Một class có thể tạo ra vô số object.

// Class — bản thiết kế
class Dog {
    String name;
    int age;

    void bark() {
        System.out.println(name + " says: Woof!");
    }
}

// Object — thực thể cụ thể
Dog rex  = new Dog();
Dog luna = new Dog();
// rex và luna là hai object riêng biệt, cùng từ class Dog

2. Tại sao quan trọng

OOP là nền tảng của Java và hầu hết backend framework hiện đại:

  • Spring Boot hoạt động dựa trên bean — object được Spring quản lý
  • JPA/Hibernate map class Java thành bảng database
  • Design patterns (Factory, Strategy, Builder...) đều xây trên class hierarchy

Hiểu rõ class/object, static/instance, và ba method toString() / equals() / hashCode() là điều kiện tối thiểu để làm việc với bất kỳ Java codebase thực tế nào.


3. Anatomy của một Class

Class Anatomy Diagram

Fields — dữ liệu

Fields (còn gọi là instance variables) lưu trạng thái của mỗi object.

class Product {
    String name;
    double price;
    int    quantity;
}

Mỗi object có bản sao riêng của các instance fields — thay đổi field của object này không ảnh hưởng object kia.

Methods — hành vi

Methods định nghĩa những gì object có thể làm.

class Product {
    String name;
    double price;
    int    quantity;

    double totalValue() {
        return price * quantity;
    }

    void restock(int amount) {
        quantity += amount;
    }
}

Naming conventions

Thành phần Convention Ví dụ
Class PascalCase BankAccount, ProductService
Field camelCase firstName, accountBalance
Method camelCase, động từ deposit(), calculateTax()
Constant UPPER_SNAKE_CASE MAX_RETRY, DEFAULT_TIMEOUT

4. Object Creation — new và Heap

Tạo object

Product p = new Product();

Ba việc xảy ra theo thứ tự:

  1. Cấp phát Heap — JVM tìm chỗ trống trên Heap, cấp phát memory cho object
  2. Khởi tạo giá trị mặc định — fields được gán giá trị default (0, null, false)
  3. Trả về reference — địa chỉ object trên Heap được lưu vào biến p trên Stack

Stack và Heap reference

Giá trị default của fields

Kiểu Default
int, long, short, byte 0
double, float 0.0
boolean false
char ''
Object (String, array, ...) null

Local variable không có default

Fields của object có giá trị mặc định. Nhưng local variable trong method thì không — truy cập trước khi gán sẽ gây lỗi compile.

int x;
System.out.println(x); // ❌ lỗi compile: variable x might not have been initialized

Nhiều reference, một object

Product a = new Product();
Product b = a;   // b trỏ vào CÙNG object với a — không phải bản sao

a.price = 100;
System.out.println(b.price); // 100 — cùng object

5. this keyword

this là reference trỏ về chính object đang thực thi method. Dùng khi tên field bị che khuất bởi tham số cùng tên.

class Circle {
    double radius;

    void setRadius(double radius) {
        this.radius = radius; // this.radius = field; radius = tham số
    }

    double area() {
        return Math.PI * radius * radius; // this ở đây không bắt buộc
    }
}

Khi nào cần this, khi nào không?

this bắt buộc khi tên tham số trùng với tên field. Ngoài ra this là tùy chọn — Java tự hiểu radius là field nếu không có local variable cùng tên. Đừng lạm dụng this ở mọi chỗ — code sẽ dài và khó đọc hơn mà không thêm giá trị gì.


6. Static vs Instance

Instance members

Gắn với từng object cụ thể. Mỗi object có bản sao riêng.

class Counter {
    int count = 0; // instance field — mỗi object có riêng

    void increment() { count++; }
}

Counter c1 = new Counter();
Counter c2 = new Counter();
c1.increment();
System.out.println(c1.count); // 1
System.out.println(c2.count); // 0 — không bị ảnh hưởng

Static members

Gắn với class, dùng chung bởi tất cả object. Tồn tại trong Metaspace.

class Counter {
    static int totalCreated = 0; // dùng chung, sống trong Metaspace
    int count = 0;

    Counter() {
        totalCreated++; // mỗi lần tạo object mới, tăng counter chung
    }
}

Counter c1 = new Counter();
Counter c2 = new Counter();
System.out.println(Counter.totalCreated); // 2 — gọi qua tên class

Static method

Không có this. Không thể truy cập instance field trực tiếp.

class MathUtils {
    static int square(int n) {
        return n * n; // không cần object
    }
}

int result = MathUtils.square(5); // gọi qua tên class
Instance Static
Gắn với Object Class
Memory Heap (mỗi object) Metaspace (dùng chung)
Gọi qua Object reference Tên class
Truy cập this Không
Dùng khi Cần state của object Utility, không cần state

Anti-pattern: gọi static qua object reference

Counter c = new Counter();
c.totalCreated;        // ❌ hoạt động nhưng misleading — đọc như instance field
Counter.totalCreated;  // ✅ rõ ràng đây là static

7. toString(), equals(), hashCode()

Ba method này được kế thừa từ Object — class gốc của mọi class trong Java. Default implementation thường không hữu ích — hầu hết class nên override cả ba.

toString()

Default trả về ClassName@hexHashCode — vô nghĩa với người đọc.

class Point {
    int x, y;

    Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public String toString() {
        return "Point(" + x + ", " + y + ")";
    }
}

Point p = new Point(3, 4);
System.out.println(p); // Point(3, 4) — Java tự gọi toString()

Override toString() để debug dễ hơn

System.out.println(obj), string concatenation "value: " + obj, và logger đều tự gọi toString(). Object không override → log vô nghĩa như com.example.Point@7ef88735.

equals()

Default so sánh địa chỉ (giống ==) — hai object khác nhau luôn false dù cùng dữ liệu.

class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                    // cùng địa chỉ
        if (!(o instanceof Point other)) return false; // khác kiểu
        return x == other.x && y == other.y;           // so sánh dữ liệu
    }
}

Point p1 = new Point(3, 4);
Point p2 = new Point(3, 4);
System.out.println(p1 == p2);      // false — khác địa chỉ
System.out.println(p1.equals(p2)); // true — cùng dữ liệu

hashCode()

Contract: nếu a.equals(b)true thì a.hashCode() == b.hashCode() phải là true.

class Point {
    int x, y;

    @Override
    public boolean equals(Object o) { /* ... */ }

    @Override
    public int hashCode() {
        return Objects.hash(x, y); // kết hợp hash của các fields dùng trong equals()
    }
}

Quy tắc: override equals() → bắt buộc override hashCode()

Nếu chỉ override equals(): hai object bằng nhau theo equals nhưng hashCode khác nhau → HashSet lưu cả hai như object riêng biệt, HashMap không tìm được key đã put vào. Bug này không có compile error hay exception — chỉ ra kết quả sai.

Cách nhanh nhất

IntelliJ: Alt+InsertGenerateequals() and hashCode() → tự sinh code chuẩn.

Hoặc dùng Record (Java 16+) — tự động có toString(), equals(), hashCode():

record Point(int x, int y) {} // xong, không cần viết gì thêm

Point p1 = new Point(3, 4);
Point p2 = new Point(3, 4);
System.out.println(p1.equals(p2)); // true
System.out.println(p1);            // Point[x=3, y=4]

8. Code ví dụ

Đã kiểm chứng

Bản đầy đủ có thể compile: OopDemo.java

import java.util.Objects;

public class BankAccount {

    private static int totalAccounts = 0; // (1)!

    private final String accountId;
    private final String owner;
    private double balance;

    public BankAccount(String owner, double initialBalance) {
        this.owner          = owner;
        this.balance        = initialBalance;
        this.accountId      = "ACC-" + (++totalAccounts); // (2)!
    }

    public void deposit(double amount) {
        if (amount <= 0) return;
        balance += amount;
    }

    public boolean withdraw(double amount) {
        if (amount <= 0 || amount > balance) return false;
        balance -= amount;
        return true;
    }

    public double getBalance() { return balance; }

    public static int getTotalAccounts() { return totalAccounts; } // (3)!

    @Override
    public String toString() {
        return accountId + " [" + owner + "] $" + String.format("%.2f", balance);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof BankAccount other)) return false;
        return accountId.equals(other.accountId); // (4)!
    }

    @Override
    public int hashCode() {
        return Objects.hash(accountId);
    }

    public static void main(String[] args) {
        BankAccount alice = new BankAccount("Alice", 1000);
        BankAccount bob   = new BankAccount("Bob",   500);

        alice.deposit(200);
        bob.withdraw(100);

        System.out.println(alice);                          // ACC-1 [Alice] $1200.00
        System.out.println(bob);                            // ACC-2 [Bob] $400.00
        System.out.println(BankAccount.getTotalAccounts()); // 2

        BankAccount ref = alice;
        System.out.println(alice == ref);           // true — cùng địa chỉ
        System.out.println(alice.equals(ref));      // true
        System.out.println(alice == bob);           // false
        System.out.println(alice.equals(bob));      // false — khác accountId
    }
}
  1. static — field này sống trong Metaspace, dùng chung bởi tất cả instance. Tăng mỗi khi constructor chạy.
  2. accountId được gán từ totalAccounts sau khi tăng — mỗi tài khoản có ID tự động, duy nhất.
  3. static method — gọi qua BankAccount.getTotalAccounts(), không cần object. Chỉ truy cập được static member.
  4. Hai BankAccount bằng nhau nếu cùng accountId — không phụ thuộc vào balance hay owner.

9. Lỗi thường gặp

Lỗi 1 — Gọi instance method qua tên class

Dog.bark(); // ❌ lỗi compile — bark() là instance method, cần object

Dog rex = new Dog();
rex.bark(); // ✅

Lỗi 2 — Quên new, dùng null reference

Product p;
p.price = 100; // ❌ NullPointerException — p chưa trỏ vào object nào

Product p = new Product(); // ✅ tạo object trước
p.price = 100;

Lỗi 3 — Override equals() nhưng quên hashCode()

class User {
    String email;

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof User u)) return false;
        return email.equals(u.email);
    }
    // ❌ quên hashCode()
}

Set<User> users = new HashSet<>();
users.add(new User("a@example.com"));
users.contains(new User("a@example.com")); // false — bug! HashSet dùng hashCode để định vị bucket
// ✅ Luôn override cả hai
@Override
public int hashCode() {
    return Objects.hash(email);
}

Lỗi 4 — Gọi static member qua object reference

Counter c = new Counter();
int n = c.totalCreated;       // ❌ hoạt động nhưng misleading
int n = Counter.totalCreated; // ✅ rõ ràng đây là class-level data

Lỗi 5 — Nhầm nhiều reference là nhiều object

Product a = new Product();
Product b = a; // b không phải bản sao — cùng object với a

b.price = 999;
System.out.println(a.price); // 999 — a và b cùng object

10. Câu hỏi phỏng vấn

Q1: Class và Object khác nhau như thế nào?

Class là bản thiết kế định nghĩa cấu trúc và hành vi — tồn tại ở compile time, metadata lưu trong Metaspace. Object là thực thể được tạo ra từ class lúc runtime — sống trên Heap. Một class có thể tạo ra vô số object, mỗi object có bản sao riêng của instance fields nhưng dùng chung bytecode của các method.

Q2: ==equals() khác nhau thế nào?

== so sánh địa chỉ (reference equality) — true chỉ khi hai biến trỏ vào cùng một object trên Heap. equals() so sánh nội dung (value equality) — hành vi do class định nghĩa khi override. Default equals() từ Object cũng so sánh địa chỉ, nên phải override nếu muốn so sánh theo dữ liệu.

Q3: Tại sao phải override hashCode() khi đã override equals()?

Contract Java: hai object bằng nhau theo equals() phải có cùng hashCode(). HashMapHashSet dùng hashCode() để xác định bucket trước, sau đó mới dùng equals() để phân biệt trong bucket. Vi phạm contract — override equals() mà không override hashCode() — khiến hai object bằng nhau có thể rơi vào bucket khác nhau, làm HashSet.contains()HashMap.get() trả về kết quả sai dù không có exception.

Q4: this dùng để làm gì?

this là reference trỏ vào object đang thực thi method. Dùng để: (1) phân biệt field với tham số cùng tên, (2) gọi constructor khác trong cùng class bằng this(...), (3) truyền object hiện tại làm argument cho method khác. Không thể dùng this trong static method vì static method không gắn với bất kỳ object nào.

Q5: Static field và instance field lưu ở đâu trong JVM memory?

Instance field lưu trên Heap — mỗi object có bản sao riêng, tồn tại cho đến khi object bị GC thu hồi. Static field lưu trong Metaspace (Method Area) — dùng chung cho tất cả instance của class, tồn tại suốt vòng đời của class trong JVM.


11. Tài liệu tham khảo

Tài liệu Nội dung
JLS §8 — Class Declarations Đặc tả chính thức về class
Oracle Tutorial — Classes Hướng dẫn chính thức
JEP 395 — Records Record — tự động toString/equals/hashCode
Effective Java — Joshua Bloch Item 10: equals(), Item 11: hashCode(), Item 12: toString()
Head First Java — Sierra & Bates Chapter 2: A Trip to Objectville

Bình luận