Java OO & Functional
6/38
Bài 6 / 38~12 phútKế thừa & Đa hìnhLộ trình · chặng 03/21Miễn phí… lượt xem

Interface — hợp đồng capability với default method

Interface là gì, đa kế thừa interface, default method Java 8+, private method Java 9+, functional interface, và so sánh chi tiết với abstract class.

TL;DR: Abstract class xác định bản chất đối tượng là gì (Is-a), còn Interface quy định đối tượng có khả năng làm được gì (Can-do). Java hỗ trợ đa kế thừa interface, default/private method để mở rộng thư viện an toàn mà không phá vỡ mã nguồn cũ, cùng Functional Interface làm nền tảng cho Lambda Expression.

Ở bài trước, chúng ta đã dùng abstract class LibraryItem để dựng khung xương cốt lõi ("Is-a"): mọi ấn phẩm đều có mã số, tiêu đề và cách tính phí trễ hạn. Nhưng trong thực tế, một thực thể không chỉ có một bản chất duy nhất, mà còn sở hữu nhiều năng lực ("Can-do") độc lập: sách in mượn được, tài liệu số tải về được, bài báo lập chỉ mục tìm kiếm được.

Java cấm một class kế thừa nhiều class cha để loại trừ rủi ro tranh chấp trạng thái, nhưng cho phép class implement nhiều interface. Đây là giải pháp cho bài toán một đối tượng mang nhiều vai trò: DigitalEbook vừa là ấn phẩm thư viện, vừa có thể tải xuống (DigitalDownloadable), vừa hỗ trợ tìm kiếm toàn văn (Searchable).

1. Tại sao một class cần nhiều vai trò thay vì một cây kế thừa?

Trong đời thực, một người có chứng chỉ lái xe, chứng chỉ ngoại ngữ và chứng chỉ cứu hộ. Mỗi chứng chỉ chứng minh một năng lực hành động cụ thể. Cơ quan tuyển dụng chỉ cần biết ứng viên có chứng chỉ tương ứng hay không để giao việc, không cần ép tất cả nhân viên phải chung một nguồn gốc gia phả.

Một class chỉ có một cha LibraryItem để kế thừa bản sắc nhưng có thể mang nhiều vai trò qua interface Searchable và DigitalDownloadable

Đời thựcLập trình JavaHệ thống quản lý thư viện
Chứng chỉ năng lựcinterfaceSearchable, Borrowable
Người có nhiều chứng chỉClass implement nhiều interfacePrintedBook implements Borrowable, Searchable
Yêu cầu trên chứng chỉAbstract method trong interfaceList<String> searchKeywords();
Hướng dẫn thực hiện mẫudefault methoddefault boolean matches(String query) { ... }
Tư duy thiết kế

Hãy tự hỏi: Thực thể này là gì (Is-a -> dùng class/abstract class) hay thực thể này làm được gì (Can-do -> dùng interface)? Nếu một khả năng có thể gắn cho nhiều đối tượng khác họ hàng (ví dụ sách in và bài giảng video đều tìm kiếm được), khả năng đó bắt buộc phải là một interface.

2. Cú pháp interface và quy ước ngầm định

Một interface năng lực tìm kiếm trong thư viện:

Java
public interface Searchable {
    // Ngam dinh: public static final int MAX_KEYWORD_LENGTH = 100;
    int MAX_KEYWORD_LENGTH = 100;

    // Ngam dinh: public abstract List<String> searchKeywords();
    List<String> searchKeywords();
}

Các quy tắc cốt lõi:

  1. Mọi phương thức khai báo không có body ngầm định là public abstract.
  2. Mọi thuộc tính (field) trong interface ngầm định là public static final (hằng số toàn cục). Interface không chứa instance field (không lưu trạng thái đối tượng).
  3. Interface không có constructor và không thể khởi tạo trực tiếp bằng toán tử new.

Lớp cụ thể hiện thực năng lực:

Java
public class PrintedBook extends LibraryItem implements Searchable {
    private final int pageCount;
    private final String shelfLocation;

    public PrintedBook(String id, String title, String author, int pageCount, String shelfLocation) {
        super(id, title, author);
        this.pageCount = pageCount;
        this.shelfLocation = shelfLocation;
    }

    @Override
    public double calculateLateFee(int daysOverdue) {
        return daysOverdue * 2000.0;
    }

    @Override
    public List<String> searchKeywords() {
        return List.of(getTitle().toLowerCase(), getAuthor().toLowerCase(), shelfLocation.toLowerCase());
    }
}

3. Class ghép nhiều capability qua đa kế thừa interface như thế nào?

Một tài liệu số (DigitalEbook) vừa là ấn phẩm thư viện, vừa tìm kiếm được, vừa có khả năng tải về máy:

Java
public interface DigitalDownloadable {
    String getDownloadUrl();
    long getFileSizeBytes();
}

public class DigitalEbook extends LibraryItem implements Searchable, DigitalDownloadable {
    private final String downloadUrl;
    private final long fileSize;

