Gọi mã Kotlin từ Java

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách viết hoặc điều chỉnh mã Kotlin để dễ dàng gọi được hơn từ mã Java.

Kiến thức bạn sẽ học được

  • Cách tận dụng @JvmField, @JvmStatic và chú thích khác.
  • Các hạn chế khi truy cập vào các tính năng ngôn ngữ nhất định của Kotlin từ mã Java.

Những điều bạn cần biết

Lớp học lập trình này dành cho các lập trình viên và giả định rằng bạn đã có kiến thức cơ bản về Java và Kotlin.

Lớp học lập trình này mô phỏng quá trình di chuyển một phần của dự án lớn hơn viết bằng ngôn ngữ lập trình Java, để kết hợp mã Kotlin mới.

Để đơn giản hóa mọi thứ, chúng ta sẽ có một tệp .java duy nhất có tên là UseCase.java. Tệp này sẽ đại diện cho cơ sở mã hiện có.

Chúng ta sẽ tưởng tượng rằng mình vừa thay thế một số chức năng được viết ban đầu bằng Java bằng một phiên bản mới được viết bằng Kotlin và chúng ta cần hoàn tất việc tích hợp chức năng đó.

Nhập dự án

Mã dự án có thể được sao chép từ dự án GitHub tại đây: GitHub

Ngoài ra, bạn có thể tải xuống và trích xuất dự án từ một tệp lưu trữ zip có tại đây:

Tải tệp zip xuống

Nếu bạn đang sử dụng IntelliJ IDEA, hãy chọn " Import Project".

Nếu bạn đang sử dụng Android Studio, hãy chọn "Nhập dự án (Gradle, Eclipse ADT, v.v.)"

Hãy mở UseCase.java và bắt đầu khắc phục các lỗi mà chúng tôi phát hiện thấy.

Hàm đầu tiên gặp vấn đề là registerGuest:

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

Lỗi cho cả Repository.getNextGuestId()Repository.addUser(...) đều giống nhau: " Không tĩnh được truy cập từ ngữ cảnh tĩnh."

Bây giờ, hãy xem một trong các tệp Kotlin. Mở tệp Repository.kt.

Chúng tôi thấy rằng Kho lưu trữ của chúng tôi là một singleton được khai báo bằng cách sử dụng từ khóa đối tượng. Vấn đề là Kotlin đang tạo một thực thể tĩnh bên trong lớp của chúng ta, thay vì tiết lộ chúng dưới dạng thuộc tính tĩnh và phương thức.

Ví dụ: Repository.getNextGuestId() có thể được tham chiếu bằng cách sử dụng Repository.INSTANCE.getNextGuestId(), nhưng có cách tốt hơn.

Chúng ta có thể lấy Kotlin để tạo các phương thức và thuộc tính tĩnh bằng cách chú thích các thuộc tính và phương thức của kho lưu trữ bằng @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)
   }
}

Thêm chú thích @JvmStatic vào mã của bạn bằng cách sử dụng IDE.

Nếu chúng ta chuyển về UseCase.java, các thuộc tính và phương thức trên Repository không còn gây ra lỗi nữa, ngoại trừ Repository.BACKUP_PATH. Chúng tôi sẽ quay lại sau.

Bây giờ, hãy sửa lỗi tiếp theo trong phương thức registerGuest().

Hãy xem xét tình huống sau: chúng ta có một lớp StringUtils với một số hàm tĩnh cho các thao tác chuỗi. Khi chuyển đổi sang Kotlin, chúng tôi đã chuyển đổi các phương thức này thành hàm mở rộng. Java không có các hàm mở rộng, vì vậy, Kotlin biên dịch các phương thức này dưới dạng các hàm tĩnh.

Rất tiếc, nếu xem xét phương thức registerGuest() bên trong UseCase.java, chúng tôi có thể thấy rằng có điều gì đó không ổn:

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

Lý do là Kotlin đặt các hàm này "top-level" hoặc cấp gói bên trong một lớp có tên dựa trên tên tệp. Trong trường hợp này, vì tệp có tên là StringUtils.kt, nên lớp tương ứng có tên là StringUtilsKt.

Chúng tôi có thể thay đổi tất cả các tệp tham chiếu StringUtils thành StringUtilsKt và sửa lỗi này, nhưng cách này không lý tưởng vì:

  • Có thể có nhiều vị trí trong mã của chúng tôi sẽ cần được cập nhật.
  • Tên này thật khó hiểu.

