Command Palette

Search for a command to run...

[Java Basics] Interface trong Java và so sánh với abstract class

Interface là một danh sách những việc mà một type làm được, và nó không nói gì về việc type đó cái gì. Nó không có field để giữ state, không có constructor để chạy, không có gì để kế thừa — và chính vì vậy một class được phép implement bao nhiêu interface tùy ý, trong khi chỉ được extends đúng một class.

Sự bất đối xứng đó là toàn bộ nội dung bài này. Inheritance trả lời câu hỏi "is a" và bạn chỉ có một câu trả lời; interface trả lời câu hỏi "can do" và bạn có bao nhiêu câu trả lời cũng được. Bài viết đi qua cách khai báo, những modifier mà compiler tự thêm vào, default, staticprivate method mang lại điều gì và vì sao, diamond problem cùng cách giải quyết, rồi khép lại bằng phần so sánh với abstract class — thứ quyết định bạn thực sự nên viết cái nào.

Một class box đứng cạnh ba interface plug: extends chỉ nhận một, implements nhận nhiều

Mọi message lỗi và mọi dòng output bên dưới đều lấy từ việc compile và chạy code thật trên OpenJDK 21.0.6. Các mốc release cho default, staticprivate interface method được ghi là Java 8 và Java 9 vì đó là lúc ngôn ngữ có chúng, không phải vì bài dùng một compiler khác.

Interface là gì: một contract không có identity riêng

Interface khai báo signature của các method. Class nào viết implements là cam kết cung cấp body cho từng method đó, và đổi lại class được dùng ở bất cứ chỗ nào cần type của interface.

interface Drawable {
    void draw();
    double area();
}

class Circle implements Drawable {
    private final double r;

    Circle(double r) { this.r = r; }

    @Override
    public void draw() {
        System.out.println("Circle r=" + r);
    }

    @Override
    public double area() {
        return Math.PI * r * r;
    }
}

class Square implements Drawable {
    private final double side;

    Square(double side) { this.side = side; }

    @Override
    public void draw() {
        System.out.println("Square side=" + side);
    }

    @Override
    public double area() {
        return side * side;
    }
}

public class Basic {
    public static void main(String[] args) {
        Drawable[] shapes = { new Circle(2), new Square(3) };
        for (Drawable d : shapes) {
            d.draw();
            System.out.printf("  area = %.2f%n", d.area());
        }
        System.out.println("Circle is a Drawable? " + (shapes[0] instanceof Drawable));
    }
}
Circle r=2.0
  area = 12.57
Square side=3.0
  area = 9.00
Circle is a Drawable? true

CircleSquare không chung superclass, không chung field, không chung một body method nào. Thứ chúng chung là một khả năng, và mảng khai báo kiểu Drawable[] chứa được cả hai vì cả hai đều thỏa contract.

Hai keyword nói hai chuyện khác nhau. extends nghĩa là "là một dạng cụ thể hơn của", và nó kéo theo field cùng body method của superclass. implements nghĩa là "làm được việc này", và nó không mang theo gì ngoài nghĩa vụ. Hãy thử đọc thành câu: một Circle là một Shape, đó là inheritance; một Circle vẽ được, đó là interface. Khi câu chỉ xuôi với chữ "được" thì thứ bạn cần là interface.

Khai báo interface và implement nó

Khai báo nằm trong một file .java như mọi thứ khác, chỉ thay class bằng interface:

interface Drawable {
    void draw();
    double area();
}

Không public, không abstract, không body. Sau đó class liệt kê các interface nó thỏa mãn sau implements, ngăn cách bằng dấu phẩy, và viết body cho từng method. @Override không bắt buộc, nhưng nó biến một lỗi gõ nhầm signature thành lỗi compile thay vì một method thừa nằm im.

Lỗi khi một class không implement đủ method

Bỏ sót một cái là class chưa hoàn chỉnh. javac gọi tên chính xác method còn thiếu:

class Triangle implements Drawable {
    @Override
    public void draw() {
        System.out.println("Triangle");
    }
}
Missing.java:6: error: Triangle is not abstract and does not override abstract method area() in Drawable
class Triangle implements Drawable {
^
1 error

Cách diễn đạt của message đáng đọc kỹ. Compiler không nói "bạn quên gì đó"; nó nói class này là concrete trong khi một method của nó vẫn chưa có body, và nó gợi ý luôn lối thoát: khai báo Triangle là abstract thì lỗi biến mất, vì abstract class được phép để lại method chưa implement cho subclass của nó làm nốt.

Member của interface ngầm định là public, field ngầm định là public static final

Bạn không bao giờ phải viết public abstract cho method của interface hay public static final cho field, vì compiler viết hộ. Reflection cho thấy đúng những modifier đã nằm trong class file:

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

interface Config {
    int MAX_RETRIES = 3;
    void reload();
}

public class Modifiers {
    public static void main(String[] args) throws Exception {
        Field f = Config.class.getDeclaredField("MAX_RETRIES");
        System.out.println("field  MAX_RETRIES -> " + Modifier.toString(f.getModifiers()));
        Method m = Config.class.getDeclaredMethod("reload");
        System.out.println("method reload()    -> " + Modifier.toString(m.getModifiers()));
    }
}
field  MAX_RETRIES -> public static final
method reload()    -> public abstract

Cả hai hệ quả đều bị compiler ép ngay lúc compile, và cả hai đều làm nhiều người bất ngờ.

Giảm visibility của method đã implement

Vì method của interface vốn đã là public, phần implement không được hẹp hơn. Bỏ modifier đi thì method thành package-private, tức là hẹp hơn:

interface Greeter {
    void greet();
}

class Quiet implements Greeter {
    @Override
    void greet() {
        System.out.println("hi");
    }
}
Weaker.java:7: error: greet() in Quiet cannot implement greet() in Greeter
    void greet() {
         ^
  attempting to assign weaker access privileges; was public
1 error

Đây là lỗi compile phổ biến nhất khi người mới quên public ở method implement. Luật đằng sau nó có tính tổng quát: một override được phép mở rộng access nhưng không bao giờ được thu hẹp, nếu không thì phía gọi đang cầm reference kiểu interface sẽ với tới được một method mà object từ chối cho thấy.

Field của interface là hằng số, không phải state

int MAX_RETRIES = 3; bên trong interface không phải field của các object implement nó. Nó là một giá trị static final duy nhất dùng chung cho tất cả, và không gán lại được:

interface Config {
    int MAX_RETRIES = 3;
    String NAME = "api";
}

public class Constants {
    public static void main(String[] args) {
        System.out.println(Config.MAX_RETRIES + " " + Config.NAME);
        Config.MAX_RETRIES = 5;
    }
}
Constants.java:9: error: cannot assign a value to static final variable MAX_RETRIES
        Config.MAX_RETRIES = 5;
              ^
1 error

Cố dùng nó làm state riêng của từng object cũng hỏng y hệt, kể cả khi viết từ bên trong một default method:

interface Counter {
    int count = 0;

    default void increment() {
        count = count + 1;
    }
}
MutableState.java:5: error: cannot assign a value to static final variable count
        count = count + 1;
        ^
1 error

⚠️ Interface hoàn toàn không giữ được state thay đổi được. Nếu thiết kế cần một counter, một cache hay một connection dùng chung giữa các subclass thì đó là abstract class có field, không phải interface có hằng số.

Interface cũng từ chối hai thứ còn lại mà class dùng để dựng state. Constructor là lỗi cú pháp, vì không có object nào để dựng:

NoCtor.java:2: error: <identifier> expected
    Session(String user);
           ^
1 error

Và instance field cũng là lỗi cú pháp, vì mọi khai báo field đều ngầm là final nên bắt buộc phải có giá trị khởi tạo:

InstanceField.java:2: error: = expected
    private String user;
                       ^
1 error

Một class, nhiều interface

Một class chỉ được extends đúng một class. Viết hai cái là parser bỏ cuộc ngay ở dấu phẩy:

TwoParents.java:4: error: '{' expected
class Robot extends Machine, Vehicle { }
                           ^
1 error

implements thì nhận cả danh sách, và mỗi tên trên danh sách đó là thêm một type mà object có thể được truyền đi dưới dạng đó:

interface Printable {
    void print();
}

interface Storable {
    String serialize();
}

interface Rankable {
    int rankAgainst(Rankable other);
    int weight();
}

class Report implements Printable, Storable, Rankable {
    private final String title;
    private final int pages;

    Report(String title, int pages) {
        this.title = title;
        this.pages = pages;
    }

    @Override
    public void print() {
        System.out.println("printing " + title + " (" + pages + " pages)");
    }

    @Override
    public String serialize() {
        return "{\"title\":\"" + title + "\",\"pages\":" + pages + "}";
    }

    @Override
    public int weight() { return pages; }

    @Override
    public int rankAgainst(Rankable other) {
        return Integer.compare(weight(), other.weight());
    }
}

Mỗi call site khi đó chỉ khai báo đúng khả năng nó cần, và không bao giờ biết Report còn có hai khả năng kia:

static void render(Printable p) { p.print(); }
static void save(Storable s) { System.out.println("saving " + s.serialize()); }
static void rank(Rankable a, Rankable b) { System.out.println("rank -> " + a.rankAgainst(b)); }

public static void main(String[] args) {
    Report q3 = new Report("Q3", 12);
    Report q4 = new Report("Q4", 30);

    render(q3);
    save(q3);
    rank(q3, q4);

    System.out.println("Printable? " + (q3 instanceof Printable));
    System.out.println("Storable?  " + (q3 instanceof Storable));
    System.out.println("Rankable?  " + (q3 instanceof Rankable));
}
printing Q3 (12 pages)
saving {"title":"Q3","pages":12}
rank -> -1
Printable? true
Storable?  true
Rankable?  true

Một mũi tên extends đối lại ba mũi tên implements, và cùng một object đi vào ba call site qua ba type khác nhau

Đây chính là thứ single inheritance không làm được. Report thật sự có ba type ngoài type của chính nó, và mỗi type là một mô tả hẹp, trung thực về đúng thứ mà một phía gọi cụ thể cần.

Một interface có thể extends nhiều interface

Giữa các interface với nhau thì dùng extends, và ở đó danh sách được phép dài hơn một, vì không có state nào phải gộp:

interface Loadable { String load(); }
interface Savable  { void save(String data); }
interface Closable { void close(); }

interface Channel extends Loadable, Savable, Closable {
    boolean isOpen();
}

class MemoryChannel implements Channel {
    private final StringBuilder buf = new StringBuilder();
    private boolean open = true;

    @Override public String load() { return buf.toString(); }
    @Override public void save(String data) { buf.append(data); }
    @Override public void close() { open = false; }
    @Override public boolean isOpen() { return open; }
}
load  -> hello interfaces
open  -> true
open  -> false
as Loadable -> hello interfaces
Channel superinterfaces -> [interface Loadable, interface Savable, interface Closable]

MemoryChannel chỉ viết implements Channel một lần và nợ bốn method. Phía gọi nào chỉ đọc thì nhận Loadable, và vẫn là object đó đi vào, không cần sửa gì.

Interface đã được bổ sung những gì qua các phiên bản

Suốt mười tám năm đầu, một interface chỉ chứa được đúng hai thứ: signature của abstract method và hằng số. Ba bổ sung sau đó đã thay đổi điều này, và mỗi bổ sung đều có một lý do rất cụ thể.

Trục thời gian từ Java 1.0 đến Java 9: abstract method và hằng số, rồi default và static method, rồi private method

interface Validator {
    // Since Java 1.0: abstract methods and constants.
    String NAME = "validator";
    boolean isValid(String input);

    // Java 8: a default method has a body and is inherited by every implementor.
    default String describe(String input) {
        return tag() + (isValid(input) ? " OK   " : " FAIL ") + quote(input);
    }

    // Java 8: a static method belongs to the interface, not to implementors.
    static Validator notBlank() {
        return input -> input != null && !input.isBlank();
    }

    // Java 9: a private method shares code between default methods
    // without exposing it on the public contract.
    private String tag() {
        return "[" + NAME + "]";
    }

    private static String quote(String s) {
        return "\"" + s + "\"";
    }
}

class MaxLength implements Validator {
    private final int max;
    MaxLength(int max) { this.max = max; }
    @Override public boolean isValid(String input) { return input != null && input.length() <= max; }
}
[validator] OK   "java"
[validator] FAIL "interfaces"
[validator] FAIL "   "
[validator] OK   "x"

MaxLength chỉ implement một method và được tặng luôn describe. Validator.notBlank() trả về một implementation mà không có class nào được đặt tên cả.

Vì sao cần default method

Động cơ không phải là cho tiện. Nó là vì trước đây, thêm một method vào một interface đã phát hành sẽ phá vỡ mọi class đang implement nó, mà Java 8 lại cần thêm method vào java.util.Collection cùng họ hàng mà không được phá cả thế giới.

Bắt đầu từ một interface đã có implementor được compile theo nó:

public interface Sink {
    void accept(String line);
}

public class ConsoleSink implements Sink {
    @Override
    public void accept(String line) {
        System.out.println("> " + line);
    }
}

Giờ thêm một method dạng default, rồi compile lại chỉ mỗi interface:

import java.util.List;

public interface Sink {
    void accept(String line);

    // Added later. `default` means every existing implementor already has it.
    default void acceptAll(List<String> lines) {
        for (String line : lines) accept(line);
    }
}
only Sink.java recompiled
ConsoleSink.class byte-identical: 8052f837c422fe5ac7d9ef547dab5b74
> one
> two
> three

ConsoleSink.class không hề bị đụng vào — checksum của nó vẫn là checksum sinh ra từ interface cũ — vậy mà sink.acceptAll(List.of("two", "three")) vẫn chạy và vẫn dispatch ngược về accept cũ. Đó là toàn bộ lý do tồn tại của tính năng này.

Khai báo đúng method đó dạng abstract thì implementor cũ compile không nổi nữa:

public interface Sink {
    void accept(String line);

    void acceptAll(List<String> lines);   // abstract, not default
}
ConsoleSink.java:1: error: ConsoleSink is not abstract and does not override abstract method acceptAll(List<String>) in Sink
public class ConsoleSink implements Sink {
       ^
1 error

Một keyword là ranh giới giữa một thay đổi tương thích và một thay đổi phá vỡ. Vì vậy hãy dùng default khi bạn mở rộng một interface mà người khác đang implement — và cũng vì vậy đừng coi nó là chỗ chứa code chung, bởi default method vẫn không chạm được vào state nào.

static method thuộc về interface, không phải implementor

static method của interface được gọi qua tên interface và không được class implement kế thừa:

System.out.println(Validator.notBlank().isValid("x"));   // fine
System.out.println(MaxLength.notBlank().isValid("x"));   // not inherited
StaticNotInherited.java:13: error: cannot find symbol
        System.out.println(MaxLength.notBlank().isValid("x"));   // not inherited
                                    ^
  symbol:   method notBlank()
  location: class MaxLength
1 error

Đó là chủ ý: nó giữ cho namespace của class implement sạch sẽ, và nó cho phép một static factory nằm ngay cạnh type mà nó tạo ra, thay vì bị đẩy vào một class XxxUtils riêng.

private method, có từ Java 9

Khi đã có default method thì hai method loại đó thường muốn dùng chung một helper — và trước Java 9, cách duy nhất là để helper đó public, tức là đẩy vĩnh viễn một chi tiết cài đặt lên contract. private method của interface giải quyết chuyện này. Compile Validator ở trên theo Java 8 và compiler nói thẳng phiên bản nào đã đưa nó vào:

Evolution.java:18: error: private interface methods are not supported in -source 8
    private String tag() {
                      ^
  (use -source 9 or higher to enable private interface methods)
1 error

Có một loại member mà default method không bao giờ được cung cấp: bất cứ thứ gì đã khai báo trên java.lang.Object. Mọi class đều kế thừa chúng từ Object, nên một default của interface không bao giờ thắng được.

DefaultToString.java:2: error: default method toString in interface Named overrides a member of java.lang.Object
    default String toString() { return "named"; }
                   ^
1 error

Diamond problem với default method

Cho một class implement hai interface cùng cung cấp một default method giống hệt nhau, và không có luật nào khiến một trong hai là đáp án đúng. C++ xử lý tình huống này bằng virtual inheritance cùng một bộ luật mà phần lớn người dùng không bao giờ nắm hết. Java thì từ chối xử lý luôn:

interface Logger {
    default String prefix() { return "[log] "; }
}

interface Auditor {
    default String prefix() { return "[audit] "; }
}

class Service implements Logger, Auditor {
}
Diamond.java:9: error: types Logger and Auditor are incompatible;
class Service implements Logger, Auditor {
^
  class Service inherits unrelated defaults for prefix() from types Logger and Auditor
1 error

Từ đáng chú ý là unrelated. Nếu một interface extends interface kia thì default cụ thể hơn sẽ thắng và không có lỗi nào — interface B extends A có override default sẽ khiến class C implements A, B in ra B. Lỗi chỉ xuất hiện khi không ứng viên nào cụ thể hơn ứng viên nào, tức là đúng trường hợp mà việc đoán bừa sẽ tùy tiện.

Xung đột chưa giải quyết với message javac thật, cách giải bằng Interface.super cùng output, và luật class-wins

Giải quyết bằng Interface.super.method()

Class override method đó và chỉ rõ nó muốn body kế thừa nào. Interface.super.method() gọi đúng một superinterface trực tiếp:

class Service implements Logger, Auditor {
    @Override
    public String prefix() {
        return Logger.super.prefix() + Auditor.super.prefix();
    }
}

class LogOnly implements Logger, Auditor {
    @Override
    public String prefix() {
        return Logger.super.prefix();
    }
}
[log] [audit] both
[log] picked one

Bạn được phép gộp cả hai, chọn một, hoặc bỏ cả hai và viết cái khác. Compiler chỉ đòi bạn phải quyết định. Lưu ý interface được gọi tên phải là superinterface trực tiếp của class:

BadSuper.java:5: error: not an enclosing class: A
    @Override public String who() { return A.super.who(); }
                                            ^
1 error

Luật class-wins

Khi cuộc tranh chấp diễn ra giữa một method của superclass và một default của interface thì không có gì mơ hồ để báo lỗi: class luôn thắng, và default thậm chí không được xét tới.

interface Greeter {
    default String greet() { return "hello from the interface"; }
}

class Base {
    public String greet() { return "hello from the superclass"; }
}

class Child extends Base implements Greeter {
}
as Child   -> hello from the superclass
as Base    -> hello from the superclass
as Greeter -> hello from the superclass

Cast object sang Greeter cũng không đổi gì, vì luật này nói về body nào được kế thừa, không phải về reference type nào bạn đang cầm. Luật vẫn đúng ngay cả khi method của class không có body riêng — một method abstract ở superclass vẫn thắng default, và subclass khi đó bắt buộc phải implement nó:

AbstractWins.java:9: error: Child is not abstract and does not override abstract method greet() in Base
class Child extends Base implements Greeter {
^
1 error

Functional interface và lambda

Một interface có đúng một abstract method là functional interface, và một lambda expression chính là một implementation của nó. @FunctionalInterface là annotation tùy chọn, chỉ để bắt compiler kiểm tra con số đó:

@FunctionalInterface
interface Transform {
    String apply(String input);
}

public class Functional {
    static String run(Transform t, String input) {
        return t.apply(input);
    }

    public static void main(String[] args) {
        Transform upper = s -> s.toUpperCase();
        Transform reverse = s -> new StringBuilder(s).reverse().toString();

        System.out.println(run(upper, "interface"));
        System.out.println(run(reverse, "interface"));

        Transform anonymous = new Transform() {
            @Override
            public String apply(String input) { return "<" + input + ">"; }
        };
        System.out.println(run(anonymous, "interface"));
        System.out.println("lambda class    -> " + upper.getClass().getInterfaces()[0].getSimpleName());
    }
}
INTERFACE
ecafretni
<interface>
lambda class    -> Transform

Thêm một abstract method thứ hai là annotation làm hỏng build luôn:

NotFunctional.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  Transform is not a functional interface
    multiple non-overriding abstract methods found in interface Transform
1 error

default, staticprivate method không được tính vào con số một đó; chỉ abstract method được tính. Lambda, method reference và các type dựng sẵn trong java.util.function là một chủ đề lớn và thuộc về khóa nâng cao. Điều duy nhất cần nắm ở đây là mối liên hệ: lambda không phải một loại giá trị mới, nó là một object implement một interface chỉ có một method, và interface chính là thứ cho nó một type.

Marker interface

Marker interface không khai báo gì cả. Toàn bộ nội dung của nó là cái tên, và code kiểm tra bằng instanceof:

interface Auditable { }   // no members at all

class Order implements Auditable {
    final int id;
    Order(int id) { this.id = id; }
}

class Draft { }
Order audited? true
Draft audited? false
Auditable declares 0 methods

Ví dụ ngay trong JDK là java.io.Serializablejava.lang.Cloneable, và bạn đã dùng cả hai mà không để ý — mọi kiểu mảng đều implement chúng:

int[]     -> [interface java.lang.Cloneable, interface java.io.Serializable]

Marker là kỹ thuật có từ trước annotation: ngày nay cùng thông tin đó thường được mang bởi một annotation, thứ còn nhận thêm được argument. Marker vẫn sống ở những chỗ mà type system phải biết, vì instanceof và parameter của method kiểm tra được interface chứ không kiểm tra được annotation.

So sánh abstract class và interface

Cả hai đều cho phép khai báo một method không body và bắt subtype phải viết body, nên hai thứ hay bị nhầm với nhau. Mọi điểm còn lại đều khác:

Abstract classInterface
Instance stateCó — field tùy ý, thay đổi được hoặc finalKhông — field ngầm định là hằng public static final
ConstructorCó, và chạy qua super(...)Không — khai báo constructor là lỗi cú pháp
Cách một type gia nhậpextends, chỉ mộtimplements, bao nhiêu cũng được
Access levelpublic, protected, package-private, privateMember là public; chỉ method có body mới được private (Java 9)
Body của methodMethod concrete bình thườngdefaultstatic method (Java 8)
Member finalCho phép method final và field finalKhông có method final; mọi field vốn đã final
Member staticField và method staticChỉ method static, và không được kế thừa
Quan hệ nó diễn đạt"is a" — một dạng cụ thể hơn của cha"can do" — một khả năng, độc lập với hierarchy
Thêm member về sauThêm abstract method là phá subclassThêm default method thì không phá gì
Nó dùng để làm gìChia sẻ state và implementation giữa các type có họ hàngChia sẻ một khả năng giữa các type không họ hàng

Nên chọn cái nào

Luật gói gọn trong một câu: abstract class khi các subtype dùng chung state và implementation, interface khi các type không liên quan cùng có một khả năng.

Lấy một chương trình có cả hai nhu cầu. Bắt đầu từ khả năng: thứ gì trong chương trình ghi ra được một dòng CSV thì nói ra điều đó, và không nói gì thêm.

interface Exportable {
    String toCsvRow();
}

Giờ đến nhóm employee. EngineerManager đều là employee. Cả hai đều có name và base salary, và pay()base + bonus() với cả hai, chỉ khác mỗi bonus(). Đó là state dùng chung, cộng implementation dùng chung, cộng một quan hệ "is a" thật sự — tức là abstract class, và nó cũng tiện thể export được:

abstract class Employee implements Exportable {
    protected final String name;
    protected final double base;

    protected Employee(String name, double base) {   // an abstract class has a constructor
        this.name = name;
        this.base = base;
    }

    abstract double bonus();                         // each subclass decides

    final double pay() {                             // shared implementation
        return base + bonus();
    }

    @Override
    public String toCsvRow() {
        return name + "," + pay();
    }
}

class Engineer extends Employee {
    Engineer(String name, double base) { super(name, base); }
    @Override double bonus() { return base * 0.10; }
}

class Manager extends Employee {
    private final int reports;
    Manager(String name, double base, int reports) { super(name, base); this.reports = reports; }
    @Override double bonus() { return base * 0.15 + reports * 100; }
}

Giờ đến Invoice. Nó không phải employee, không chung field nào, không chung công thức nào, và sẽ không bao giờ nằm cùng hierarchy. Điểm chung duy nhất của nó với Employee là cả hai đều ghi ra được một dòng CSV — đúng thứ mà Exportable nói:

class Invoice implements Exportable {
    private final String ref;
    private final double amount;
    Invoice(String ref, double amount) { this.ref = ref; this.amount = amount; }
    @Override public String toCsvRow() { return ref + "," + amount; }
}

Hai câu trả lời không hề đối đầu nhau. Employee là abstract class và nó implements Exportable, nên một method xử lý được cả employee lẫn invoice:

static void dump(Exportable[] items) {
    for (Exportable e : items) System.out.println(e.toCsvRow());
}

public static void main(String[] args) {
    Employee[] staff = { new Engineer("ada", 5000), new Manager("grace", 6000, 4) };
    for (Employee e : staff) System.out.printf("%s pay = %.1f%n", e.name, e.pay());

    System.out.println("--- exported ---");
    dump(new Exportable[]{ staff[0], staff[1], new Invoice("INV-7", 240.5) });
}
ada pay = 5500.0
grace pay = 7300.0
--- exported ---
ada,5500.0
grace,7300.0
INV-7,240.5

Hai panel: subclass dùng chung state giải bằng abstract class, các type không liên quan cùng một khả năng giải bằng interface

Trong thực tế, interface đến trước trong một thiết kế và abstract class đến sau, như một tiện ích tùy chọn dành cho những implementor tình cờ có họ hàng với nhau. Thứ tự đó lộ ra khắp JDK, nơi một interface như List nằm trên một implementation một phần như AbstractList.

Programming to an interface

Đây là phần lợi ích thực tế, và đáng viết ra đầy đủ vì nó là lý do một interface xứng đáng với một file thêm vào. Trước hết là cùng công việc đó nhưng làm theo kiểu mã định danh, tức là thứ mà mẫu này thay thế:

public class Coupled {
    // The call site knows every channel by name. Adding one means editing this method.
    static String notify(String channel, String user, String message) {
        if (channel.equals("email")) {
            return "to: " + user + "@example.com | subject: Alert | " + message;
        } else if (channel.equals("sms")) {
            String body = message.length() > 18 ? message.substring(0, 15) + "..." : message;
            return "[" + user + "] " + body;
        }
        throw new IllegalArgumentException("unknown channel: " + channel);
    }

    public static void main(String[] args) {
        String msg = "build 412 failed on main";
        System.out.println(notify("email", "ada", msg));
        System.out.println(notify("sms", "ada", msg));
        System.out.println(notify("webhook", "ada", msg));
    }
}
to: ada@example.com | subject: Alert | build 412 failed on main
[ada] build 412 faile...
Exception in thread "main" java.lang.IllegalArgumentException: unknown channel: webhook
	at Coupled.notify(Coupled.java:10)
	at Coupled.main(Coupled.java:17)

Mỗi channel mới là một lần sửa notify, test lại nó, và hy vọng không ai quên nhánh nào. Tệ hơn, một channel lạ chỉ hỏng lúc runtime chứ không hỏng lúc compile. Bản dùng interface đẩy mỗi channel vào class riêng của nó và để call site không cần biết gì:

interface Notifier {
    String channel();
    String format(String user, String message);
}

class EmailNotifier implements Notifier {
    @Override public String channel() { return "email"; }
    @Override public String format(String user, String message) {
        return "to: " + user + "@example.com | subject: Alert | " + message;
    }
}

class SmsNotifier implements Notifier {
    @Override public String channel() { return "sms"; }
    @Override public String format(String user, String message) {
        String body = message.length() > 18 ? message.substring(0, 15) + "..." : message;
        return "[" + user + "] " + body;
    }
}

public class Decoupled {
    // The call site never changes. It knows the contract, not the implementations.
    static void send(Notifier notifier, String user, String message) {
        System.out.println("via " + notifier.channel() + ": " + notifier.format(user, message));
    }

    public static void main(String[] args) {
        String msg = "build 412 failed on main";
        for (Notifier n : new Notifier[]{ new EmailNotifier(), new SmsNotifier() }) {
            send(n, "ada", msg);
        }
    }
}
via email: to: ada@example.com | subject: Alert | build 412 failed on main
via sms: [ada] build 412 faile...

Thêm implementation thứ ba

Phép thử của thiết kế là một channel mới tốn bao nhiêu. Ở đây nó tốn đúng một file mới và không sửa ở đâu khác — send không phải compile lại, không phải test lại, và không biết WebhookNotifier tồn tại. Class mới còn mang theo field riêng, thứ mà interface vừa không biết vừa không cấm:

class WebhookNotifier implements Notifier {
    private final String url;
    WebhookNotifier(String url) { this.url = url; }

    @Override public String channel() { return "webhook"; }
    @Override public String format(String user, String message) {
        return "POST " + url + " {\"user\":\"" + user + "\",\"text\":\"" + message + "\"}";
    }
}
via webhook: POST https://hooks.example.com/ci {"user":"ada","text":"build 412 failed on main"}

Thói quen rút ra rất ngắn: hãy khai báo parameter, field và kiểu trả về bằng type tổng quát nhất mà vẫn đủ dùng. Viết Notifier n, đừng viết EmailNotifier n. Call site khi đó chạy được với mọi implementation hôm nay đã có và mọi implementation viết sau này.

Những interface trong JDK bạn đã dùng rồi

Bốn cái trong số đó đã xuất hiện trong khóa này, dưới những cái tên khác.

Comparable -> [v8, v17, v21]
Runnable   -> ran on worker
List impl  -> ArrayList
Iterable   -> ada alan
ArrayList is a List?     true
ArrayList is an Iterable? true

Comparable là thứ Arrays.sort đòi hỏi khi sort một mảng object — bài về sort đã gọi Arrays.sort trên int[], nơi thứ tự đã có sẵn; với class của bạn thì bạn cung cấp thứ tự bằng cách implement compareTo. Iterable là thứ mà vòng for mở rộng cần khi vế phải không phải mảng, còn mảng thì không phải Iterable, nên compiler sinh ra một vòng lặp theo index cho chúng. Runnable là interface một method mà một thread chạy, và List là interface mà ArrayList implement — cả hai thuộc về phần sau khóa này, lần lượt là thread và collections framework.

String cũng đáng nhìn một lần vì cùng lý do:

String    -> [interface java.io.Serializable, interface java.lang.Comparable, interface java.lang.CharSequence, interface java.lang.constant.Constable, interface java.lang.constant.ConstantDesc]

Năm interface trên một class bạn đã dùng từ bài đầu tiên. Đó là hình dạng bình thường của một type được thiết kế tốt.

Lỗi thường gặp và message thật của chúng

Quên public ở method implement. Method của interface vốn đã public, nên access package-private mặc định là một sự thu hẹp: attempting to assign weaker access privileges; was public. Hãy viết public cho mọi implementation, và @Override ngay trên nó.

Quên hẳn một method. Triangle is not abstract and does not override abstract method area() in Drawable. Hoặc implement nó, hoặc khai báo class là abstract.

Cố tạo object từ interface. Không có gì để dựng cả:

Instantiate.java:7: error: Drawable is abstract; cannot be instantiated
        Drawable d = new Drawable();
                     ^
1 error

Cú pháp trông giống việc đó là anonymous class, và cặp ngoặc nhọn phía sau chính là điểm khác biệt:

Drawable d = new Drawable() {
    @Override public void draw() { System.out.println("drawn by an anonymous class"); }
};
drawn by an anonymous class
runtime class -> Anon$1

Tưởng field của interface là state riêng của từng object. Nó là một hằng public static final duy nhất dùng chung cho tất cả, và cannot assign a value to static final variable là thứ bạn nhận được khi thử. State thuộc về class.

Đưa một helper lên interface chỉ để hai default method dùng chung. Trước Java 9 đó là lựa chọn duy nhất và nó làm rò một chi tiết cài đặt lên contract công khai. Từ Java 9 thì đó là một private method.

Gọi static method của interface qua class implement. cannot find symbol — static method của interface không được kế thừa. Hãy gọi qua tên interface.

Dùng interface trong khi các type thực ra dùng chung state. Hai subclass cần chung một field thì không lấy được nó từ interface, và một default method cũng không chạm được vào state. Đó là abstract class.

FAQ

Interface và abstract class trong Java khác nhau thế nào?

Abstract class giữ được instance state, có constructor, cho phép mọi access level và mọi kiểu trộn giữa method concrete và abstract, và một class chỉ được extends đúng một cái. Interface không có instance state, không có constructor, member ngầm định là public (method thì abstract, field thì static final), nó mang được body của default, staticprivate method kể từ Java 8 và Java 9, và một class implement bao nhiêu cũng được. Dùng abstract class khi các subtype dùng chung state và implementation; dùng interface khi các type không liên quan cùng có một khả năng.

Một class Java có thể implement nhiều interface không?

Có, và đây là lý do chính khiến interface tồn tại. class Report implements Printable, Storable, Rankable cho object ba type nữa ngoài type của chính nó, và nó truyền được vào method nhận bất kỳ type nào trong ba. Một class vẫn chỉ extends được một class — class Robot extends Machine, Vehicle là lỗi cú pháp ngay ở dấu phẩy.

Vì sao interface không có instance field?

Vì interface không có instance của riêng nó và không có constructor để khởi tạo bất cứ thứ gì. Mọi field khai báo trong interface đều ngầm là public static final, nên private String user; hỏng với = expected — compiler đang đòi giá trị khởi tạo mà một hằng số bắt buộc phải có. Nếu thiết kế cần state riêng cho từng object thì đó là class.

Default method là gì và khi nào nên viết?

default method là method của interface có body, xuất hiện từ Java 8, và mọi implementor đều kế thừa nó mà không phải viết gì. Nó tồn tại để một method có thể được thêm vào interface đã có implementor: compile lại mỗi interface là class file cũ vẫn chạy, trong khi một abstract method sẽ làm hỏng từng cái một với is not abstract and does not override. Hãy viết nó khi bạn mở rộng một interface mà người khác đang implement, hoặc khi một method có đúng một cách cài đặt hiển nhiên dựa trên các method còn lại. Đừng dùng nó làm chỗ chứa code chung — default method vẫn không chạm được vào state.

Vì sao javac báo inherits unrelated defaults?

Vì class implement hai interface cùng cung cấp một default method trùng signature, và không interface nào là subtype của interface kia, nên không có luật nào chọn ra đáp án đúng. Java từ chối đoán. Hãy override method đó trong class và chọn rõ bằng Logger.super.prefix(), gộp cả hai, hoặc viết cái khác. Nếu một interface thực sự extends interface kia thì default cụ thể hơn thắng và không có lỗi nào được báo.

Có thể tạo object từ interface trong Java không?

Không — new Drawable() cho ra Drawable is abstract; cannot be instantiated. Thứ trông như đang tạo object từ interface thực ra là một trong hai chuyện khác: anonymous class, tức new Drawable() { ... } với phần body cung cấp các method, biên dịch thành một class tên kiểu Anon$1; hoặc lambda, khi interface có đúng một abstract method.

Functional interface là gì?

Là interface có đúng một abstract method. default, staticprivate method không được tính. Chính method duy nhất đó là thứ mà một lambda expression implement, nên Transform upper = s -> s.toUpperCase(); hợp lệ đúng vì Transform có một abstract method. @FunctionalInterface là tùy chọn và chỉ để nhờ compiler kiểm tra con số đó — thêm một abstract method thứ hai là build hỏng với multiple non-overriding abstract methods found.

Kết luận

Interface là một contract và không gì khác: không state, không constructor, không identity. Mọi thứ còn lại đều suy ra từ đó. Member là public vì một contract giấu đi điều khoản của mình thì vô dụng, và field là static final vì không có chỗ nào để giữ state. Một class ký bao nhiêu contract cũng được vì việc ký không tốn gì về mặt cấu trúc, còn nó chỉ kế thừa được từ một cha vì inheritance kéo theo field và body — những thứ sẽ phải gộp lại với nhau. default method cho phép một contract đã phát hành lớn thêm mà không phá những ai đã ký, và khi hai contract va nhau thì compiler bắt bạn chọn thay vì tự chọn hộ bạn.

Đây cũng là bài khép lại phần OOP của khóa này. Encapsulation đưa state ra sau các method để object tự giữ ràng buộc của mình; inheritance cho một type tái dùng và chuyên biệt hóa một type khác; polymorphism khiến dynamic type quyết định body nào chạy; abstract class cho một type tuyên bố những gì subclass phải trả trong khi vẫn giữ được state và code dùng chung; còn interface tách hẳn khả năng ra khỏi hierarchy. Hai thứ cuối là cùng một ý tưởng nhìn từ hai đầu — một bên xuất phát từ một họ và rút ra phần chung, bên kia xuất phát từ một khả năng và để type nào cũng nhận được — và một thiết kế thật dùng cả hai, thường là interface ở trên cùng và abstract class ngay bên dưới.

Bài tiếp theo trong series: xử lý exceptiontry, catchfinally, khác biệt giữa checked và unchecked exception, và throw với throws thực sự làm gì.

Bài viết liên quan

[Java Basics] Tính trừu tượng trong Java: abstract class và abstract method

Abstract class và abstract method trong Java: vì sao không tạo được object của Shape, abstract class chứa được những gì, template method pattern, anonymous subclass, thứ tự constructor, cùng mọi compile error chạy thật trên JDK 21.

[Java Basics] Toán tử trong Java: số học, so sánh, logic, gán và toán tử ba ngôi

Hướng dẫn đầy đủ về toán tử trong Java — số học, so sánh, logic, gán, bitwise và ba ngôi — với mọi kết quả đều được compile và chạy thật trên JDK 21, kèm các bẫy về chia số nguyên, số dư, số thực và short-circuit.

[Java Basics] Vòng lặp trong Java: for, while và do-while

Vòng lặp trong Java giải thích bằng cách chạy thật: thứ tự thực thi chính xác của header for, while so với do-while, enhanced for và lý do nó không ghi ngược vào array, lỗi off-by-one với length, và ba cách tạo ra vòng lặp vô hạn.

[Java Basics] Phương thức trong Java: khai báo và gọi method

Cách khai báo và gọi method trong Java: các thành phần của một declaration, static method so với instance method và lỗi non-static method cannot be referenced from a static context, return statement, call stack và StackOverflowError, cách đọc stack trace, Javadoc, cùng những lỗi compiler thật mà người mới hay gặp.