การเรียกใช้โค้ด Kotlin จาก Java

ในโค้ดแล็บนี้ คุณจะได้เรียนรู้วิธีเขียนหรือปรับโค้ด Kotlin เพื่อให้เรียกใช้จากโค้ด Java ได้อย่างราบรื่นยิ่งขึ้น

สิ่งที่คุณจะได้เรียนรู้

  • วิธีใช้@JvmField @JvmStatic และคำอธิบายประกอบอื่นๆ
  • ข้อจำกัดในการเข้าถึงฟีเจอร์ภาษา Kotlin บางอย่างจากโค้ด Java

สิ่งที่คุณต้องทราบอยู่แล้ว

Codelab นี้เขียนขึ้นสำหรับโปรแกรมเมอร์และมีสมมติฐานว่าคุณมีความรู้พื้นฐานเกี่ยวกับ Java และ Kotlin

Codelab นี้จำลองการย้ายข้อมูลส่วนหนึ่งของโปรเจ็กต์ขนาดใหญ่ที่เขียนด้วยภาษาโปรแกรม Java เพื่อรวมโค้ด Kotlin ใหม่

เพื่อให้ง่ายขึ้น เราจะมีไฟล์ .java ไฟล์เดียวชื่อ UseCase.java ซึ่งจะแสดงฐานของโค้ดที่มีอยู่

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

นำเข้าโปรเจ็กต์

คุณสามารถโคลนโค้ดของโปรเจ็กต์จากโปรเจ็กต์ GitHub ได้ที่นี่: GitHub

หรือคุณจะดาวน์โหลดและแตกไฟล์โปรเจ็กต์จากไฟล์เก็บถาวร zip ที่นี่ก็ได้

ดาวน์โหลด Zip

หากใช้ IntelliJ IDEA ให้เลือก "Import Project"

หากใช้ Android Studio ให้เลือก "นำเข้าโปรเจ็กต์ (Gradle, Eclipse ADT ฯลฯ)"

มาเปิด UseCase.java แล้วเริ่มแก้ไขข้อผิดพลาดที่เราเห็นกัน

ฟังก์ชันแรกที่มีปัญหาคือ registerGuest

public static User registerGuest(String name) {
   User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
   Repository.addUser(guest);
   return guest;
}

ข้อผิดพลาดสำหรับทั้ง Repository.getNextGuestId() และ Repository.addUser(...) จะเหมือนกันคือ "เข้าถึงแบบไม่คงที่จากบริบทแบบคงที่ไม่ได้"

ตอนนี้เรามาดูไฟล์ Kotlin กัน เปิดไฟล์ Repository.kt

เราเห็นว่าที่เก็บเป็น Singleton ที่ประกาศโดยใช้คีย์เวิร์ดออบเจ็กต์ ปัญหาคือ Kotlin สร้างอินสแตนซ์แบบคงที่ภายในคลาสของเรา แทนที่จะแสดงเป็นพร็อพเพอร์ตี้และเมธอดแบบคงที่

เช่น คุณอาจอ้างอิง Repository.getNextGuestId() โดยใช้ Repository.INSTANCE.getNextGuestId() แต่ก็มีวิธีที่ดีกว่า

เราสามารถทำให้ Kotlin สร้างเมธอดและพร็อพเพอร์ตี้แบบคงที่ได้โดยการใส่คำอธิบายประกอบพร็อพเพอร์ตี้และเมธอดสาธารณะของ Repository ด้วย @JvmStatic:

