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

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

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) { /* … */ }

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

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

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

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

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

หากต้องการแสดงเมธอดเป็นพร็อพเพอร์ตี้ใน Kotlin คุณต้องใช้คำนำหน้าสไตล์ "Bean" ที่เข้มงวด

เมธอดตัวเข้าถึงต้องมีคำนำหน้า 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 เมธอดที่มีคำนำหน้าที่ไม่ใช่มาตรฐาน ยังคงเรียกใช้เป็นฟังก์ชันได้ ซึ่งอาจยอมรับได้ขึ้นอยู่กับ ลักษณะการทำงานของเมธอด

การโอเวอร์โหลดโอเปอเรเตอร์

โปรดระมัดระวังชื่อเมธอดที่อนุญาตให้ใช้ไวยากรณ์การเรียกใช้พิเศษ (เช่น Operator Overloading ใน 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

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

คำจำกัดความที่แนะนำ

ฟังก์ชันลำดับสูงที่ตั้งใจจะใช้จาก Java ไม่ควรใช้ประเภทฟังก์ชันที่ส่งคืน Unit เนื่องจากจะ กำหนดให้ผู้เรียกใช้ Java ส่งคืน 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 แต่ก็ยังควรทำให้เป็นอินเทอร์เฟซที่มีชื่อเพื่อให้ผู้เรียกใช้สามารถนำไปใช้กับคลาสที่มีชื่อได้ ไม่ใช่แค่ Lambda (ทั้งใน Kotlin และ Java)

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

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

พิจารณาคำจำกัดความของ Kotlin ต่อไปนี้

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

โดยกำหนดให้ผู้เรียกใช้ Java แสดงผล Unit.INSTANCE ดังนี้

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

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

เมื่อการใช้งานอินเทอร์เฟซมีจุดประสงค์เพื่อมีสถานะ การใช้ไวยากรณ์ Lambda จะไม่สมเหตุสมผล Comparable เป็นตัวอย่างที่โดดเด่น เนื่องจากมีไว้เพื่อเปรียบเทียบ 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 โดยทั่วไปแล้ว ประเภท Raw ไม่ค่อยได้ใช้ใน 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 เพื่อให้แสดงเป็นฟิลด์แบบคงที่

หากไม่มีคำอธิบายประกอบ พร็อพเพอร์ตี้เหล่านี้จะใช้ได้เฉพาะในรูปแบบ "ตัวรับ" ของอินสแตนซ์ที่มีชื่อแปลกๆ ในฟิลด์ Companion แบบคงที่เท่านั้น การใช้ @JvmStatic แทน @JvmField จะย้าย "ตัวรับ" ที่มีชื่อแปลกๆ ไปยังเมธอดแบบคงที่ในคลาส ซึ่งยังคงไม่ถูกต้อง

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

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 เพื่อออกแบบชื่อให้ดูเป็นสำนวน สำหรับทั้งแบบแผนของภาษาหรือให้ตรงกับการตั้งชื่อไลบรารีมาตรฐานที่เกี่ยวข้อง

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

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

การตรวจสอบ Lint

ข้อกำหนด

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

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

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

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

Android Studio

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

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

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

การสร้างบรรทัดคำสั่ง

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

ดึงดูด

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

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

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