Hướng dẫn về khả năng tương thích giữa Kotlin và Java

Tài liệu này trình bày bộ quy tắc cho phép chỉnh sửa các API công khai trong Java và Kotlin, mục đích là để mã lập trình sẽ phù hợp khi sử dụng trong ngôn ngữ khác.

Lần cập nhật gần đây nhất: 18-05-2018

Java (khi sử dụng trong Kotlin)

Không dùng từ khoá cố định

Đừng sử dụng từ khoá cố định của Kotlin làm tên phương thức hoặc tên trường. Các từ khoá này yêu cầu phải sử dụng dấu phẩy ngược (`) để thoát khi gọi hàm trong Kotlin. Bạn được phép sử dụng từ khoá không cố định, từ khoá bổ trợgiá trị nhận dạng đặc biệt.

Ví dụ: hàm when của Mockito yêu cầu phải có dấu phẩy ngược khi sử dụng trong Kotlin:

val callable = Mockito.mock(Callable::class.java)
Mockito.`when`(callable.call()).thenReturn(/* … */)

Tránh dùng tên của hàm mở rộng Any

Tránh dùng tên của hàm mở rộng trên Any làm tên phương thức hoặc tên của thuộc tính mở rộng trên Any làm tên trường, trừ phi nhất thiết phải dùng. Mặc dù trường và phương thức con sẽ luôn được ưu tiên hơn hàm hoặc thuộc tính mở rộng của Any, nhưng cách làm này có thể gây khó khăn khi đọc mã để biết mã nào đang được gọi.

Chú giải tính chất rỗng (null)

Mọi thông số tự tạo, kết quả trả về và loại trường trong API công khai đều phải có chú giải tính chất rỗng. Các loại không có chú giải sẽ được hiểu là loại "platform" có tính chất rỗng không rõ ràng.

Theo mặc định, trình biên dịch Kotlin sẽ tuân theo chú giải JSR 305 nhưng vẫn gắn cờ các chú giải này kèm theo cảnh báo. Bạn cũng có thể thiết lập cờ để trình biên dịch coi chú giải là lỗi.

Thông số Lambda nằm ở cuối cùng

Các loại thông số đủ điều kiện để chuyển đổi SAM (chuyển đổi phương thức đơn trừu tượng) phải nằm ở cuối cùng.

Ví dụ: chữ ký phương thức RxJava 2’s Flowable.create() được xác định là:

public static  Flowable create(
    FlowableOnSubscribe source,
    BackpressureStrategy mode) { /* … */ }

Vì FlowableOnSubscribe đủ điều kiện để chuyển đổi SAM nên các lệnh gọi hàm của phương thức này trong Kotlin sẽ có dạng như sau:

Flowable.create({ /* … */ }, BackpressureStrategy.LATEST)

Tuy nhiên, nếu tham số đảo ngược trong chữ ký phương thức, thì các lệnh gọi hàm có thể sử dụng cú pháp trailing-lambda:

Flowable.create(BackpressureStrategy.LATEST) { /* … */ }

Tiền tố của thuộc tính

Để một phương thức được biểu thị dưới dạng thuộc tính trong Kotlin, bạn phải sử dụng tiền tố kiểu "bean" nghiêm ngặt.

Phương thức truy cập (accessor method) yêu cầu phải có tiền tố "get" hoặc với phương thức trả về boolean thì bạn có thể sử dụng tiền tố "is".

public final class User {
  public String getName() { /* … */ }
  public boolean isActive() { /* … */ }
}
val name = user.name // Invokes user.getName()
val active = user.isActive // Invokes user.isActive()

Phương thức biến đổi (mutator method) được liên kết yêu cầu phải có tiền tố "set".

public final class User {
  public String getName() { /* … */ }
  public void setName(String name) { /* … */ }
  public boolean isActive() { /* … */ }
  public void setActive(boolean active) { /* … */ }
}
user.name = "Bob" // Invokes user.setName(String)
user.isActive = true // Invokes user.setActive(boolean)

Nếu bạn muốn các phương thức được biểu thị dưới dạng thuộc tính, thì đừng sử dụng các tiền tố không chuẩn như trình truy cập có tiền tố "has" hay "set" hoặc không có tiền tố "get". Bạn vẫn có thể gọi phương thức có tiền tố không chuẩn dưới dạng các hàm có thể được chấp nhận, tuỳ thuộc vào hành vi của phương thức đó.

Nạp chồng toán tử

Hãy lưu ý đến các tên phương thức cho phép cú pháp đặc biệt call-site (tức là nạp chồng toán tử) trong Kotlin. Đảm bảo rằng tên các phương thức như vậy vẫn có nghĩa khi sử dụng ở dạng cú pháp rút gọn.

public final class IntBox {
  private final int value;
  public IntBox(int value) {
    this.value = value;
  }
  public IntBox plus(IntBox other) {
    return new IntBox(value + other.value);
  }
}
val one = IntBox(1)
val two = IntBox(2)
val three = one + two // Invokes one.plus(two)

Kotlin (khi sử dụng trong Java)

Tên tệp

Khi một tệp chứa các hàm hoặc thuộc tính cấp cao nhất, hãy luôn chú giải tệp đó bằng @file:JvmName("Foo") để cung cấp tên phù hợp.

Theo mặc định, các thành phần cấp cao nhất trong tệp MyClass.kt sẽ chuyển đến một lớp có tên là MyClassKt. Tên này không hay và để lộ ngôn ngữ lập trình dưới dạng thông tin về cách triển khai.

Hãy cân nhắc thêm @file:JvmMultifileClass để kết hợp các thành phần cấp cao nhất của nhiều tệp vào cùng một lớp.

Đối số lambda

Bạn có thể triển khai giao diện phương thức đơn (SAM) được xác định trong Java bằng cả Kotlin và Java khi dùng cú pháp lambda, tương ứng với cách triển khai theo đặc tính ngôn ngữ. Kotlin có một số tuỳ chọn để xác định các giao diện như vậy, mỗi tuỳ chọn sẽ có một chút khác biệt.

Định nghĩa được ưu tiên dùng

Hàm bậc cao hơn dành để sử dụng trong Java sẽ không lấy các loại hàm trả về Unit vì việc đó sẽ yêu cầu phương thức gọi Java trả về Unit.INSTANCE. Thay vì dùng cùng dòng loại hàm này trong chữ ký, hãy dùng các giao diện chức năng (SAM). Ngoài ra, hãy cân nhắc sử dụng giao diện chức năng (SAM) thay vì giao diện thông thường khi xác định các giao diện dự kiến sẽ dùng dưới dạng đối số lambda, qua đó cho phép sử dụng đặc tính ngôn ngữ trong Kotlin.

Hãy xem xét định nghĩa Kotlin sau:

fun interface GreeterCallback {
  fun greetName(String name)
}

fun sayHi(greeter: GreeterCallback) = /* … */

Khi được gọi từ Kotlin:

sayHi { println("Hello, $it!") }

Khi được gọi từ Java:

sayHi(name -> System.out.println("Hello, " + name + "!"));

Ngay cả khi loại hàm không trả về Unit, bạn vẫn nên đặt loại hàm này làm giao diện được đặt tên để cho phép phương thức gọi triển khai với một lớp được đặt tên chứ không chỉ các đối số lambda (trong cả Kotlin và Java).

class MyGreeterCallback : GreeterCallback {
  override fun greetName(name: String) {
    println("Hello, $name!");
  }
}

Tránh các loại hàm trả về Unit

Hãy xem xét định nghĩa Kotlin sau:

fun sayHi(greeter: (String) -> Unit) = /* … */

Định nghĩa này yêu cầu phương thức gọi Java trả về Unit.INSTANCE:

sayHi(name -> {
  System.out.println("Hello, " + name + "!");
  return Unit.INSTANCE;
});

Tránh các giao diện chức năng khi quá trình triển khai được dành để đưa ra trạng thái

Khi triển khai giao diện được dành để đưa ra trạng thái, việc sử dụng cú pháp lambda là không hợp lý. Giao diện Comparable (So sánh được) là một ví dụ nổi bật, vì giao diện này dành để so sánh this với other và cú pháp lambda không có this. Việc không thêm tiền tố fun vào giao diện này sẽ buộc phương thức gọi sử dụng cú pháp object : ..., qua đó cho phép giao diện đưa ra trạng thái, cung cấp gợi ý cho phương thức gọi.

Hãy xem xét định nghĩa Kotlin sau:

// No "fun" prefix.
interface Counter {
  fun increment()
}

Định nghĩa này ngăn sử dụng cú pháp lambda trong Kotlin, điều này đòi hỏi phải có phiên bản dài hơn:

runCounter(object : Counter {
  private var increments = 0 // State

  override fun increment() {
    increments++
  }
})

Tránh thông số tổng quát Nothing

Loại dữ liệu có thông số tổng quát là Nothing được biểu thị dưới dạng loại dữ liệu thô trong Java. Loại dữ liệu thô hiếm khi được dùng trong Java và bạn nên tránh sử dụng.

Ghi nhận lại ngoại lệ

Các hàm có thể gửi ngoại lệ đã kiểm tra phải ghi nhận lại những ngoại lệ này bằng @Throws. Ngoại lệ về thời gian chạy phải được ghi lại trong KDoc.

Hãy lưu ý đến các API mà một hàm tham chiếu đến vì chúng có thể gửi ngoại lệ đã kiểm tra mà Kotlin ngầm cho phép lan truyền.

Bản sao phòng vệ

Khi trả về các tập hợp được chia sẻ hoặc các tập hợp chỉ được phép đọc và không có người sở hữu từ API công khai, hãy đưa các tập hợp đó vào một vùng chứa không thể sửa đổi hoặc thực hiện sao chép phòng vệ. Mặc dù Kotlin thực thi thuộc tính chỉ đọc của các tập hợp này, nhưng Java thì không thực thi như vậy. Khi không có vùng chứa bao bọc hoặc bản sao phòng vệ, các thuộc tính bất biến có thể bị vi phạm bằng cách trả về một tệp tham chiếu tập hợp dài hạn.

Hàm companion (đồng hành)

Các hàm công khai trong một đối tượng companion phải được chú giải bằng @JvmStatic để có thể biểu thị dưới dạng phương thức tĩnh.

Nếu không có chú giải, các hàm này chỉ dùng được làm instance method (phương thức thực thể) trên trường Companion tĩnh.

Không đúng: không có chú giải

class KotlinClass {
    companion object {
        fun doWork() {
            /* … */
        }
    }
}
public final class JavaClass {
    public static void main(String... args) {
        KotlinClass.Companion.doWork();
    }
}

Đúng:chú giải @JvmStatic

class KotlinClass {
    companion object {
        @JvmStatic fun doWork() {
            /* … */
        }
    }
}
public final class JavaClass {
    public static void main(String... args) {
        KotlinClass.doWork();
    }
}

Hằng companion (đồng hành)

Các thuộc tính công khai, không phải const và là hằng có hiệu lực trong một companion object phải được chú thích bằng @JvmField để biểu thị dưới dạng trường tĩnh.

Nếu không có chú giải, các thuộc tính này chỉ có mặt dưới dạng thực thể có tên kỳ lạ là "getters" trên trường Companion tĩnh. Việc sử dụng @JvmStatic thay vì @JvmField sẽ di chuyển các "getters" có tên kỳ lạ sang các phương thức tĩnh trên lớp, nhưng như vậy thì vẫn chưa đúng.

Không đúng: không có chú giải

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.Companion.getBIG_INTEGER_ONE());
    }
}

Không chính xác: @JvmStatic chú giải

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmStatic val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.getBIG_INTEGER_ONE());
    }
}

Đúng:chú giải @JvmField

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmField val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.BIG_INTEGER_ONE);
    }
}

Đặt tên theo quy ước ngôn ngữ

Kotlin có các quy ước gọi hàm khác với Java. Điều có thể thay đổi cách bạn đặt tên cho các hàm. Sử dụng @JvmName để thiết kế tên sao cho phù hợp với quy ước của cả hai ngôn ngữ hoặc phù hợp với cách đặt tên thư viện chuẩn tương ứng.

Điều này thường xảy ra nhất đối với hàm mở rộng và thuộc tính mở rộng do khác nhau về vị trí của loại nhận.

sealed class Optional
data class Some(val value: T): Optional()
object None : Optional()

@JvmName("ofNullable")
fun  T?.asOptional() = if (this == null) None else Some(this)
// FROM KOTLIN:
fun main(vararg args: String) {
    val nullableString: String? = "foo"
    val optionalString = nullableString.asOptional()
}
// FROM JAVA:
public static void main(String... args) {
    String nullableString = "Foo";
    Optional optionalString =
          Optionals.ofNullable(nullableString);
}

Nạp chồng hàm cho giá trị mặc định

Các hàm chứa tham số có giá trị mặc định phải sử dụng @JvmOverloads. Nếu không có chú giải này, thì không thể gọi hàm đó bằng bất kỳ giá trị mặc định nào.

Khi sử dụng @JvmOverloads, hãy kiểm tra các phương thức được tạo để đảm bảo rằng các phương thức đó đều hợp lý. Nếu không, hãy thực hiện lại một hoặc cả hai bước tái cấu trúc như sau cho đến khi bạn hài lòng:

  • Thay đổi thứ tự thông số để ưu tiên đặt các thông số có giá trị mặc định ở cuối.
  • Di chuyển giá trị mặc định vào phương thức nạp chồng hàm thủ công

Không đúng: Không có @JvmOverloads

class Greeting {
    fun sayHello(prefix: String = "Mr.", name: String) {
        println("Hello, $prefix $name")
    }
}
public class JavaClass {
    public static void main(String... args) {
        Greeting greeting = new Greeting();
        greeting.sayHello("Mr.", "Bob");
    }
}

Đúng:chú giải @JvmOverloads.

class Greeting {
    @JvmOverloads
    fun sayHello(prefix: String = "Mr.", name: String) {
        println("Hello, $prefix $name")
    }
}
public class JavaClass {
    public static void main(String... args) {
        Greeting greeting = new Greeting();
        greeting.sayHello("Bob");
    }
}

Kiểm tra để tìm lỗi mã nguồn

Yêu cầu

  • Phiên bản Android Studio: 3.2 Canary 10 trở lên
  • Phiên bản Plugin Android cho Gradle: 3.2 trở lên

Các bước kiểm tra được hỗ trợ

Hiện đã có các bước kiểm tra để tìm lỗi mã nguồn cho Android. Điều này sẽ giúp bạn phát hiện và gắn cờ một số vấn đề về khả năng tương tác như mô tả ở trên. Hiện tại, việc kiểm tra chỉ có thể phát hiện vấn đề trong Java (khi sử dụng trong Kotlin). Cụ thể, các bước kiểm tra được hỗ trợ bao gồm:

  • Không xác định được tính chất rỗng
  • Quyền truy cập vào thuộc tính
  • Không có từ khoá cố định trong Kotlin
  • Thông số Lambda nằm ở cuối cùng

Android Studio

Để bật các bước kiểm tra này, hãy chuyển đến File (Tệp) > Preferences (Tuỳ chọn) > Editor (Chỉnh sửa) > Inspections (Kiểm soát) và kiểm tra các quy tắc mà bạn muốn bật trong phần Khả năng tương thích của Kotlin:

Hình 1. Các tuỳ chọn cài đặt về khả năng tương thích của Kotlin trong Android Studio.

Sau khi bạn chọn các quy tắc mà mình muốn bật, các bước kiểm tra mới này sẽ chạy khi bạn chạy công cụ kiểm tra mã (Analyze (Phân tích) > Inspect Code (Kiểm tra mã)…)

Bản dựng dòng lệnh

Để bật các bước kiểm tra này thông qua bản dựng dòng lệnh, hãy thêm dòng lệnh sau đây vào tệp build.gradle của bạn:

Groovy

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

Để biết tập hợp đầy đủ các cấu hình được hỗ trợ bên trong lintOptions, hãy tham khảo bài viết Tham khảo về DSL dành cho Gradle của Android.

Sau đó, chạy ./gradlew lint từ dòng lệnh.