object Repository {
   val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

เพิ่มคำอธิบายประกอบ @JvmStatic ลงในโค้ดโดยใช้ IDE

หากเราเปลี่ยนกลับไปใช้ UseCase.java พร็อพเพอร์ตี้และเมธอดใน Repository จะไม่ทำให้เกิดข้อผิดพลาดอีกต่อไป ยกเว้น Repository.BACKUP_PATH เราจะกลับมาพูดถึงเรื่องนี้ในภายหลัง

ตอนนี้มาแก้ไขข้อผิดพลาดถัดไปในเมธอด registerGuest() กัน

ลองพิจารณาสถานการณ์ต่อไปนี้ เรามีคลาส StringUtils ที่มีฟังก์ชันแบบคงที่หลายฟังก์ชันสำหรับการดำเนินการสตริง เมื่อแปลงเป็น Kotlin เราได้แปลงเมธอดเป็นฟังก์ชันส่วนขยาย Java ไม่มีฟังก์ชันส่วนขยาย ดังนั้น Kotlin จึงคอมไพล์เมธอดเหล่านี้เป็นฟังก์ชันแบบคงที่

แต่หากดูที่เมธอด registerGuest() ใน UseCase.java เราจะเห็นว่ามีบางอย่างผิดปกติ

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

เนื่องจาก Kotlin จะวางฟังก์ชัน "ระดับบนสุด" หรือฟังก์ชันระดับแพ็กเกจเหล่านี้ไว้ในคลาสที่มีชื่อตามชื่อไฟล์ ในกรณีนี้ เนื่องจากไฟล์ชื่อ StringUtils.kt คลาสที่เกี่ยวข้องจึงชื่อ StringUtilsKt

เราสามารถเปลี่ยนการอ้างอิงทั้งหมดของ StringUtils เป็น StringUtilsKt และแก้ไขข้อผิดพลาดนี้ได้ แต่ก็ไม่ใช่วิธีที่ดีที่สุดเนื่องจาก

  • อาจมีหลายที่ในโค้ดของเราที่ต้องอัปเดต
  • ชื่อเองก็ดูแปลกๆ

ดังนั้นแทนที่จะปรับโครงสร้างโค้ด Java เรามาอัปเดตโค้ด Kotlin เพื่อใช้ชื่ออื่นสำหรับเมธอดเหล่านี้กัน

เปิด StringUtils.Kt แล้วค้นหาการประกาศแพ็กเกจต่อไปนี้

package com.google.example.javafriendlykotlin

เราสามารถบอกให้ Kotlin ใช้ชื่ออื่นสำหรับเมธอดระดับแพ็กเกจได้โดยใช้@file:JvmNameคำอธิบายประกอบ มาใช้คำอธิบายประกอบนี้เพื่อตั้งชื่อคลาส StringUtils กัน

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

ตอนนี้หากเราย้อนกลับไปดูที่ UseCase.java เราจะเห็นว่าข้อผิดพลาดสำหรับ StringUtils.nameToLogin() ได้รับการแก้ไขแล้ว

ขออภัย เราได้แทนที่ข้อผิดพลาดนี้ด้วยข้อผิดพลาดใหม่เกี่ยวกับพารามิเตอร์ที่ส่งไปยังตัวสร้างสำหรับ User มาทำตามขั้นตอนถัดไปและแก้ไขข้อผิดพลาดสุดท้ายนี้ใน UseCase.registerGuest() กัน

Kotlin รองรับค่าเริ่มต้นสำหรับพารามิเตอร์ เราดูได้ว่ามีการใช้ตัวแปรอย่างไรโดยดูที่ภายในinitบล็อกของRepository.kt

Repository.kt:

_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))

เราจะเห็นว่าสำหรับผู้ใช้ "warlow" เราสามารถข้ามการใส่ค่าสำหรับ displayName ได้เนื่องจากมีการระบุค่าเริ่มต้นสำหรับ displayName ใน User.kt

User.kt:

data class User(
   val id: Int,
   val username: String,
   val displayName: String = username.toTitleCase(),
   val groups: List<String> = listOf("guest")
)

แต่การเรียกใช้เมธอดจาก Java จะไม่ทำงานในลักษณะเดียวกัน

UseCase.java:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

ภาษาโปรแกรม Java ไม่รองรับค่าเริ่มต้น หากต้องการแก้ไขปัญหานี้ เราจะบอกให้ Kotlin สร้างการโอเวอร์โหลดสำหรับตัวสร้างของเราโดยใช้คำอธิบายประกอบ @JvmOverloads

ก่อนอื่น เราต้องทำการอัปเดตเล็กน้อยกับ User.kt

เนื่องจากคลาส User มีเพียงตัวสร้างหลักตัวเดียว และตัวสร้างไม่มีคำอธิบายประกอบใดๆ เราจึงละเว้นคีย์เวิร์ด constructor แต่ตอนนี้เราต้องการใส่คำอธิบายประกอบ จึงต้องใส่constructorคีย์เวิร์ด

data class User constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

เมื่อมีคีย์เวิร์ด constructor เราจะเพิ่มคำอธิบายประกอบ @JvmOverloads ได้

data class User @JvmOverloads constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

