Java Core / OOP
Mental model trước, keyword sau

OOP: từ object đến thiết kế dễ thay đổi

OOP không phải chỉ là gom field và method vào class. Mục tiêu là đặt state cùng behavior bảo vệ state đó, tạo boundary rõ và cho phép thay đổi implementation mà ít ảnh hưởng client.

1.1.01 · OOP design8 mục nền6 mục interview checklistJava SE 21 · không dùng preview
Cách học: đi từ invariant → contract → dispatch → substitutability → ownership → lựa chọn kiểu → SOLID → interview evidence. Tám mục nền được giữ; phần bổ sung có nhãn riêng. Quy tắc ngôn ngữ có reference, còn gợi ý thiết kế và bài tập được trình bày như đề xuất có phạm vi.

1. Object, class và invariant

Class là định nghĩa kiểu; object là instance có identity, state và behavior. Một class tốt không chỉ chứa dữ liệu mà còn giữ invariant: điều kiện phải luôn đúng.

Ví dụ nguồn · BankAccount · minh họa guard; chưa có luồng nạp tiền hoặc số dư ban đầu.

public final class BankAccount {
    private long balance;

    public void withdraw(long amount) {
        if (amount <= 0 || amount > balance) {
            throw new IllegalArgumentException("Invalid amount");
        }
        balance -= amount;
    }
}

Nếu để balance public hoặc chỉ sinh getter/setter vô điều kiện, caller có thể tạo state sai. Đây là lý do encapsulation quan trọng hơn việc “field phải private” một cách máy móc.

Bổ sung · ví dụ thiết kế và kiểm chứng

Biến invariant thành điều kiện kiểm tra được

Với ví dụ này, viết rõ balance ≥ 0, đầu vào hợp lệ là 0 < amount ≤ balance, và một lần rút bị từ chối phải giữ nguyên số dư. Constructor phải tạo state hợp lệ; mọi đường cập nhật sau đó phải giữ cùng quy tắc. Đó là một contract có thể test, không chỉ là quy ước đặt tên field.

Giới hạn của đoạn nguồn: balance khởi tạo mặc định bằng 0, chưa có constructor nhận số dư hay thao tác nạp tiền. Vì vậy không có lần rút dương nào thành công ngay sau new BankAccount(). Đoạn này minh họa guard, không phải ứng dụng tài khoản hoàn chỉnh.

Bài tập bổ sung · lưu thành BankAccountCheck.java · chạy độc lập, không cần thư viện.

public class BankAccountCheck {
    static final class Account {
        private long balance;

        Account(long initialBalance) {
            if (initialBalance < 0) {
                throw new IllegalArgumentException("Negative balance");
            }
            balance = initialBalance;
        }

        void withdraw(long amount) {
            if (amount <= 0 || amount > balance) {
                throw new IllegalArgumentException("Invalid amount");
            }
            balance -= amount;
        }

        long balance() { return balance; }
    }

    static void rejected(Account account, long amount) {
        long before = account.balance();
        try {
            account.withdraw(amount);
            throw new AssertionError("Expected rejection");
        } catch (IllegalArgumentException expected) {
            if (account.balance() != before) {
                throw new AssertionError("State changed after rejection");
            }
        }
    }

    public static void main(String[] args) {
        Account account = new Account(100);
        rejected(account, 0);
        rejected(account, -1);
        rejected(account, 101);
        account.withdraw(100);
        if (account.balance() != 0) {
            throw new AssertionError("Wrong balance");
        }
        rejected(account, 1);
        System.out.println("PASS: invariant and rejected transitions");
    }
}

Acceptance: ba đầu vào 0, −1, 101 bị từ chối khi số dư là 100; rút đúng 100 thành công; rút thêm 1 thất bại; mọi lần thất bại đều không đổi state. Output: PASS: invariant and rejected transitions. Thử bỏ guard rồi chạy lại để thấy test bắt được lỗi.

Scope guarantee: mẫu bổ sung chỉ kiểm chuyển trạng thái tuần tự trên một object trong bộ nhớ. private không làm thao tác check-then-update trở thành atomic. Khi chia sẻ giữa threads, cần bảo vệ toàn bộ vùng kiểm tra và cập nhật bằng một cơ chế đồng bộ phù hợp; khi có persistence/nhiều process, cần thiết kế transaction/concurrency riêng. Không suy ra bảo đảm tài chính hay tính bền vững dữ liệu từ bài tập này. [O9]

