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 đó là 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, static và private 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ọ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, static và private 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
Circle và Square 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

Đâ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ể.

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.

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, static và private 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.Serializable và java.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 class | Interface | |
|---|---|---|
| Instance state | Có — field tùy ý, thay đổi được hoặc final | Không — field ngầm định là hằng public static final |
| Constructor | Có, và chạy qua super(...) | Không — khai báo constructor là lỗi cú pháp |
| Cách một type gia nhập | extends, chỉ một | implements, bao nhiêu cũng được |
| Access level | public, protected, package-private, private | Member là public; chỉ method có body mới được private (Java 9) |
| Body của method | Method concrete bình thường | default và static method (Java 8) |
Member final | Cho phép method final và field final | Không có method final; mọi field vốn đã final |
Member static | Field và method static | Chỉ 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ề sau | Thêm abstract method là phá subclass | Thê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àng | Chia 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. Engineer và Manager đều là employee. Cả hai đều có name và base salary, và pay() là 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

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, static và private 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, static và private 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ý exception — try, catch và finally, khác biệt giữa checked và unchecked exception, và throw với throws thực sự làm gì.