คำแนะนำเกี่ยวกับการทำงานร่วมกันของ Kotlin-Java

เอกสารนี้เป็นชุดกฎสำหรับการเขียน API สาธารณะใน Java และ Kotlin โดยมีเจตนาให้โค้ดมีลักษณะเป็นสำนวนเมื่อนำมาใช้จาก ภาษา

อัปเดตล่าสุด: 29-07-2024

Java (สำหรับการใช้ Kotlin)

ไม่มีคีย์เวิร์ดที่ยาก

อย่าใช้คีย์เวิร์ดแข็งใดๆ ของ Kotlin เป็นชื่อวิธีการ หรือช่องต่างๆ ซึ่งจำเป็นต้องใช้เครื่องหมายแบ็กทิกเพื่อหลบหนีเมื่อเรียกจาก Kotlin คีย์เวิร์ดชั่วคราว คีย์เวิร์ดตัวแก้ไข และ อนุญาตให้ใช้ตัวระบุพิเศษ

ตัวอย่างเช่น ฟังก์ชัน when ของ Mockito ต้องใช้เครื่องหมายแบ็กทิกเมื่อใช้จาก Kotlin ดังนี้

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

หลีกเลี่ยงการใช้ชื่อส่วนขยายAny

หลีกเลี่ยงการใช้ชื่อฟังก์ชันส่วนขยายใน Any สำหรับ หรือชื่อของคุณสมบัติของส่วนขยายใน Any สำหรับ เว้นแต่มีความจำเป็นอย่างยิ่ง ในขณะที่วิธีการและฟิลด์สำหรับสมาชิกจะ มีความสำคัญเหนือฟังก์ชันหรือพร็อพเพอร์ตี้ส่วนขยายของ Any เวลาอ่านรหัสจะยาก เมื่อจะรู้ว่าจะเรียกรหัสใด

คำอธิบายประกอบความสามารถในการเว้นว่าง

พารามิเตอร์ ผลลัพธ์ และประเภทฟิลด์ที่ไม่ใช่แบบพื้นฐานใน API สาธารณะควร มีข้อมูลเสริมที่เป็นค่าว่าง ประเภทที่ไม่มีคำอธิบายประกอบจะถูกตีความว่า "แพลตฟอร์ม" ประเภท ซึ่งมีความสามารถในการเว้นว่างไม่ชัดเจน

โดยค่าเริ่มต้น แฟล็กของคอมไพเลอร์ Kotlin จะใช้คำอธิบายประกอบ JSR 305 แต่จะมีการแฟล็ก โดยมีคำเตือน คุณยังสามารถตั้งค่าแฟล็กเพื่อให้คอมไพเลอร์ดำเนินการกับ คำอธิบายประกอบว่าเป็นข้อผิดพลาด

พารามิเตอร์ Lambda ลำดับสุดท้าย

ประเภทพารามิเตอร์ที่มีสิทธิ์สําหรับ Conversion ของ SAM ควรเป็นประเภทสุดท้าย

ตัวอย่างเช่น ลายเซ็นเมธอด Flowable.create() ของ RxJava 2 จะมีคำจำกัดความดังนี้

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

เนื่องจาก FlowableOnSubscription มีสิทธิ์สำหรับ Conversion SAM การเรียกใช้ฟังก์ชัน วิธีการจาก Kotlin มีลักษณะดังนี้

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

แต่หากมีการกลับพารามิเตอร์ในลายเซ็นเมธอด การเรียกใช้ฟังก์ชัน สามารถใช้ไวยากรณ์ lambda ต่อท้ายได้ เช่น

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

คำนำหน้าพร็อพเพอร์ตี้

สำหรับวิธีการที่จะนำเสนอเป็นพร็อพเพอร์ตี้ใน Kotlin รูปแบบ "ถั่ว" ที่เข้มงวด ต้องใช้คำนำหน้า

เมธอดของตัวเข้าถึงต้องมีคำนำหน้า get หรือสำหรับเมธอดส่งกลับแบบบูลีน 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()

เมธอด Mutator ที่เกี่ยวข้องต้องมีคำนำหน้า 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)