2. Bốn trụ cột và điểm hay nhầm

Khái niệmCâu hỏi nó trả lờiVí dụ Java
EncapsulationAi được phép thấy/sửa state, và invariant được bảo vệ ở đâu?private field + method nghiệp vụ như withdraw.
AbstractionClient cần biết contract nào và có thể bỏ qua chi tiết nào?PaymentGateway.charge() che HTTP, retry, vendor SDK.
InheritanceKiểu con có thật sự là một kiểu cha và giữ nguyên contract không?CardPayment implements PaymentMethod.
PolymorphismCùng một lời gọi chạy implementation phù hợp với object runtime nào?gateway.charge() dispatch tới Stripe/PayPal adapter.
Phân biệt nhanh: encapsulation giấu và kiểm soát state; abstraction giấu độ phức tạp sau một contract. Hai khái niệm liên quan nhưng không đồng nghĩa.

Bổ sung · phân biệt contract và implementation

Một boundary phải nói cả cách thất bại

Trong ví dụ PaymentGateway.charge(), abstraction cho client một contract; encapsulation bảo vệ cách adapter giữ state và thông tin cấu hình. Đề xuất khi thiết kế contract: ghi rõ đầu vào, kết quả thành công, lỗi nghiệp vụ, timeout và trách nhiệm retry. Ẩn vendor SDK không có nghĩa là giấu luôn mọi kết quả thất bại.

Từ CardPayment implements PaymentMethod, hãy đọc là subtyping qua interface: một kiểu thực thi contract của kiểu khác, không phải kế thừa mutable state từ một superclass. Java tách hai việc này; bảng bốn trụ cột là mental model, không phải bốn keyword tương ứng một-một. [O1] [O2]

Tự kiểm: vẽ một caller và hai adapter. Đổi adapter mà không sửa caller; nêu một invariant nội bộ và một điều kiện lỗi nhìn thấy qua contract. Đây là evidence cụ thể cho cả abstraction lẫn encapsulation.

3. Overriding, overloading và dynamic dispatch

Ví dụ nguồn · trích đoạn trong method; giả định đã có PaymentMethod, CardPayment và biến order.

PaymentMethod method = new CardPayment();
method.pay(order); // Runtime dispatch gọi CardPayment.pay(...)

Bổ sung · đã đối chiếu tài liệu

Hai bước độc lập: chọn signature, rồi chọn implementation

Bài tập bổ sung · DispatchDemo.java · dự đoán hai dòng output trước khi chạy.

public class DispatchDemo {
    static class Base {
        String pay(Object order) { return "Base/Object"; }
    }

    static class Card extends Base {
        @Override
        String pay(Object order) { return "Card/Object"; }

        String pay(String order) { return "Card/String"; }
    }

    public static void main(String[] args) {
        Base method = new Card();
        System.out.println(method.pay("order")); // Card/Object
        Card card = new Card();
        System.out.println(card.pay("order"));   // Card/String
    }
}

Ở lời gọi đầu, static type Base khiến compiler chọn pay(Object); object thực là Card nên runtime chạy override của signature đó. Lời gọi thứ hai có static type Card, nên overload pay(String) được chọn. Runtime không quay lại chọn overload theo kiểu thật của đối số. [O4]

Không thể overload chỉ bằng kiểu trả về. static method có thể bị hide, không override; private method và constructor không được kế thừa để override. Dùng @Override để compiler phát hiện việc vô tình tạo overload thay vì override. [O1] [O2]

Bẫy lifecycle: lời gọi tới một overridable instance method từ constructor vẫn có thể dispatch vào subclass khi phần khởi tạo của subclass chưa xong. Ưu tiên không gọi các hook overridable trong constructor; đừng cho rằng constructor vô hiệu hóa dynamic dispatch. [O5]

4. “Is-a” và “has-a”

Inheritance dùng cho quan hệ is-a: mọi object con phải thay được object cha mà không phá kỳ vọng của caller. Composition dùng cho has-a/uses-a: object nhận collaborator qua constructor và ủy quyền công việc.

Ví dụ nguồn · CheckoutService · cần định nghĩa PaymentGateway của ứng dụng để compile.

public final class CheckoutService {
    private final PaymentGateway gateway;

