Sự trưởng thành của Interface qua các phiên bản Java và Kotlin

Trong Java hay Kotlin đều không có đa kế thừa, để khắc phục được điều này, các lập trình viên đã sử dụng interface

Mặc dù vẫn còn những hạn chế nhất định khi sử dụng interface nhưng qua nhiều phiên bản của Java và Kotlin, interface đã ngày càng mạnh mẽ theo thời gian

Hôm nay chúng ta sẽ cùng nhau nhìn xem interface sẽ có những tính năng gì nổi bật qua các phiên bản nhé

1. Interface trong Java 7

Trong Java 7, một interface chỉ có thể khai báo 2 loại :

  1. Hằng số (Constant Variables)
  2. Phương thức trừu tượng (Abstract Method)
interface OnCheckNumberListener {

    int MAX_VALUE = 1000;

    boolean isEvenNumber(int number);
}

Những phương thức của interface trong phiên bản này không được phép có thân hàm

Class implement interface đều phải cài đặt lại những phương thức trong interface

Bây giờ hãy cùng mình xét một bài toán nho nhỏ: Có 3 class A, B, C cùng implement interface OnCheckNumberListener

Khi ấy, cả 3 class A, B, C đều phải cài đặt phương thức isEvenNumber()như sau:

class A implements OnCheckNumberListener {

    @Override
    public boolean isEvenNumber(int number) {
        return number % 2 == 0;
    }
}

class B implements OnCheckNumberListener {

    @Override
    public boolean isEvenNumber(int number) {
        return number % 2 == 0;
    }
}

class C implements OnCheckNumberListener {

    @Override
    public boolean isEvenNumber(int number) {
        return number % 2 == 0;
    }
}

Rõ ràng thấy, phương thức isEvenNumber() bắt buộc phải viết lại ở cả 3 class A, B, C nhưng nội dung của chúng thì giống nhau hoàn toàn

Nếu làm như vậy sẽ không thể tối ưu tính tái sử dụng mã nguồn được

Để giải quyết vấn đề trên, ở Java 7, chúng ta có thể tạo ra 1 lớp abstract để cài đặt phương thức isEvenNumber()

public abstract class CheckNumber implements OnCheckNumberListener {

    @Override
    public boolean isEvenNumber(int number) {
        return number % 2 == 0;
    }
}

Và lúc này, ở 3 class A, B, C sẽ không phải cài đặt lại phương thức isEvenNumber khi extends class CheckNumber

class A extends CheckNumber{
    
}
class B extends CheckNumber{
    
}
class C extends CheckNumber{
    
}

Cách này có vẻ khả quan, nhưng hãy nhớ 1 điều rằng, Java không có hỗ trợ đa kế thừa, vì thế cách này chỉ khả thi khi A, B, C chưa kế thừa lớp cha nào trước đó mà thôi.

2. Interface trong Java 8

Trong Java 8, ngoài hằng số và những phương thức abstract, chúng ta có thể khai báo những phương thức mặc định (default methods) và phương thức tĩnh (static methods)

  1. Hằng số (Constant Variables)
  2. Phương thức trừu tượng (Abstract Method)
  3. Phương thức mặc định (Default method)
  4. Phương thức tĩnh (Static method)
interface OnCheckNumberListener {

    int MAX_VALUE = 1000;

    boolean isEvenNumber(int number);

    default boolean isOddNumber(int number) {
        return number % 2 != 0;
    }

    static boolean isNegativeNumber(int number){
        return number < 0; 
    }
}

Có lẽ với sự cải tiến này trong Java 8, chúng ta sẽ không cần phải tạo một lớp abstract class CheckNumber nữa, mà có thể cài đặt luôn trong interface

class A implements OnCheckNumberListener {

    @Override
    public boolean isEvenNumber(int number) {
        return !isOddNumber(number);
    }
}

Tuy nhiên, không phải lúc nào hàm default của chúng ta cũng ngắn gọn, chúng có thể rất dài mà chúng ta muốn tách nó ra làm nhiều hàm nhỏ hơn

Khi ấy, vì chúng đang ở trong 1 interface nên những hàm tách ra ấy sẽ phải là public, mà điều này thì có vẻ không phải là thứ mà chúng ta mong muốn

interface OnCheckNumberListener {

    int MAX_VALUE = 1000;

    boolean isEvenNumber(int number);

    default boolean isOddNumber(int number) {
        return checkFirst() && checkSecond() && checkThird(number);
    }

    default boolean checkFirst(){
        return true;
    }

    default boolean checkSecond(){
        return false;
    }

    default boolean checkThird(int number){
        return number % 2 != 0; 
    }
}