Vì vậy, thay vì tái cấu trúc mã Java, hãy cập nhật mã Kotlin để sử dụng một tên khác cho các phương thức này.

Mở StringUtils.Kt và tìm khai báo gói sau:

package com.google.example.javafriendlykotlin

Chúng tôi có thể yêu cầu Kotlin sử dụng một tên khác cho các phương thức ở cấp gói bằng cách dùng chú thích @file:JvmName. Hãy dùng chú thích này để đặt tên cho lớp StringUtils.

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

Bây giờ, nếu xem lại UseCase.java, chúng ta có thể thấy lỗi cho StringUtils.nameToLogin() đã được giải quyết.

Rất tiếc, lỗi này đã được thay thế bằng lỗi mới về các thông số được chuyển vào hàm dựng cho User. Hãy tiếp tục sang bước tiếp theo và khắc phục lỗi cuối cùng này trong UseCase.registerGuest().

Kotlin hỗ trợ các giá trị mặc định cho các tham số. Chúng ta có thể xem cách họ sử dụng bằng cách xem bên trong khối init của Repository.kt.

Repository.kt:

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

Chúng ta có thể thấy rằng đối với người dùng "warlow", chúng ta có thể bỏ qua việc nhập giá trị cho displayName vì có giá trị mặc định được chỉ định cho User.kt.

User.kt:

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

Rất tiếc, hàm này không hoạt động theo cách tương tự khi gọi phương thức từ Java.

UseCase.java:

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

Giá trị mặc định không được hỗ trợ trong ngôn ngữ lập trình Java. Để khắc phục vấn đề này, hãy yêu cầu Kotlin tạo tình trạng nạp chồng cho hàm dựng bằng cách dùng chú thích @JvmOverloads.

Trước tiên, chúng tôi phải cập nhật một chút đối với User.kt.

Vì lớp User chỉ có một hàm dựng chính duy nhất, và hàm dựng không bao gồm bất kỳ chú thích nào, nên từ khóa constructor đã bị bỏ qua. Bây giờ, chúng tôi muốn chú thích cho từ khóa, tuy nhiên, từ khóa constructor phải được bao gồm:

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

Khi có từ khóa constructor, chúng ta có thể thêm chú thích @JvmOverloads:

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

Nếu chuyển về UseCase.java, chúng ta có thể thấy không có thêm lỗi nào trong hàm registerGuest!

Bước tiếp theo mà chúng ta cần thực hiện là khắc phục sự cố với cuộc gọi bị hỏng ở user.hasSystemAccess() trong UseCase.getSystemUsers(). Hãy tiếp tục thực hiện bước tiếp theo hoặc tiếp tục đọc để tìm hiểu sâu hơn về những việc @JvmOverloads đã làm để khắc phục lỗi.

@JvmOverloads

Để hiểu rõ hơn về chức năng của @JvmOverloads, hãy tạo một phương thức thử nghiệm trong UseCase.java:

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

Chúng ta có thể tạo User chỉ bằng hai thông số: idusername:

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

Chúng ta cũng có thể tạo User bằng cách thêm thông số thứ ba cho displayName trong khi vẫn sử dụng giá trị mặc định cho groups:

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

Tuy nhiên, bạn không thể bỏ qua displayName và chỉ cung cấp giá trị cho groups mà không cần viết thêm mã:

Vì vậy, hãy xóa dòng đó hoặc mở đầu dòng đó bằng "//#39"; để nhận xét ra.

Trong Kotlin, nếu muốn kết hợp các tham số mặc định và không mặc định, chúng ta sẽ phải sử dụng các tham số có tên.

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

Lý do là Kotlin sẽ tạo ra tình trạng nạp chồng cho các hàm, kể cả hàm dựng, nhưng sẽ chỉ tạo một tình trạng quá tải cho mỗi thông số có giá trị mặc định.

Hãy xem lại UseCase.java và giải quyết vấn đề tiếp theo của chúng ta: cuộc gọi đến user.hasSystemAccess() theo phương thức 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;
}

Đây là một lỗi thú vị! Nếu sử dụng tính năng tự động hoàn thành IDE\39;s trên lớp User, bạn sẽ thấy hasSystemAccess() được đổi tên thành getHasSystemAccess().

Để khắc phục sự cố, chúng tôi muốn Kotlin tạo một tên khác cho thuộc tính val hasSystemAccess. Để làm điều này, chúng ta có thể dùng chú thích @JvmName. Hãy quay trở lại User.kt và xem nơi chúng ta nên áp dụng.