    public DigitalEbook(String id, String title, String author, String downloadUrl, long fileSize) {
        super(id, title, author);
        this.downloadUrl = downloadUrl;
        this.fileSize = fileSize;
    }

    @Override
    public double calculateLateFee(int daysOverdue) {
        return 0.0; // Tai lieu so tu het han, khong tinh phi vat ly
    }

    @Override
    public List<String> searchKeywords() {
        return List.of(getTitle().toLowerCase(), getAuthor().toLowerCase(), "ebook", "pdf");
    }

    @Override public String getDownloadUrl() { return downloadUrl; }
    @Override public long getFileSizeBytes() { return fileSize; }
}

Interface cũng có thể kế thừa interface khác qua từ khoá extends:

Java
public interface BorrowableItem extends Searchable {
    int maxBorrowDays();
    boolean isAvailable();
}

4. Default method tiến hoá API mà không làm hỏng class cũ ra sao?

Trước Java 8, nếu bạn thêm một phương thức mới vào interface đã công bố (ví dụ thêm matches(String query) vào Searchable), tất cả các lớp đang implement Searchable trên toàn hệ thống lập tức bị lỗi biên dịch.

Java 8 giải quyết bằng default method:

Java
public interface Searchable {
    List<String> searchKeywords();

    // Default method: Dien dat qua abstract method searchKeywords()
    default boolean matches(String query) {
        if (query == null || query.isBlank()) return false;
        String normalized = query.trim().toLowerCase();
        return searchKeywords().stream()
                .anyMatch(keyword -> keyword.contains(normalized));
    }
}

Các lớp cũ vẫn biên dịch bình thường và ngay lập tức dùng được hàm matches. Nếu muốn tối ưu riêng (ví dụ tra theo B-tree), lớp con có thể chủ động @Override.

JDK đã tận dụng cơ chế này để nâng cấp toàn bộ Collection Framework: Iterable.forEach(), Collection.stream(), Collection.removeIf(), Map.getOrDefault().

Xử lý xung đột Diamond Problem khi trùng default method

Khi một lớp implement hai interface có cùng tên và chữ ký default method:

Java
public interface ArchiveRecord {
    default String formatSummary() { return "Archive Record Default"; }
}

public interface CatalogEntry {
    default String formatSummary() { return "Catalog Entry Default"; }
}

public class Manuscript implements ArchiveRecord, CatalogEntry {
    // Bat buoc phai override de tu giai quyet xung dot
    @Override
    public String formatSummary() {
        return ArchiveRecord.super.formatSummary() + " | " + CatalogEntry.super.formatSummary();
    }
}

Trình biên dịch Java từ chối đoán mò giúp lập trình viên. Bạn bắt buộc phải ghi đè phương thức và chỉ định rõ qua cú pháp <InterfaceName>.super.<method>().

5. Static và Private method trong interface

Từ Java 8, interface cho phép phương thức static để làm hàm tiện ích hoặc Factory method:

Java
public interface Searchable {
    List<String> searchKeywords();

    static boolean isQueryValid(String query) {
        return query != null && !query.trim().isEmpty();
    }
}

Phương thức static của interface chỉ gọi qua tên interface (ví dụ Searchable.isQueryValid("java")), không kế thừa xuống lớp con.

Từ Java 9, interface hỗ trợ phương thức private. Đây là các hàm trợ thủ gom logic dùng chung giữa nhiều default method, tránh lặp mã và không bị lộ ra ngoài:

Java
public interface DataValidator {
    default boolean validateLenient(String input) { return checkBasic(input); }
    default boolean validateStrict(String input) { return checkBasic(input) && input.length() >= 5; }

    private boolean checkBasic(String input) {
        return input != null && !input.isBlank();
    }
}

6. Functional Interface: Nền tảng của Lambda Expression

Một interface chỉ có duy nhất một abstract method được gọi là Functional Interface. Java 8 cho phép truyền trực tiếp biểu thức Lambda hoặc Method Reference vào bất kỳ nơi nào mong đợi functional interface:

Java
@FunctionalInterface
public interface ItemFilter {
    boolean test(LibraryItem item);
}

// Su dung voi Lambda expression gon gang
ItemFilter filter = item -> item.getTitle().startsWith("Java");

JDK cung cấp sẵn các functional interface chuẩn trong package java.util.function:

  • Predicate<T>: kiểm tra điều kiện (boolean test(T t))
  • Function<T, R>: biến đổi dữ liệu (R apply(T t))
  • Consumer<T>: tiêu thụ dữ liệu mà không trả về (void accept(T t))
  • Supplier<T>: cung cấp dữ liệu mới (T get())

7. So sánh thực tế: Khi nào chọn Interface, khi nào chọn Abstract Class?

Tiêu chíInterface (Capability Contract)Abstract Class (Template Base)
Kế thừaĐa hiện thực (implements A, B, C)Đơn kế thừa (extends BaseOnly)
Trạng thái (State)Không có instance field, chỉ có hằng số public static finalCó đầy đủ instance field (private, protected) để lưu state
ConstructorKhông có constructorCó constructor để khởi tạo trạng thái của lớp cha
Mục đích thiết kếĐịnh nghĩa đối tượng làm được gì ("Can-do")Định nghĩa bản chất danh tính đối tượng ("Is-a")
LambdaTương thích hoàn hảo nếu là Functional InterfaceKhông dùng được với cú pháp Lambda