หากต้องการให้เปิดเผยเมธอดเป็นพร็อพเพอร์ตี้ อย่าใช้คำนำหน้าที่ไม่เป็นมาตรฐาน เช่น ตัวเข้าถึง has, set หรือที่ไม่ใช่ get นำหน้า วิธีการที่มีคำนำหน้าที่ไม่เป็นมาตรฐาน ยังคงเรียกใช้เป็นฟังก์ชันได้ ซึ่งอาจยอมรับได้โดยขึ้นอยู่กับ ของเมธอด

โอเปอเรเตอร์มากเกินไป

โปรดระวังชื่อวิธีการที่อนุญาตให้ใช้ไวยากรณ์ที่เรียกใช้เว็บไซต์สำหรับการโทรพิเศษ (เช่น โอเวอร์โหลดใน Kotlin) ตรวจสอบให้แน่ใจว่าเมธอดตั้งชื่อเป็น จึงเป็นโอกาสเหมาะอย่างยิ่งสำหรับการใช้กับไวยากรณ์ที่สั้นลง

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 (สำหรับการใช้ Java)

ชื่อไฟล์

เมื่อไฟล์มีฟังก์ชันหรือพร็อพเพอร์ตี้ระดับบนสุด ให้ใส่คำอธิบายประกอบเสมอ ด้วย @file:JvmName("Foo") เพื่อระบุชื่อดีๆ

โดยค่าเริ่มต้น สมาชิกระดับสูงในไฟล์ MyClass.kt จะรวมอยู่ในชั้นเรียนชื่อ MyClassKt ซึ่งไม่น่าสนใจและทำให้เกิดการนำไปใช้อย่างไม่ถูกต้อง รายละเอียด

ลองเพิ่ม @file:JvmMultifileClass เพื่อรวมสมาชิกระดับบนสุดจาก หลายไฟล์รวมกันเป็น ชั้นเรียนเดียวได้

อาร์กิวเมนต์ Lambda

อินเทอร์เฟซแบบ Single Method (SAM) ที่กำหนดไว้ใน Java สามารถนำไปใช้ทั้งใน Kotlin ได้ และ Java โดยใช้ไวยากรณ์ lambda ซึ่งเข้าใจการนำไปใช้ในรูปแบบสำนวน Kotlin มีตัวเลือกมากมายสำหรับการกำหนดอินเทอร์เฟซดังกล่าว โดยแต่ละตัวเลือกมี แตกต่างกัน

คำจำกัดความที่ต้องการ

ฟังก์ชันลําดับที่สูงขึ้นซึ่งมีไว้สำหรับใช้จาก Java ไม่ควรใช้ประเภทฟังก์ชันที่แสดง Unit เช่นเดียวกับ ต้องใช้ Java Caller แสดงผล Unit.INSTANCE แทนที่จะใส่ฟังก์ชันไว้ในบรรทัด พิมพ์ลายเซ็นในลายเซ็น ใช้อินเทอร์เฟซฟังก์ชัน (SAM) และ ให้พิจารณาใช้อินเทอร์เฟซที่ทำงาน (SAM) แทนการใช้ เมื่อกำหนดอินเทอร์เฟซที่คาดว่าจะใช้เป็น lambda ซึ่งอนุญาตให้ใช้สำนวนการใช้งานจาก Kotlin

ลองดูคำจำกัดความของ Kotlin นี้

fun interface GreeterCallback {
  fun greetName(String name)
}

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

เมื่อเรียกใช้จาก Kotlin

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

เมื่อเรียกใช้จาก Java:

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

แม้ว่าประเภทฟังก์ชันจะไม่แสดง Unit แต่ประเภทฟังก์ชันก็อาจยังคงเป็น "ดี" ที่จะทำให้เป็นอินเทอร์เฟซที่มีชื่อเพื่อให้ผู้โทรสามารถใช้งานด้วย ไม่ใช่แค่ lambdas (ทั้งใน Kotlin และ Java)

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

หลีกเลี่ยงประเภทฟังก์ชันที่แสดง Unit

ลองดูคำจำกัดความของ Kotlin นี้

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

จำเป็นต้องใช้ Java Caller แสดงผล Unit.INSTANCE:

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

หลีกเลี่ยงอินเทอร์เฟซฟังก์ชันเมื่อการใช้งานควรมีสถานะ