    public CheckoutService(PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

Ưu tiên composition khi mục tiêu chỉ là tái sử dụng behavior hoặc thay implementation. Inheritance làm subclass phụ thuộc state/lifecycle của superclass và dễ tạo “fragile base class”.

Bài test Liskov: nếu code đang nhận base type mà phải hỏi instanceof, bỏ qua method, đổi precondition mạnh hơn hoặc trả kết quả trái contract cho một subtype, hierarchy có thể sai.

Bổ sung · quy tắc chẩn đoán và trade-off

Dùng substitutability để quyết định, không chỉ nhìn tên class

Một phép thử tự biên soạn: client nhận PaymentMethod và đã được phép gọi pay(order) với một order hợp lệ. Một subtype bất ngờ đòi client downcast trước, hoặc luôn ném lỗi cho thao tác mà base contract cam kết hỗ trợ, là tín hiệu cần xem lại hierarchy. Không tăng precondition và không làm yếu postcondition mà caller đang dựa vào.

Không biến dấu hiệu thành luật cấm: sự xuất hiện của instanceof chưa đủ chứng minh vi phạm LSP. Một pattern match ở boundary hoặc trên một sealed hierarchy có chủ đích có thể hoàn toàn hợp lệ. Câu hỏi chẩn đoán vẫn là: caller có bị phá contract khi thay subtype hay không?

Trong CheckoutService, constructor injection cho phép truyền adapter thật hoặc test double. Đề xuất thêm kiểm tra non-null cho dependency và kiểm thử cùng một contract trên các adapter. Trade-off: composition thêm bước ủy quyền và cấu hình; inheritance vẫn phù hợp khi quan hệ is-a, extension points và lifecycle thực sự ổn định.

Evidence: viết một bộ contract tests chạy cho ít nhất hai implementation; cùng đầu vào hợp lệ phải đáp ứng cùng postcondition. Không chỉ test từng concrete class bằng bộ giả định riêng.

5. Association, aggregation và composition

Quan hệ objectÝ nghĩa thực dụngLifecycle
AssociationMột object biết/dùng object khác.Không nói ownership.
AggregationWhole chứa part nhưng part có thể sống độc lập.Lifecycle tương đối độc lập.
CompositionWhole sở hữu part như một phần invariant.Part thường sinh/chết cùng whole.

Trong code thực tế, hãy nói rõ ownership, mutability và lifecycle. Quan hệ object không tự động quyết định cardinality hay cascade trong database.

Bổ sung · làm rõ phạm vi thuật ngữ

Ownership là một quyết định thiết kế, không phải hiệu ứng của cú pháp

“Composition” ở mục 4 được dùng theo nghĩa rộng: ghép behavior bằng collaborator. “Composition” trong bảng quan hệ object nhấn mạnh ownership/lifecycle. Chỉ nhìn một field hay constructor injection chưa đủ kết luận quan hệ lifecycle: gateway có thể được nhiều service dùng chung.

Khi review, ghi lại ai tạo part, ai được sửa nó, ai chịu trách nhiệm kết thúc sử dụng/đóng resource, và có alias nào giữ part ngoài whole hay không. final khóa việc gán lại reference, không tự đóng băng object được tham chiếu. [O1]

Hệ quả thực dụng: sơ đồ ownership không tự cấu hình cascade, transaction hay thứ tự xóa ở database; cũng không nên hiểu “sinh/chết cùng nhau” là lời hứa JVM sẽ thu gom các object cùng một thời điểm. Hãy kiểm tra alias và lifecycle thực tế thay vì suy ra từ một mũi tên UML.

6. Interface, abstract class và kiểu Java hiện đại

Bổ sung · đã đối chiếu tài liệu

Chọn kiểu theo điều cần bảo vệ

Lựa chọnPhù hợp khiChi phí / điều không được suy ra
InterfaceCần contract/capability với nhiều implementation.Không có instance state của interface; default/static/private methods không biến nó thành base class có constructor.
Abstract classMột family thật sự chia sẻ state và construction.Subclass gắn với lifecycle của base class; Java chỉ cho một direct superclass.
Sealed typeMuốn kiểm soát các direct subtype của mô hình.Nhánh non-sealed có thể mở lại khả năng mở rộng; không mặc định mọi leaf đều bị đóng.
RecordMuốn biểu diễn value carrier bằng components.Bất biến nông; component mutable vẫn cần ownership/defensive copy phù hợp.

Bảng này triển khai các lựa chọn ở phần nguồn; cú pháp và giới hạn class/interface theo JLS. [O1] [O2]

Version baseline: record và pattern matching cho instanceof là tính năng chính thức từ Java 16; sealed types từ Java 17; pattern matching cho switch từ Java 21. Ví dụ dưới dùng JDK 21 và không cần --enable-preview. Đây là baseline của bài, không phải khẳng định JDK mới nhất. [O6]

Bài tập bổ sung · ValueTypesDemo.java · defensive snapshot và exhaustive switch.

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public class ValueTypesDemo {
    record Labels(List<String> values) {
        Labels {
            values = List.copyOf(values);
        }
    }

    sealed interface PaymentResult permits Approved, Declined {}
    record Approved(String receiptId) implements PaymentResult {}
    record Declined(String reason) implements PaymentResult {}

    static String describe(PaymentResult result) {
        return switch (Objects.requireNonNull(result)) {
            case Approved a -> "approved:" + a.receiptId();
            case Declined d -> "declined:" + d.reason();
        };
    }

    public static void main(String[] args) {
        var original = new ArrayList<>(List.of("java"));
        var labels = new Labels(original);
        original.add("changed");
        if (!labels.values().equals(List.of("java"))) {
            throw new AssertionError("Alias leaked");
        }
        try {
            labels.values().add("oops");
            throw new AssertionError("Expected unmodifiable list");
        } catch (UnsupportedOperationException expected) {
            // The public accessor does not expose a mutable list.
        }
        System.out.println(labels.values());
        System.out.println(describe(new Approved("R1")));
        System.out.println(describe(new Declined("blocked")));
    }
}

Expected output: lần lượt là [java], approved:R1, declined:blocked. List.copyOf tạo kết quả không phản ánh những lần sửa cấu trúc list đầu vào về sau, không cho phép sửa qua accessor và từ chối null list/null elements. Đây không phải deep copy; bài dùng String immutable làm phần tử. Với component là một mutable object, cần quyết định copy đến mức nào. [O7]

Thử làm hỏng: bỏ List.copyOf để thấy alias leak; thêm một subtype mới vào sealed model và quan sát yêu cầu cập nhật switch. Objects.requireNonNull ở đây chủ động từ chối null; exhaustiveness theo subtype không thay thế chính sách null. [O4] [O6]

Liên hệ equality: Object.equals mặc định kiểm identity; value type cần contract equality phù hợp. Khi tự override equals, phải giữ consistency với hashCode. Đừng dùng mutable state đang thay đổi làm nền tảng cho key trong hash collection. [O8]

7. SOLID như công cụ chẩn đoán

Nguyên tắcDấu hiệu vi phạmHướng sửa thường gặp
SRPMột class đổi vì nhiều nhóm lý do không liên quan.Tách policy, persistence, formatting hoặc integration boundary.
OCPThêm loại mới phải sửa chuỗi if/switch rải rác.Đưa variation sau interface/strategy khi variation là thật.
LSPSubtype không giữ contract của base type.Sửa hierarchy, contract hoặc chuyển sang composition.
ISPClient phụ thuộc method nó không dùng.Tách interface theo capability của consumer.
DIPBusiness policy phụ thuộc trực tiếp framework/vendor detail.Dependency hướng vào abstraction thuộc core; adapter implement ở ngoài.

SOLID không yêu cầu mỗi class chỉ có một method hay mọi dependency đều cần interface. Chỉ tạo abstraction khi nó bảo vệ một boundary hoặc variation có ý nghĩa.

Bổ sung · phân tích thiết kế tự biên soạn

Từ một nguyên tắc đến bằng chứng thay đổi

Đề xuất cách dùng bảng SOLID: chọn một thay đổi đã xảy ra, liệt kê những nơi phải sửa, rồi xác định dependency nào khiến thay đổi lan rộng. Ví dụ đổi vendor thanh toán: ưu tiên giữ business policy và contract ổn định, để vendor mapping nằm trong adapter. Nếu chỉ thêm một lớp chuyển tiếp không bảo vệ được gì, abstraction đó chưa có lý do tồn tại.

DIP là định hướng dependency vào abstraction phù hợp; dependency injection chỉ là cách cung cấp object cộng tác. Một constructor nhận trực tiếp vendor SDK vẫn có injection, nhưng business policy có thể còn phụ thuộc vendor. Đây là phân tích thiết kế, không phải lỗi mà compiler tự bắt.

Trade-off với OCP: một switch tập trung trên sealed domain nhỏ, có chủ đích đóng, không tự động là thiết kế xấu. Cảnh báo trong bảng nguồn nhắm vào nhánh điều kiện rải rác và variation gây sửa nhiều nơi. Đừng mở rộng framework/plugin model chỉ để tránh mọi switch.

Evidence đề xuất: lưu diff của một thay đổi thật, contract tests trước/sau và một đoạn giải thích vì sao tách hoặc không tách boundary. Số interface không phải thước đo chất lượng thiết kế.

8. Checklist trả lời phỏng vấn

Bổ sung · interview evidence

Biến 6 mục checklist thành bài trả lời có bằng chứng

Mục nguồnBằng chứng tối thiểu để tự đánh giá
1. Invariant / contract / substitutabilityNêu một invariant; chỉ ra guard, postcondition và test cho cả thành công lẫn thất bại.
2. Abstraction ≠ encapsulationDùng cùng một ví dụ gateway để tách contract nhìn từ caller và state/chi tiết bên trong.
3. Overriding ≠ overloadingDự đoán đúng hai dòng DispatchDemo và giải thích static type ở bước chọn signature.
4. Inheritance và compositionĐưa một case is-a hợp lệ, một case chỉ uses-a và mô tả contract test dùng chung.
5. Interface / abstract / record / sealedNêu một trade-off mỗi loại; chạy thử alias leak và chỉ ra baseline Java của switch.
6. SOLID không hình thứcChỉ ra một diff hoặc tình huống thay đổi và giải thích boundary có ý nghĩa; không đo bằng số class.

Lệnh chạy các chương trình bổ sung sau khi lưu mỗi block vào đúng tên file.

javac --release 21 BankAccountCheck.java DispatchDemo.java ValueTypesDemo.java
java BankAccountCheck
java DispatchDemo
java ValueTypesDemo

Acceptance của buổi học: cả ba chương trình chạy đúng output đã ghi; người học giải thích được các lỗi khi bỏ guard hoặc bỏ defensive snapshot. Lưu source thử nghiệm, phiên bản java -version, stdout và câu trả lời cho 6 mục. Đây là bài kiểm chứng trong chuyên đề, không cộng vào tổng lab của Part 01.

Câu trả lời mẫu để tự diễn đạt lại: “Tôi dùng object để giữ state hợp lệ sau một contract. Tôi chọn inheritance khi subtype thay được base mà không làm sai kỳ vọng caller; nếu chỉ muốn thay collaborator hay tái sử dụng behavior, tôi cân nhắc composition. Sau đó tôi dùng test và thay đổi thực tế để kiểm thiết kế, thay vì chỉ đếm keyword OOP.”

Tài liệu tham khảo & phạm vi phiên bản

Đối chiếu ngày 12/09/2026. Dùng tài liệu Java SE/JDK 21 làm baseline; tài liệu lịch sử được ghi rõ. Các liên kết là nguồn đọc thêm, không phải dependency để hiển thị trang.

[O1] JLS 8 · Classes — Class, inheritance, overriding, constructor, record và sealed class. Bản Java SE 21.

[O2] JLS 9 · Interfaces — Interface, default/static/private methods và quan hệ implementation. Bản Java SE 21.

[O3] Oracle Java Object-Oriented Concepts — Giữ reference nguồn cho khái niệm nền. Tutorial này hướng tới JDK 8, không dùng để xác định mốc record/sealed/pattern switch.

[O4] JLS 15.12 · Method Invocation — Chọn method tại compile time và xác định method chạy tại runtime; chương 15 cũng quy định switch expressions.

[O5] JLS 12.5 · Creation of New Class Instances — Trình tự khởi tạo và dynamic dispatch trong construction.

[O6] Oracle · Java Language Changes — Mốc chính thức của record, sealed types và pattern matching cho switch.

[O7] Java 21 API · List.copyOf — Unmodifiable snapshot, không phản ánh thay đổi cấu trúc collection đầu vào và giới hạn null.

[O8] Java 21 API · Object.equals / hashCode — Identity mặc định và contract equality/hash code.

[O9] JLS 17 · Threads and Locks — Phân biệt encapsulation với đồng bộ và visibility giữa threads.