Có hai cách để chúng tôi áp dụng chú thích. Cách đầu tiên là áp dụng trực tiếp phương thức này cho phương thức get(), như trong ví dụ sau:

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

Tín hiệu này cho Kotlin biết để thay đổi chữ ký của phương thức getter được xác định rõ ràng thành tên đã cung cấp.

Ngoài ra, bạn có thể áp dụng tính năng đó cho tài sản bằng cách sử dụng tiền tố get: như sau:

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

Phương thức thay thế đặc biệt hữu ích với các thuộc tính đang sử dụng phương thức getter được xác định ngầm định, Ví dụ:

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

Việc này cho phép thay đổi tên của phương thức getter mà không cần phải xác định rõ phương thức getter.

Bất kể sự khác biệt này, bạn có thể sử dụng bất kỳ cách nào phù hợp hơn với mình. Cả hai đều sẽ giúp Kotlin tạo một phương thức getter có tên là hasSystemAccess().

Nếu chuyển về UseCase.java, chúng tôi có thể xác minh rằng getSystemUsers() hiện không có lỗi!

Lỗi tiếp theo là trong formatUser(), nhưng nếu bạn muốn đọc thêm về quy ước đặt tên phương thức getter của Kotlin, hãy tiếp tục đọc ở đây trước khi chuyển sang bước tiếp theo.

Cách đặt tên và đặt tên

Khi viết Kotlin, chúng ta dễ dàng quên mã viết như:

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

Thực sự đang gọi một hàm để nhận giá trị của displayName. Chúng ta có thể xác minh điều này bằng cách chuyển đến Tools > Kotlin > Show Kotlin Bytecode (Trình đơn Bytecode) trong trình đơn rồi nhấp vào nút Decompile (Mô tả):

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

Khi muốn truy cập vào những công cụ này từ Java, chúng ta cần ghi rõ tên của phương thức getter.

Trong hầu hết trường hợp, tên Java của phương thức getter cho các thuộc tính Kotlin chỉ đơn giản là get + tên thuộc tính, như chúng ta đã thấy với User.getHasSystemAccess()User.getDisplayName(). Một ngoại lệ là những thuộc tính có tên bắt đầu bằng "is" Trong trường hợp này, tên Java cho phương thức getter là tên của thuộc tính Kotlin.

Ví dụ: một thuộc tính trên User như:

val isAdmin get() = //...

Sẽ được truy cập từ Java bằng:

boolean userIsAnAdmin = user.isAdmin();

Bằng cách sử dụng chú thích @JvmName, Kotlin sẽ tạo mã byte có tên được chỉ định, thay vì tên mặc định, cho mục được chú thích.

Quy trình này cũng áp dụng cho những phương thức setter có tên đã tạo luôn là set + tên tài sản. Ví dụ: hãy tham gia lớp sau:

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

Hãy tưởng tượng chúng ta muốn thay đổi tên phương thức setter từ setRed() thành updateRed(), trong khi vẫn giữ nguyên phương thức getter. Chúng ta có thể dùng phiên bản @set:JvmName để thực hiện việc này:

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

Từ Java, chúng ta có thể viết:

color.updateRed(0.8f);

UseCase.formatUser() dùng quyền truy cập trực tiếp vào trường để lấy các giá trị của thuộc tính của một đối tượng User.

Trong Kotlin, các thuộc tính thường hiển thị thông qua phương thức getter và setter. Trong đó có val tài sản.

Bạn có thể thay đổi hành vi này bằng cách sử dụng thẻ chú thích @JvmField. Khi thuộc tính này được áp dụng cho một thuộc tính trong một lớp, Kotlin sẽ bỏ qua các phương thức getter (và phương thức setter cho các thuộc tính var) và trường sao lưu có thể được truy cập trực tiếp.

Vì các đối tượng User là không thể thay đổi, nên chúng tôi muốn hiển thị từng thuộc tính của họ dưới dạng các trường và do đó chúng tôi sẽ chú thích từng thuộc tính bằng @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
}

Giờ đây, nếu xem lại UseCase.formatUser(), chúng ta thấy rằng lỗi đã được khắc phục!

@JvmField hoặc const

Do đó, sẽ có một lỗi tìm kiếm tương tự khác trong tệp UseCase.java:

Repository.saveAs(Repository.BACKUP_PATH);

Nếu sử dụng tính năng tự động hoàn thành tại đây, chúng ta có thể thấy rằng có Repository.getBACKUP_PATH(), vì vậy, bạn có thể muốn thay đổi chú thích trên BACKUP_PATH từ @JvmStatic thành @JvmField.