สำหรับการใช้งานอินเทอร์เฟซต้องมีสถานะ โดยใช้ lambda ไม่สมเหตุสมผล แบบเปรียบเทียบเป็นตัวอย่างที่เห็นได้ชัด เนื่องจากเป็นตัวช่วยเปรียบเทียบ this กับ other และ lambda ไม่มี this ไม่ใช่ คำนำหน้าอินเทอร์เฟซด้วย fun จะบังคับให้ผู้โทรใช้ object : ... ทำให้สามารถระบุสถานะและให้คำแนะนำแก่ผู้โทร

ลองดูคำจำกัดความของ Kotlin นี้

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

เพื่อป้องกันการเกิดไวยากรณ์ lambda ใน Kotlin ซึ่งต้องใช้เวอร์ชันที่ยาวกว่านี้

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

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

หลีกเลี่ยงคำทั่วไป Nothing คำ

ประเภทที่มีพารามิเตอร์ทั่วไปคือ Nothing จะแสดงเป็นประเภทข้อมูลดิบใน Java ดิบ ชนิดไม่ค่อยใช้ใน Java และควรหลีกเลี่ยง

ข้อยกเว้นของเอกสาร

ฟังก์ชันที่ส่งข้อยกเว้นที่เลือกได้ควรระบุเป็นเอกสารประกอบ @Throws คุณควรระบุข้อยกเว้นรันไทม์ไว้ในเอกสาร KDoc

ใส่ใจกับ API ที่ฟังก์ชันได้รับมอบสิทธิ์ให้ด้วย เนื่องจากอาจมีการตรวจสอบ ยกเว้นที่ Kotlin อนุญาตให้เผยแพร่โดยไม่มีการแจ้งเตือน

การป้องกันสำเนา

เมื่อแสดงผลคอลเล็กชันแบบอ่านอย่างเดียวที่แชร์หรือไม่เป็นเจ้าของจาก API สาธารณะ ให้รวม ไว้ในคอนเทนเนอร์ที่แก้ไขไม่ได้หรือทำสำเนาเพื่อป้องกัน แม้จะมี Kotlin การบังคับใช้พร็อพเพอร์ตี้แบบอ่านอย่างเดียวโดยไม่มีการบังคับใช้ใน Java หากไม่มี Wrapper หรือการป้องกัน จะละเมิดค่าคงที่ได้ด้วยวิธี แสดงข้อมูลอ้างอิงคอลเล็กชันที่มีระยะเวลานาน

ฟังก์ชันแยกหน้าจอประชุม

ฟังก์ชันสาธารณะในออบเจ็กต์ที่แสดงร่วมต้องมีคำอธิบายประกอบด้วย @JvmStatic เป็นวิธีการแบบคงที่

หากไม่มีคำอธิบายประกอบ ฟังก์ชันเหล่านี้จะใช้เป็นวิธีอินสแตนซ์เท่านั้น ในช่อง Companion แบบคงที่

ไม่ถูกต้อง: ไม่มีคำอธิบายประกอบ

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

ถูกต้อง: @JvmStatic คำอธิบายประกอบ

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

ค่าคงที่ที่แสดงร่วมกัน

พร็อพเพอร์ตี้สาธารณะที่ไม่ใช่ const ซึ่งเป็นค่าคงที่ที่มีประสิทธิภาพใน companion object จะต้องกำกับด้วย @JvmField เพื่อแสดงเป็นช่องแบบคงที่

หากไม่มีคำอธิบายประกอบ คุณสมบัติเหล่านี้จะมีชื่อแปลกๆ เท่านั้น เช่น "getters" ในช่อง Companion แบบคงที่ ใช้ @JvmStatic แทน ของ @JvmField ย้าย "getters" ที่มีชื่อแปลกๆ ไปยังเมธอดแบบคงที่ในชั้นเรียน แต่ยังไม่ถูกต้อง

ไม่ถูกต้อง: ไม่มีคำอธิบายประกอบ

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());
    }
}

ไม่ถูกต้อง: @JvmStatic หมายเหตุ

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());
    }
}

ถูกต้อง: @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);
    }
}

การตั้งชื่อตามสำนวน