หากเราเปลี่ยนกลับไปเป็น UseCase.java เราจะเห็นว่าไม่มีข้อผิดพลาดในฟังก์ชัน registerGuest อีกต่อไป

ขั้นตอนถัดไปคือการแก้ไขการเรียกที่เสียไปยัง user.hasSystemAccess() ใน UseCase.getSystemUsers() ดำเนินการตามขั้นตอนถัดไป หรืออ่านต่อเพื่อดูรายละเอียดเพิ่มเติมเกี่ยวกับสิ่งที่ @JvmOverloads ได้ทำเพื่อแก้ไขข้อผิดพลาด

@JvmOverloads

มาสร้างเมธอดทดสอบใน UseCase.java เพื่อให้เข้าใจว่า @JvmOverloads ทำอะไรได้บ้าง

private void testJvmOverloads() {
   User syrinx = new User(1001, "syrinx");
   User ione = new User(1002, "ione", "Ione Saldana");

   List<String> groups = new ArrayList<>();
   groups.add("staff");
   User beaulieu = new User(1002, "beaulieu", groups);
}

เราสร้าง User ได้โดยใช้พารามิเตอร์เพียง 2 รายการ ได้แก่ id และ username

User syrinx = new User(1001, "syrinx");

นอกจากนี้ เรายังสร้าง User โดยใส่พารามิเตอร์ที่ 3 สำหรับ displayName ขณะที่ยังคงใช้ค่าเริ่มต้นสำหรับ groups ได้ด้วย ดังนี้

User ione = new User(1002, "ione", "Ione Saldana");

แต่คุณไม่สามารถข้าม displayName และระบุค่าสำหรับ groups โดยไม่ต้องเขียนโค้ดเพิ่มเติมได้

ดังนั้นเรามาลบบรรทัดนั้นหรือใส่ "//" ไว้ข้างหน้าเพื่อแสดงความคิดเห็นกัน

ใน Kotlin หากต้องการรวมพารามิเตอร์เริ่มต้นและพารามิเตอร์ที่ไม่ใช่ค่าเริ่มต้น เราต้องใช้พารามิเตอร์ที่มีชื่อ

// This doesn't work...
User(104, "warlow", listOf("staff", "inactive"))
// But using named parameters, it does...
User(104, "warlow", groups = listOf("staff", "inactive"))

เนื่องจาก Kotlin จะสร้างการโอเวอร์โหลดสำหรับฟังก์ชัน รวมถึงตัวสร้าง แต่จะสร้างการโอเวอร์โหลดเพียง 1 รายการต่อพารามิเตอร์ที่มีค่าเริ่มต้น

มาย้อนกลับไปดู UseCase.java และแก้ไขปัญหาถัดไปกัน นั่นคือการเรียกใช้ user.hasSystemAccess() ในเมธอด UseCase.getSystemUsers()

public static List<User> getSystemUsers() {
   ArrayList<User> systemUsers = new ArrayList<>();
   for (User user : Repository.getUsers()) {
       if (user.hasSystemAccess()) {     // Now has an error!
           systemUsers.add(user);
       }
   }
   return systemUsers;
}

ข้อผิดพลาดนี้น่าสนใจ หากใช้ฟีเจอร์เติมข้อความอัตโนมัติของ IDE ในคลาส User คุณจะเห็นว่ามีการเปลี่ยนชื่อ hasSystemAccess() เป็น getHasSystemAccess()

หากต้องการแก้ไขปัญหานี้ เราต้องการให้ Kotlin สร้างชื่ออื่นสำหรับvalพร็อพเพอร์ตี้ hasSystemAccess โดยเราสามารถใช้คำอธิบายประกอบ @JvmName เพื่อดำเนินการดังกล่าวได้ มาเปลี่ยนกลับไปที่ User.kt แล้วดูว่าเราควรใช้ที่ใด

เราสามารถใช้คำอธิบายประกอบได้ 2 วิธี วิธีแรกคือการใช้กับget()เมธอดโดยตรง ดังนี้

val hasSystemAccess
   @JvmName("hasSystemAccess")
   get() = "sys" in groups

ซึ่งจะเป็นสัญญาณให้ Kotlin เปลี่ยนลายเซ็นของตัวรับค่าที่กำหนดไว้อย่างชัดเจนเป็นชื่อที่ระบุ

