Hướng dẫn về khả năng tương thích Kotlin-Java

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

Lần cập nhật gần đây nhất: 29/07/2024

Java (khi sử dụng trong Kotlin)

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

Đừng dùng từ khoá cố định của Kotlin làm tên phương thức hoặc trường khác. 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 từ Kotlin. Từ khoá không cố định, từ khoá bổ trợgiá trị nhận dạng đặc biệt đều được cho phép.

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 sử dụng tên của hàm mở rộng trên Any cho hoặc tên của thuộc tính mở rộng trên Any cho trừ khi thực sự cần thiết. Mặc dù các trường và phương thức thành phần sẽ luôn luôn được ưu tiên hơn các hàm hoặc thuộc tính mở rộng của Any, nó có thể khó biết được 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. Loại không có chú thích sẽ được hiểu là "nền tảng" loại dữ liệu 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 đó 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 Flowable.create() của RxJava 2 được xác định là:

public static <T> Flowable<T> create(
    FlowableOnSubscribe<T> 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 từ 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, kiểu "bean" nghiêm ngặt phải sử dụng tiền tố.

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

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 hiển 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, set hoặc không có tiền tố get. Phương thức có tiền tố không chuẩn vẫn có thể gọi dưới dạng 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 (chẳng hạn như nạp chồng toán tử trong Kotlin). Đảm bảo rằng tên phương thức là như vậy sẽ có ý nghĩa khi sử dụng với 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ú thích tệp đó với @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 không hấp dẫn và để lộ ngôn từ khi triển khai chi tiết hơn.

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

Đối số lambda

Giao diện phương thức đơn (SAM) được xác định trong Java có thể triển khai được bằng cả Kotlin và Java dùng cú pháp lambda, cùng dòng triển khai theo thành 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 có một chút khác biệt.

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

Các hàm bậc cao hơn dùng trong Java không được nhận các loại hàm trả về Unit như sẽ yêu cầu phương thức gọi Java trả về Unit.INSTANCE. Thay vì dùng cùng dòng hàm nhập vào chữ ký, hãy sử dụng các giao diện chức năng (SAM). Ngoài ra hãy cân nhắc dùng các giao diện chức năng (SAM) thay vì các giao diện khi xác định các giao diện dự kiến sẽ dùng làm lambda, cho phép sử dụng thành ngữ từ 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ý. Comparable (So sánh được) là một ví dụ nổi bật, vì so sánh this với other và lambda không có this. Không phải thêm tiền tố fun vào giao diện này để buộc phương thức gọi sử dụng object : ... để cho phép mã có 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ó tham số tổng quát là Nothing được biểu thị dưới dạng loại dữ liệu thô trong Java. Thô loại dữ liệu 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ó thể đọc không có người sở hữu từ API công khai, hãy gói chúng trong 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 họ, thì không có hành động thực thi nào như vậy trên Java ở bên. Khi không có vùng chứa bao bọc hoặc bản sao phòng vệ, các bất biến có thể bị vi phạm bằng cách trả về một tệp tham chiếu bộ sưu tậ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 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 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 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ó sẵn dưới dạng có tên kỳ lạ thực thể "getters" trên trường Companion tĩnh. Đang dùng @JvmStatic trong số @JvmField di chuyển các "getters" có tên kỳ lạ vào các phương thức tĩnh trên lớp, nên vẫn chưa chính xác.

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 thân thuộc cho quy ước của cả hai ngôn ngữ hoặc để phù hợp với thư viện chuẩn tương ứng của chúng khi đặt tên.

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

sealed class Optional<T : Any>
data class Some<T : Any>(val value: T): Optional<T>()
object None : Optional<Nothing>()

@JvmName("ofNullable")
fun <T> 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<String> 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 đã tạo để đảm bảo từng phương thức dễ hiểu. Nếu không, hãy thực hiện lại một hoặc cả hai lần tái cấu trúc sau đây cho đến khi 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 về phía kết thúc.
  • 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 nhằm giúp bạn phát hiện và gắn cờ một số Các vấn đề về khả năng tương tác đã mô tả ở trên. Chỉ các vấn đề trong Java (đối với Kotlin mức tiêu thụ) 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 Tệp > Lựa chọn ưu tiên > Trình chỉnh sửa > Quy trình kiểm tra và hãy kiểm tra các quy tắc mà bạn muốn bật trong phần Khả năng tương tác 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 sẽ 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 sau 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.