Lời khuyên thiết kế từ Effective Java (Item 20)

"Prefer interfaces to abstract classes" (Ưu tiên dùng interface thay vì abstract class).

Interface giữ thiết kế linh hoạt, không chiếm dụng khe kế thừa duy nhất và dễ dàng retrofit vào các class sẵn có. Chỉ dùng abstract class khi:

  1. Bạn cần chia sẻ trạng thái phức tạp (nhiều instance field gắn kết nhau).
  2. Bạn cần kiểm soát chặt chẽ vòng đời qua constructor của lớp cha.
  3. Bạn muốn áp dụng pattern Skeletal Implementation.

8. Pattern: Interface kết hợp Skeletal Abstract Class

Đây là kiến trúc kinh điển được JDK áp dụng trên Collections Framework (List + AbstractList):

  1. Định nghĩa Interface tạo hợp đồng linh hoạt cao nhất.
  2. Cung cấp một Abstract Class cài đặt sẵn phần lớn logic khung.
Java
public interface CatalogExporter {
    String generateReport(List<LibraryItem> items);
    String getMimeType();
}

public abstract class AbstractCatalogExporter implements CatalogExporter {
    @Override
    public String generateReport(List<LibraryItem> items) {
        StringBuilder sb = new StringBuilder();
        sb.append(renderHeader()).append("\n");
        for (LibraryItem item : items) {
            sb.append(renderItem(item)).append("\n");
        }
        sb.append(renderFooter());
        return sb.toString();
    }

    protected abstract String renderHeader();
    protected abstract String renderItem(LibraryItem item);
    protected String renderFooter() { return "--- End of Report ---"; }
}

public class PlainTextCatalogExporter extends AbstractCatalogExporter {
    @Override public String getMimeType() { return "text/plain"; }
    @Override protected String renderHeader() { return "=== DANH MUC AN PHAM ==="; }
    @Override protected String renderItem(LibraryItem item) {
        return "[" + item.getId() + "] " + item.getTitle();
    }
}

Nếu một class xuất báo cáo đã trót kế thừa một base class khác, lập trình viên vẫn có thể implements CatalogExporter trực tiếp mà không bị bế tắc kế thừa.

9. Những cạm bẫy thường gặp khi thiết kế Interface

❌ Bẫy 1: Khai báo field với ý định lưu trạng thái đối tượng

Java
public interface Config {
    int TIMEOUT = 30; // Thuc chat la public static final
}

Mọi field trong interface đều là hằng số static. Đừng dùng interface để định nghĩa cấu hình mutable.

❌ Bẫy 2: Thêm abstract method vào interface đã phát hành ra bên ngoài

Java
public interface Searchable {
    List<String> searchKeywords();
    double calculateRelevance(String query); // Pha vo code cua tat ca client dang dung
}

Khắc phục: Dùng default method để giữ backward compatibility.

❌ Bẫy 3: Lạm dụng Default Method để nhồi nhét nghiệp vụ phụ thuộc state Default method không thể truy cập trực tiếp biến instance. Nếu phương thức gọi lòng vòng qua hàng loạt getter/setter giả định, hãy chuyển nó sang Abstract Class hoặc Domain Service.

10. Deep Dive Oracle & OpenJDK References

Tài liệu tham khảo đặc tả
  • JLS §9 (Interfaces): Đặc tả kỹ thuật chính thức về quy tắc phân giải method của interface.
  • JEP 335 (Default Methods): Cơ sở thiết kế thêm default method vào Java 8.
  • JEP 213 (Private Interface Methods): Phương thức private trong interface từ Java 9.
  • Effective Java (Joshua Bloch): Item 20: Prefer interfaces to abstract classes; Item 21: Design interfaces for posterity.

11. Tự kiểm tra kiến thức

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Đoạn mã sau có biên dịch thành công không và tại sao? interface Reader { default void read() { System.out.println("Read"); } } interface Scanner { default void read() { System.out.println("Scan"); } } class MultiTool implements Reader, Scanner { }
  2. Q2
    Tại sao field trong interface luôn mặc định là public static final mà không thể là private hoặc protected?
  3. Q3
    Một interface có 1 abstract method, 2 default methods và 1 static method thì có được coi là Functional Interface không?
  4. Q4
    Pattern Skeletal Implementation mang lại lợi ích gì vượt trội so với việc chỉ dùng đơn thuần một Abstract Class?
  5. Q5
    Tại sao Effective Java Item 21 cảnh báo lập trình viên phải thận trọng khi thiết kế default method cho interface?

Bài tiếp theo: Sealed class — kiểm soát chính xác subclass + exhaustive pattern matching

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 3/21 · OOP: class, kế thừa, đa hình, abstraction — bài 13/16 của chặng

Ôn phỏng vấn

Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Sealed class — kiểm soát tập subtype với pattern matching exhaustive