หรือจะใช้กับพร็อพเพอร์ตี้โดยใช้คำนำหน้า get: ในลักษณะนี้ก็ได้

@get:JvmName("hasSystemAccess")
val hasSystemAccess
   get() = "sys" in groups

วิธีอื่นนี้มีประโยชน์อย่างยิ่งสำหรับพร็อพเพอร์ตี้ที่ใช้ตัวรับค่าเริ่มต้นที่กำหนดโดยนัย เช่น

@get:JvmName("isActive")
val active: Boolean

ซึ่งจะช่วยให้เปลี่ยนชื่อของ Getter ได้โดยไม่ต้องกำหนด Getter อย่างชัดเจน

แม้จะมีความแตกต่างกัน แต่คุณก็สามารถใช้คำใดก็ได้ที่รู้สึกว่าเหมาะสมกับคุณ ทั้ง 2 แบบจะทำให้ Kotlin สร้างตัวรับค่าที่มีชื่อว่า hasSystemAccess()

หากเราเปลี่ยนกลับไปเป็น UseCase.java เราจะยืนยันได้ว่าตอนนี้ getSystemUsers() ไม่มีข้อผิดพลาดแล้ว

ข้อผิดพลาดถัดไปอยู่ใน formatUser() แต่หากต้องการอ่านเพิ่มเติมเกี่ยวกับรูปแบบการตั้งชื่อตัวรับใน Kotlin ให้อ่านต่อที่นี่ก่อนไปยังขั้นตอนถัดไป

การตั้งชื่อ Getter และ Setter

เมื่อเขียน Kotlin เราอาจลืมไปว่าการเขียนโค้ด เช่น

val myString = "Logged in as ${user.displayName}")

เป็นการเรียกใช้ฟังก์ชันเพื่อรับค่าของ displayName จริงๆ เราสามารถยืนยันได้โดยไปที่เครื่องมือ > Kotlin > แสดงไบต์โค้ด Kotlin ในเมนู แล้วคลิกปุ่มแยกโค้ด

String myString = "Logged in as " + user.getDisplayName();

เมื่อต้องการเข้าถึงข้อมูลเหล่านี้จาก Java เราต้องเขียนชื่อของ Getter อย่างชัดเจน

ในกรณีส่วนใหญ่ ชื่อ Java ของ Getter สำหรับพร็อพเพอร์ตี้ Kotlin จะเป็น get + ชื่อพร็อพเพอร์ตี้ ดังที่เราได้เห็นใน User.getHasSystemAccess() และ User.getDisplayName() ข้อยกเว้นอย่างหนึ่งคือพร็อพเพอร์ตี้ที่มีชื่อขึ้นต้นด้วย "is" ในกรณีนี้ ชื่อ Java สำหรับ Getter คือชื่อของพร็อพเพอร์ตี้ Kotlin

เช่น พร็อพเพอร์ตี้ใน User เช่น

val isAdmin get() = //...

จะเข้าถึงได้จาก Java ด้วย

boolean userIsAnAdmin = user.isAdmin();

เมื่อใช้คำอธิบายประกอบ @JvmName Kotlin จะสร้างไบต์โค้ดที่มีชื่อที่ระบุแทนชื่อเริ่มต้นสำหรับรายการที่ใส่คำอธิบายประกอบ

ซึ่งจะทำงานเหมือนกันสำหรับตัวตั้งค่า ซึ่งชื่อที่สร้างขึ้นจะเป็น set + ชื่อพร็อพเพอร์ตี้ เสมอ ตัวอย่างเช่น ลองดูคลาสต่อไปนี้

class Color {
   var red = 0f
   var green = 0f
   var blue = 0f
}

สมมติว่าเราต้องการเปลี่ยนชื่อ Setter จาก setRed() เป็น updateRed() โดยไม่แตะต้อง Getter เราใช้เวอร์ชัน @set:JvmName เพื่อทำสิ่งนี้ได้ดังนี้

class Color {
   @set:JvmName("updateRed")
   var red = 0f
   @set:JvmName("updateGreen")
   var green = 0f
   @set:JvmName("updateBlue")
   var blue = 0f
}

จากนั้นเราจะเขียนจาก Java ได้ดังนี้

color.updateRed(0.8f);

UseCase.formatUser() ใช้การเข้าถึงฟิลด์โดยตรงเพื่อรับค่าของพร็อพเพอร์ตี้ของออบเจ็กต์ User