3. Interface trong Java 9

Java 9 đã giới thiệu các phương thức private và phương thứcprivate static trong các interface.

  1. Hằng số
  2. Phương thức abstract
  3. Phương thức default
  4. Phương thức static
  5. Phương thức private
  6. Phương thức private static
public interface DBLogging {
	String MONGO_DB_NAME = "ABC_Mongo_Datastore";
	String NEO4J_DB_NAME = "ABC_Neo4J_Datastore";
	String CASSANDRA_DB_NAME = "ABC_Cassandra_Datastore";

	default void logInfo(String message) {
		log(message, "INFO");
	}

	default void logWarn(String message) {
		log(message, "WARN");
	}

	default void logError(String message) {
		log(message, "ERROR");
	}

	default void logFatal(String message) {
		log(message, "FATAL");
	}

	private void log(String message, String msgPrefix) {
		// Step 1: Connect to DataStore
		// Step 2: Log Message with Prefix and styles etc.
		// Step 3: Close the DataStore connection
	}
	// Any other abstract, static, default methods
}

4. Interface trong Kotlin

Ở Kotlin, interface được hỗ trợ nhiều hơn khi có thể có thể khai báo

  1. Thuộc tính
  2. Phương thức trừu tượng (Abstract Method)
  3. Phương thức default

Khác với các lớp trừu tượng, interface trong Kotlin không thể lưu trữ những trạng thái, mặc dù có thể có thuộc tính nhưng chúng cần phải là trừu tượng hoặc phải cài đặt cách thức truy cập

interface MyInterface {
    val prop: Int // abstract

    val propertyWithImplementation: String
        get() = "foo"

    fun foo() {
        print(prop)
    }
    
    fun foo2() // abstract
}

class Child : MyInterface {
    override val prop: Int = 29
    
    override fun foo2() {
    // body
    }
}

4.1 Giải quyết vấn đề xung đột ghi đè của Interface trong Kotlin

Vấn đề xung đột ghi đè của interface thường xảy ra khi một lớp implements nhiều interface có phương thức cùng tên

interface A {
    fun foo() { print("A") }
    fun bar()
}

interface B {
    fun foo() { print("B") }
    fun bar() { print("bar") }
}

class C : A {
    override fun bar() { print("bar") }
}

class D : A, B {
    override fun foo() {
        super<A>.foo()
        super<B>.foo()
    }

    override fun bar() {
        super<B>.bar()
    }
}

Chúng ta có 2 interface A và B đều khai báo các hàmfoo()bar().

Cả hai đều cài đặt foo(), nhưng chỉ B cài đặt bar(), còn A thì để abstract(không cài đặt)

Bây giờ, nếu chúng ta có một lớp C implements A, rõ ràng, chúng ta phải ghi đè và cài đặt phương thức bar().

Tuy nhiên, nếu chúng ta có lớp D implements cả A và B, chúng ta cần phải cài đặt lại tất cả những phương thức trùng tên từ A và B để đảm bảo chính xác phương thức đó được thực hiện như nào.

4.2 Thực hư việc khai báo thuộc tính trong interface

Xét interface đơn giản sau

interface A {
    var first: Int

    val second: Int
}

Đoạn code trên, nếu chúng ta đọc bytecode mà nó sinh ra, nó sẽ tương đương trong Java như sau

interface A {
    
    int getFirst(); 
    
    void setFirst(first: Int); 

    int getSecond();
}

Vậy các bạn có thể thấy, bản chất khi chúng ta khai báo 1 thuộc tính trong interface chính là tạo ra 2 hàm gettersetter cho thuộc tính đó

Tuy nhiên, vì interface trong Kotlin không lưu được trạng thái nên thuộc tính thực sự của nó không tồn tại mà chỉ tồn tại gettersetter

Nếu là val thì sẽ chỉ có getter, var thì đầy đủ cả getter/setter

Nếu một lớp implement interface A theo cách

class AImpl : A {
    override var first: Int = 3
    override val second: Int get() = 4
}

thì sẽ tương đương trong Java

public final class AImpl implements A {
    public int first = 3; 
    
    public int getFirst(){
        return first; 
    }
    
    public void setFirst(int first){
        this.first = first; 
    }
    
    public int getSecond(){
        return 4; 
    }
}

Kết luận

Qua đó các bạn có thể thấy, Interface ngày càng được cải thiện qua nhiều phiên bản, hỗ trợ rất nhiều đặc biệt có thể khắc phục triệt để hơn nhược điểm không hỗ trợ đa kế thừa của nó

Tham khảo

[1] https://kotlinlang.org/docs/reference/interfaces.html [2] https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html