Kotlin มีวิธีการเรียกใช้ที่แตกต่างจาก Java ซึ่งอาจเปลี่ยนวิธีที่คุณ ฟังก์ชันชื่อ ใช้ @JvmName เพื่อออกแบบชื่อให้ฟังดูเป็นสำนวน สำหรับข้อตกลงของทั้ง 2 ภาษา หรือเพื่อจับคู่กับไลบรารีมาตรฐานที่เกี่ยวข้อง การตั้งชื่อ

กรณีนี้เกิดขึ้นบ่อยที่สุดกับฟังก์ชันของส่วนขยายและคุณสมบัติของส่วนขยาย เนื่องจากประเภทของตัวรับแตกต่างกัน

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);
}

ฟังก์ชันโอเวอร์โหลดสำหรับค่าเริ่มต้น

ฟังก์ชันที่มีพารามิเตอร์ที่มีค่าเริ่มต้นต้องใช้ @JvmOverloads หากไม่มีคำอธิบายประกอบนี้ จะเป็นไปไม่ได้ที่จะเรียกใช้ฟังก์ชันโดยใช้ ค่าเริ่มต้น

เมื่อใช้ @JvmOverloads ให้ตรวจสอบเมธอดที่สร้างขึ้นเพื่อให้แน่ใจว่าแต่ละเมธอด ที่สมเหตุสมผล หากไม่ตรง ให้ทำการเปลี่ยนโครงสร้างภายในโค้ดข้อใดข้อหนึ่งหรือทั้ง 2 อย่างต่อไปนี้ จนกว่าจะพอใจ:

  • เปลี่ยนลำดับของพารามิเตอร์ตามที่ต้องการให้พารามิเตอร์ที่มีค่าเริ่มต้นอยู่ตรงกับ สิ้นสุด
  • ย้ายค่าเริ่มต้นเป็นโอเวอร์โหลดของฟังก์ชันที่กำหนดเอง

ไม่ถูกต้อง: ไม่ใช่ @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");
    }
}

ถูกต้อง: @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");
    }
}

การตรวจสอบขุย

ข้อกำหนด

  • เวอร์ชัน Android Studio: 3.2 Canary 10 ขึ้นไป
  • เวอร์ชันปลั๊กอิน Android Gradle: 3.2 ขึ้นไป

การตรวจสอบที่รองรับ

ตอนนี้มีการตรวจสอบ Lint ของ Android ที่จะช่วยให้คุณตรวจหาและแจ้งว่า ปัญหาความสามารถในการทำงานร่วมกัน ตามที่อธิบายไว้ก่อนหน้านี้ เฉพาะปัญหาใน Java (สำหรับ Kotlin ทั้งหมด) ที่ตรวจพบ การตรวจสอบที่รองรับมีดังนี้

  • ค่าว่างที่ไม่รู้จัก
  • การเข้าถึงพร็อพเพอร์ตี้
  • ไม่มีคีย์เวิร์ดของ Hard Kotlin
  • สุดท้าย พารามิเตอร์ Lambda

Android Studio

หากต้องการเปิดใช้การตรวจสอบเหล่านี้ ให้ไปที่ไฟล์ > ค่ากำหนด > ตัวแก้ไข > การตรวจสอบและ ให้ตรวจสอบกฎที่ต้องการเปิดใช้ในส่วน Kotlin Interoperability ดังนี้

รูปที่ 1 การตั้งค่าความสามารถในการทำงานร่วมกันของ Kotlin ใน Android Studio

เมื่อคุณตรวจสอบกฎที่ต้องการเปิดใช้แล้ว การตรวจสอบใหม่จะ ทำงานเมื่อคุณเรียกใช้การตรวจสอบโค้ด (วิเคราะห์ > ตรวจสอบโค้ด...)

บิลด์บรรทัดคำสั่ง

หากต้องการเปิดใช้การตรวจสอบเหล่านี้จากบิลด์บรรทัดคำสั่ง ให้เพิ่มบรรทัดต่อไปนี้ใน ไฟล์ build.gradle ของคุณ:

ดึงดูด

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

สำหรับชุดการกำหนดค่าทั้งหมดที่รองรับภายใน lintOptions โปรดดู เอกสารอ้างอิง Gradle DSL สำหรับ Android

จากนั้นเรียกใช้ ./gradlew lint จากบรรทัดคำสั่ง