ใน Kotlin โดยปกติแล้วพร็อพเพอร์ตี้จะแสดงผ่านตัวรับและตัวตั้งค่า ซึ่งรวมถึงพร็อพเพอร์ตี้ val

คุณเปลี่ยนลักษณะการทำงานนี้ได้โดยใช้คำอธิบายประกอบ @JvmField เมื่อใช้กับพร็อพเพอร์ตี้ในคลาส Kotlin จะข้ามการสร้างเมธอด Getter (และ Setter สำหรับพร็อพเพอร์ตี้ var) และสามารถเข้าถึงฟิลด์สำรองได้โดยตรง

เนื่องจากออบเจ็กต์ User เปลี่ยนแปลงไม่ได้ เราจึงต้องการแสดงพร็อพเพอร์ตี้แต่ละรายการเป็นฟิลด์ และจะใส่คำอธิบายประกอบแต่ละรายการด้วย @JvmField

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   @get:JvmName("hasSystemAccess")
   val hasSystemAccess
       get() = "sys" in groups
}

หากย้อนกลับไปดู UseCase.formatUser() ตอนนี้ เราจะเห็นว่าข้อผิดพลาดได้รับการแก้ไขแล้ว

@JvmField หรือ const

นอกจากนี้ ยังมีข้อผิดพลาดที่คล้ายกันอีกรายการหนึ่งในไฟล์ UseCase.java ดังนี้

Repository.saveAs(Repository.BACKUP_PATH);

หากเราใช้การเติมข้อความอัตโนมัติที่นี่ เราจะเห็นว่ามี Repository.getBACKUP_PATH() และอาจอยากเปลี่ยนคำอธิบายประกอบใน BACKUP_PATH จาก @JvmStatic เป็น @JvmField

มาลองกันเลย เปลี่ยนกลับไปใช้ Repository.kt แล้วอัปเดตคำอธิบายประกอบโดยทำดังนี้