Hãy thử cách này. Chuyển trở lại Repository.kt và cập nhật chú thích:

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

Nếu xem UseCase.java ngay bây giờ, chúng tôi sẽ thấy lỗi đó biến mất, nhưng cũng có ghi chú trên BACKUP_PATH:

Trong Kotlin, các loại duy nhất có thể là const là nguyên thủy, chẳng hạn như int, floatString. Trong trường hợp này, vì BACKUP_PATH là một chuỗi nên chúng ta có thể đạt được hiệu suất tốt hơn bằng cách sử dụng const val thay vì val được chú thích bằng @JvmField, trong khi vẫn có thể truy cập vào giá trị dưới dạng một trường.

Hãy thay đổi điều đó ngay tại Repository.kt:

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

Nếu xem lại UseCase.java, chúng ta thấy chỉ có một lỗi còn lại.

Lỗi cuối cùng cho biết Exception: 'java.io.IOException' is never thrown in the corresponding try block.

Tuy nhiên, nếu xem mã của Repository.saveAs trong Repository.kt, chúng tôi thấy rằng mã sẽ gửi một ngoại lệ. Chuyện gì đang xảy ra?

Java có khái niệm ""đã kiểm tra ngoại lệ". Đây là những trường hợp ngoại lệ có thể khôi phục được, chẳng hạn như người dùng nhập sai tên tệp hoặc mạng tạm thời không sử dụng được. Sau khi phát hiện trường hợp ngoại lệ đã kiểm tra, nhà phát triển có thể cung cấp cho người dùng ý kiến phản hồi về cách khắc phục vấn đề.

Vì các trường hợp ngoại lệ đã chọn được kiểm tra tại thời điểm biên dịch, nên bạn khai báo các trường hợp đó trong chữ ký của phương thức:

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

Mặt khác, Kotlin không kiểm tra các trường hợp ngoại lệ và điều này gây ra vấn đề ở đây.

Giải pháp là yêu cầu Kotlin thêm IOException có khả năng được gửi vào chữ ký của Repository.saveAs(), để mã byte JVM bao gồm mã đó dưới dạng một trường hợp ngoại lệ đã kiểm tra.

Chúng tôi thực hiện việc này bằng chú thích Kotlin @Throws, giúp tương tác với Java/Kotlin. Trong Kotlin, các trường hợp ngoại lệ hoạt động tương tự như Java, nhưng không giống như Java, Kotlin chỉ có các trường hợp ngoại lệ chưa đánh dấu. Vì vậy, nếu muốn thông báo mã Java của bạn rằng một hàm Kotlin gửi một ngoại lệ, bạn cần dùng chú thích @Writes để thêm chữ ký vào hàm Kotlin Chuyển sang Repository.kt file và cập nhật saveAs() để thêm chú thích mới:

@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...
}

Khi có chú thích @Throws, chúng ta có thể thấy rằng tất cả các lỗi về trình biên dịch trong UseCase.java đã được khắc phục! Thật tuyệt!

Bạn có thể muốn biết liệu bạn có phải sử dụng các khối trycatch khi gọi saveAs() từ Kotlin ngay bây giờ hay không.

Không đâu. Hãy nhớ rằng Kotlin không kiểm tra các trường hợp ngoại lệ, và việc thêm @Throws vào một phương thức sẽ không thay đổi:

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

Bạn vẫn nên bắt các ngoại lệ khi có thể xử lý được, nhưng Kotlin không buộc bạn xử lý các ngoại lệ đó.

Trong lớp học lập trình này, chúng ta đã tìm hiểu các khái niệm cơ bản về cách viết mã Kotlin, cũng hỗ trợ việc viết mã Java đặc trưng.

Chúng ta đã nói về cách sử dụng chú thích để thay đổi cách Kotlin tạo mã byte JVM, chẳng hạn như:

  • @JvmStatic để tạo thành viên và phương thức tĩnh.
  • @JvmOverloads để tạo phương thức quá tải cho các hàm có giá trị mặc định.
  • @JvmName để thay đổi tên của phương thức getter và setter.
  • @JvmField để hiển thị một thuộc tính trực tiếp dưới dạng một trường, thay vì thông qua các phương thức getter và setter.
  • @Throws để khai báo các trường hợp ngoại lệ đã chọn.

Nội dung cuối cùng của tệp là:

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

ChuỗiUtils.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
}