1. Giới thiệu
Ngày xưa (trước Java 8) interface rất nghiêm ngặt: bạn chỉ có thể khai báo các phương thức abstract (không có triển khai) và các hằng (public static final). Điều đó khá ổn cho đến khi xuất hiện một vấn đề lớn: việc phát triển thư viện.
Hãy tưởng tượng tình huống
Bạn đã phát triển một thư viện phổ biến, trong đó có một interface:
public interface Movable {
void move(int x, int y);
}
Hàng nghìn lập trình viên trên khắp thế giới viết các lớp của riêng họ để triển khai interface này. Vài năm sau, bạn nhận ra rằng mọi người đều thiếu phương thức reset(), phương thức đưa đối tượng về vị trí ban đầu. Bạn thêm vào interface:
public interface Movable {
void move(int x, int y);
void reset();
}
Và rồi thảm họa bắt đầu: mọi dự án sử dụng interface của bạn đều không còn biên dịch được! Bởi vì giờ đây họ buộc phải triển khai phương thức mới mà trước đó không ai biết đến. Việc nâng cấp trở nên đau đớn.
Default method — giải pháp!
Java 8 giới thiệu các phương thức default: giờ bạn có thể thêm một phương thức với phần triển khai ngay trong interface! Tất cả các lớp cũ tự động nhận được một triển khai mặc định, và mã của họ không bị vỡ. Còn nếu muốn — bạn có thể override phương thức theo cách của mình.
2. Cú pháp của phương thức default
Phương thức default là một phương thức thông thường có phần triển khai bên trong interface, được đánh dấu bằng từ khóa default.
public interface Movable {
void move(int x, int y);
default void reset() {
// Triển khai điển hình: quay về gốc tọa độ
move(0, 0);
}
}
Giải thích:
- Mọi phương thức trong interface mặc định là public và abstract, nhưng phương thức default thì không phải abstract mà có thân (body).
- Từ khóa default luôn được viết trước kiểu trả về của phương thức.
Trong lớp thì trông như thế nào?
public class Robot implements Movable {
private int x, y;
@Override
public void move(int x, int y) {
this.x = x;
this.y = y;
System.out.println("Robot đã được di chuyển tới (" + x + ", " + y + ")");
}
// không bắt buộc triển khai reset() — phiên bản default sẽ hoạt động!
}
Bây giờ, nếu chúng ta gọi reset() trên đối tượng Robot, triển khai từ interface Movable sẽ được sử dụng:
public class Main {
public static void main(String[] args) {
Movable robot = new Robot();
robot.move(10, 20); // Robot đã được di chuyển tới (10, 20)
robot.reset(); // Robot đã được di chuyển tới (0, 0)
}
}
3. Phương thức default trong thư viện chuẩn
Các phương thức default không phải được thêm vào một cách ngẫu nhiên, mà để cho phép phát triển các interface chuẩn khổng lồ của Java mà không làm hỏng mã cũ.
Ví dụ: interface List (Java 8+)
Trong Java 8, interface List được bổ sung những phương thức đã có sẵn triển khai, ví dụ forEach, replaceAll, sort:
default void forEach(Consumer<Entity> action) {
for (Entity e : this) {
action.accept(e);
}
}
Nếu bạn tự triển khai một danh sách và không override forEach, nó vẫn sẽ hoạt động — nhờ phương thức default.
Bạn sẽ tìm hiểu thêm về generic type (Consumer<Entity>) ở cấp độ 26 :P
4. Tại sao cần phương thức default?
- Phát triển API mà không làm vỡ mã: có thể thêm phương thức mới vào interface mà không cần phải triển khai chúng trong mọi lớp hiện có.
- Mẫu hành vi dùng chung: có thể khai báo hành vi mặc định để các lớp dùng lại hoặc override.
- Giảm trùng lặp: nếu hành vi giống nhau cho phần lớn các triển khai — không cần sao chép mã vào từng lớp.
Phép so sánh
Hãy tưởng tượng bạn có một hợp đồng thuê căn hộ (interface). Trước đây trong đó có ghi: “Người thuê phải trả tiền nước”. Sau đó thêm: “Người thuê phải trả tiền điện”. Nếu không có các phương thức default, bạn sẽ phải viết lại tất cả hợp đồng với mọi người thuê! Còn với phương thức default — chỉ cần thêm một điều khoản, và nếu ai đó cần — họ có thể thỏa thuận theo cách riêng.
5. Hạn chế và đặc điểm của phương thức default
Phương thức default không thể override các phương thức của lớp Object
Bạn không thể khai báo trong interface một phương thức default có chữ ký trùng với equals, hashCode hoặc toString từ lớp Object. Đây là cơ chế bảo vệ khỏi nhầm lẫn: bất kỳ đối tượng nào trong Java đã có các phương thức này.
// Lỗi biên dịch!
interface Broken {
default boolean equals(Object obj) { return false; }
}
Xung đột giữa các phương thức default
Nếu một lớp triển khai hai interface, mỗi interface đều có một phương thức default với cùng chữ ký thì sao? Trình biên dịch Java sẽ thẳng thắn nói: “Tự xử lý đi, tôi không biết phải làm gì!”
interface A {
default void hello() { System.out.println("Hello from A"); }
}
interface B {
default void hello() { System.out.println("Hello from B"); }
}
class C implements A, B {
// Bắt buộc phải giải quyết xung đột:
@Override
public void hello() {
// Có thể chọn gọi phương thức của ai, hoặc tự cài đặt của mình
A.super.hello(); // hoặc B.super.hello();
}
}
Nếu không triển khai hello() trong lớp C, sẽ có lỗi biên dịch.
Phương thức default có thể gọi các phương thức khác của interface
Phương thức default có thể gọi các phương thức khác của interface, kể cả phương thức abstract. Điều kiện là lớp phải cung cấp triển khai.
interface Printer {
void print(String text);
default void printTwice(String text) {
print(text);
print(text);
}
}
6. Ví dụ: phát triển ứng dụng với phương thức default
Hãy xem một ví dụ sử dụng phương thức default trong interface Movable:
public interface Movable {
void move(int x, int y);
default void reset() {
move(0, 0);
}
}
Và có lớp Robot triển khai interface này:
public class Robot implements Movable {
private int x = 5;
private int y = 7;
@Override
public void move(int x, int y) {
this.x = x;
this.y = y;
System.out.println("Robot đã được di chuyển tới (" + x + ", " + y + ")");
}
// không triển khai reset() — dùng default method!
}
Giờ hãy thử gọi cả hai phương thức:
public class Main {
public static void main(String[] args) {
Movable robot = new Robot();
robot.move(10, 20); // Robot đã được di chuyển tới (10, 20)
robot.reset(); // Robot đã được di chuyển tới (0, 0)
}
}
Nếu muốn Robot reset theo một cách đặc biệt — chỉ cần override reset() trong lớp:
@Override
public void reset() {
System.out.println("Robot tắt nguồn và trở về căn cứ!");
move(0, 0);
}
7. Phương thức default và việc triển khai nhiều interface
Phương thức default đặc biệt hữu ích khi một lớp triển khai nhiều interface. Tuy nhiên có một lưu ý: nếu cả hai interface đều có phương thức default với cùng chữ ký, trình biên dịch sẽ yêu cầu bạn giải quyết xung đột một cách tường minh.
Ví dụ xung đột
interface A {
default void show() { System.out.println("A"); }
}
interface B {
default void show() { System.out.println("B"); }
}
class C implements A, B {
@Override
public void show() {
// Chọn rõ ràng sẽ dùng default method của ai
A.super.show(); // hoặc B.super.show();
}
}
8. Sơ đồ: cách gọi phương thức default hoạt động
+-------------------+
| Movable |
|-------------------|
| +move(int, int) | <- phương thức abstract
| +reset() | <- default method
+-------------------+
^
|
+-------------------+
| Robot |
|-------------------|
| +move(int, int) | <- triển khai
| | (không triển khai reset())
+-------------------+
|
Gọi reset()
|
Sử dụng triển khai
từ interface Movable
9. Những lỗi thường gặp khi làm việc với phương thức default
Lỗi số 1: cố gắng tạo phương thức default nhưng không có triển khai.
Phương thức default bắt buộc phải có thân! Nếu bạn viết default void foo();, trình biên dịch sẽ nói ngay: “Bạn quên dấu ngoặc nhọn à?”
Lỗi số 2: xung đột giữa các phương thức default từ các interface khác nhau.
Nếu một lớp triển khai hai interface với cùng một phương thức default, bạn phải giải quyết xung đột một cách tường minh — nếu không trình biên dịch sẽ không cho biên dịch mã.
Lỗi số 3: cố gắng khai báo phương thức default có chữ ký của phương thức trong Object.
Không thể tạo phương thức default equals, hashCode hoặc toString trong interface — chỉ có thể khai báo các phương thức abstract với những tên này.
Lỗi số 4: quên rằng phương thức default không phải “ma thuật”, mà chỉ là công cụ tiện lợi.
Phương thức default không làm thay đổi nguyên tắc rằng interface là một hợp đồng (contract). Nếu hành vi mặc định không phù hợp — hãy luôn override phương thức default trong lớp.
GO TO FULL VERSION