object Repository {
   @JvmField
   val BACKUP_PATH = "/backup/user.repo"

หากดูที่ UseCase.java ตอนนี้ เราจะเห็นว่าข้อผิดพลาดหายไปแล้ว แต่ก็มีหมายเหตุใน BACKUP_PATH ด้วย

ใน Kotlin มีเพียงประเภทที่สามารถเป็น const เท่านั้นที่เป็นประเภทดั้งเดิม เช่น int, float และ String ในกรณีนี้ เนื่องจาก BACKUP_PATH เป็นสตริง เราจึงได้รับประสิทธิภาพที่ดีขึ้นโดยใช้ const val แทน val ที่มีคำอธิบายประกอบด้วย @JvmField ในขณะที่ยังคงเข้าถึงค่าเป็นฟิลด์ได้

มาเปลี่ยนใน Repository.kt กันเลย

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

หากย้อนกลับไปดูที่ UseCase.java เราจะเห็นว่าเหลือข้อผิดพลาดเพียงรายการเดียว

ข้อผิดพลาดสุดท้ายระบุว่า Exception: 'java.io.IOException' is never thrown in the corresponding try block.

หากดูโค้ดสำหรับ Repository.saveAs ใน Repository.kt เราจะเห็นว่าโค้ดดังกล่าวจะส่งข้อยกเว้น เกิดอะไรขึ้น

Java มีแนวคิดของ "Checked Exception" ซึ่งเป็นข้อยกเว้นที่อาจกู้คืนได้ เช่น ผู้ใช้พิมพ์ชื่อไฟล์ผิด หรือเครือข่ายไม่พร้อมใช้งานชั่วคราว หลังจากตรวจพบข้อยกเว้นที่ต้องตรวจสอบแล้ว นักพัฒนาแอปจะแสดงความคิดเห็นต่อผู้ใช้เกี่ยวกับวิธีแก้ไขปัญหาได้

เนื่องจากระบบจะตรวจสอบข้อยกเว้นที่ต้องตรวจสอบในเวลาคอมไพล์ คุณจึงต้องประกาศข้อยกเว้นในลายเซ็นของเมธอด

public void openFile(File file) throws FileNotFoundException {
   // ...
}

ในทางกลับกัน Kotlin ไม่มีข้อยกเว้นที่ต้องตรวจสอบ ซึ่งเป็นสาเหตุของปัญหาในที่นี้

วิธีแก้ปัญหาคือขอให้ Kotlin เพิ่ม IOException ที่อาจเกิดขึ้นกับลายเซ็นของ Repository.saveAs() เพื่อให้ไบต์โค้ด JVM มีข้อยกเว้นที่ตรวจสอบแล้ว

เราทำเช่นนี้ด้วยคำอธิบายประกอบ @Throws ของ Kotlin ซึ่งช่วยให้การทำงานร่วมกันของ Java/Kotlin เป็นไปได้ ใน Kotlin ข้อยกเว้นจะทำงานคล้ายกับ Java แต่ Kotlin มีเฉพาะข้อยกเว้นที่ไม่ได้ตรวจสอบ ซึ่งแตกต่างจาก Java ดังนั้น หากต้องการแจ้งโค้ด Java ว่าฟังก์ชัน Kotlin จะส่งข้อยกเว้น คุณต้องใช้คำอธิบายประกอบ @Throws กับลายเซ็นฟังก์ชัน Kotlin ให้เปลี่ยนไปใช้ Repository.kt file และอัปเดต saveAs() เพื่อรวมคำอธิบายประกอบใหม่

@JvmStatic
@Throws(IOException::class)
fun saveAs(path: String?) {
   val outputFile = File(path)
   if (!outputFile.canWrite()) {
       throw FileNotFoundException("Could not write to file: $path")
   }
   // Write data...
}

เมื่อมีคำอธิบายประกอบ @Throws เราจะเห็นว่าข้อผิดพลาดของคอมไพเลอร์ทั้งหมดใน UseCase.java ได้รับการแก้ไขแล้ว ไชโย

คุณอาจสงสัยว่าต้องใช้บล็อก try และ catch เมื่อเรียกใช้ saveAs() จาก Kotlin หรือไม่

ไม่ โปรดทราบว่า Kotlin ไม่มีข้อยกเว้นที่ต้องตรวจสอบ และการเพิ่ม @Throws ลงในเมธอดไม่ได้เปลี่ยนแปลงสิ่งนี้

fun saveFromKotlin(path: String) {
   Repository.saveAs(path)
}

การดักจับข้อยกเว้นยังคงมีประโยชน์เมื่อจัดการได้ แต่ Kotlin ไม่ได้บังคับให้คุณจัดการ

ใน Codelab นี้ เราได้กล่าวถึงข้อมูลพื้นฐานเกี่ยวกับวิธีเขียนโค้ด Kotlin ที่รองรับการเขียนโค้ด Java ที่เป็นสำนวนด้วย

เราได้พูดคุยกันถึงวิธีใช้คำอธิบายประกอบเพื่อเปลี่ยนวิธีที่ Kotlin สร้างไบต์โค้ด JVM เช่น

  • @JvmStatic เพื่อสร้างสมาชิกและเมธอดแบบคงที่
  • @JvmOverloads เพื่อสร้างเมธอดที่โอเวอร์โหลดสำหรับฟังก์ชันที่มีค่าเริ่มต้น
  • @JvmName เพื่อเปลี่ยนชื่อของ Getter และ Setter
  • @JvmField เพื่อแสดงพร็อพเพอร์ตี้เป็นฟิลด์โดยตรง แทนที่จะผ่านตัวรับและตัวตั้งค่า
  • @Throws เพื่อประกาศข้อยกเว้นที่ตรวจสอบแล้ว

เนื้อหาสุดท้ายของไฟล์มีดังนี้

User.kt

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   val hasSystemAccess
       @JvmName("hasSystemAccess")
       get() = "sys" in groups
}

Repository.kt

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   @Throws(IOException::class)
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

StringUtils.kt

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

fun String.toTitleCase(): String {
   if (isNullOrBlank()) {
       return this
   }

   return split(" ").map { word ->
       word.foldIndexed("") { index, working, char ->
           val nextChar = if (index == 0) char.toUpperCase() else char.toLowerCase()
           "$working$nextChar"
       }
   }.reduceIndexed { index, working, word ->
       if (index > 0) "$working $word" else word
   }
}

fun String.nameToLogin(): String {
   if (isNullOrBlank()) {
       return this
   }
   var working = ""
   toCharArray().forEach { char ->
       if (char.isLetterOrDigit()) {
           working += char.toLowerCase()
       } else if (char.isWhitespace() and !working.endsWith(".")) {
           working += "."
       }
   }
   return working
}