+1

Writing Java-friendly Kotlin code (Phần 1)

Writing Java-friendly Kotlin code Trong khi Kotlin ngày càng trở nên phổ biến, rất nhiều thư viện Java đang được support đến Kotlin để sử dụng chúng trong Kotlin nhiều hơn, sạch hơn. Mọi người, những ai đã sử dụng Kotlin, hiểu rằng ngôn ngữ này là dễ chịu hơn nhiều để viết hơn là Java. Vì vậy, sẽ không phải tốt nếu phải viết lại thư viện cho Kotlin từ đầu ? Sử dụng tất cả các tính năng của nó, trong khi vẫn giữ được sự thân thiện cửa java ở phía người gọi? Điều này là có thể và trong bài này tôi sẽ cung cấp cho bạn một số lời khuyên về làm thế nào để đạt được nó. Một trong những yếu tố chính của Kotlin là khả năng tương tác tuyệt vời của Java. Và nó thực sự dễ dàng để gọi mã Java từ Kotlin, tuy nhiên, ngược lại có những sai lầm của nó. Tuy nhiên thì vẫn cần có sự support từ những người tạo ra Kotlin. Trong bài viết này chủ yếu nói đến JVM backend của Kotlin.

  • Statics, @JvmStatic
    • Chúng ta hãy cùng xem xét thư viện Analytics. Chúng tôi đã thiết kế điểm tương tác chính như một đối tượng:
    object Analytics {
  fun init() {...}
  fun send(event: Event) {...}
  fun close() {...}
}

Trên Kotlin call-site, nhìn thật tuyệt vời, đơn giản chỉ là:

Analytics.send(Event("custom_event"))

Nhưng từ Java chúng ta phải làm:

Analytics.INSTANCE.send(new Event("custom_event"));

Chúng ta có thể làm cho mã Java giống với Kotlin không? Vâng, chúng tôi có thể. Đơn giản chỉ cần đánh dấu tất cả các public method của chúng tôi như @ JvmStatic và chúng sẽ được biên dịch như các static Java methods:

object Analytics {
  @JvmStatic fun init() {...}
  @JvmStatic fun send(event: Event) {...}
  @JvmStatic fun close() {...}
}
...
// Java call-site
Analytics.send(new Event("custom_event"));

Nếu chúng ta có bất kỳ thuộc tính public nào trong đối tượng, chúng cũng nên đánh dấu @JvmStatic - trong khi các trường sao lưu là mặc định, chúng ta vẫn cần phải sửa đổi các getter và setters:

@JvmStatic var isInited: Boolean = false
private set
  • Default parameters, @JvmOverloads
    • Cùng lúc đó, lớp Event của chúng ta sẽ như sau:
    data class Event(val name: String, val context: Map<String, Any> = 
    emptyMap())

Chúng tôi thiết lập một giá trị mặc định cho context vì chúng ta muốn có thể tạo ra một Marker Event mà không có bất kỳ dữ liệu nào. Getters và setters với tên thích hợp sẽ được tạo thuận tiện cho chúng tôi. Và chúng ta có thể mong đợi hai constructor được tạo ra: một cái là name và cái khác là name và context. Thật vậy, hai constructor sẽ được tạo ra, nhưng không phải là điều chúng tôi mong đợi. Một sẽ là một constructor đầy đủ và một sẽ là một constructor tổng hợp đầy đủ với một tham số cờ bit bổ sung để cấu hình, cho dù các tham số khác có giá trị mặc định hay không, được đặt hay không. May mắn thay, vế sau sẽ không thể truy cập từ Java. Tất nhiên, chúng ta có thể sử dụng một constructor đầy đủ ở khắp mọi nơi trong Java, nhưng chúng ta không thể làm tốt hơn? Chắc chắn! Chúng ta có thể tạo ra các constructor overloading bằng cách chú thích hàm khởi tạo cơ bản với @JvmOverloads:

data class Event @JvmOverloads constructor(val name: String, val context: Map<String, Any> = emptyMap())

Điều này sẽ tạo ra hai constructor chúng tôi đã mong đợi ban đầu. Chúng ta cũng có thể sử dụng thủ thuật này cho các hàm với các tham số mặc định có thể được gọi từ Java.

  • Checked exceptions, @Throws Tại một số thời điểm, chúng tôi quyết định thêm khả năng sử dụng plugin với Analytics của chúng tôi.
  interface Plugin {
  fun init()
  /** @throws IOException if sending failed */
  fun send(event: Event)
  fun close()
}

Chúng tôi thiết kế nó theo cách mà tất cả các plugin được xử lý ở một nơi, và gửi có thể ném một IOException, nó sẽ được xử lý trong lớp thư viện chính và logged:

// object Analytics
@JvmStatic fun send(event: Event) {
  log("<Internal Send event>")

  plugins.forEach { 
    try {
      it.send(event)
    } catch (e: IOException) {
      log("WARN: ${it.javaClass.simpleName} fired IOE")
    } 
  }
}

Không cần phải thiết lập bất kỳ ngoại lệ ném trong một method Kotlin. Nhưng ở đây Java - một trong trong số các thư viện của chúng tôi quyết định viết plugin của riêng mình trong Java, ném đúng IOException khi nó cần thiết, thêm phần ném vào chữ ký và thấy một lỗi biên dịch:

Overridden method does not throw IOException

Chúng tôi không muốn làm cho người sử dụng của chúng tôi không hài lòng. Đáng buồn thay, Java vẫn kiểm tra các trường hợp ngoại lệ và chúng phải được ghi trong một chữ ký của method. Đối với trường hợp cụ thể đó, Kotlin đã @Throws. Đánh dấu phương pháp của chúng tôi với chú thích này và bao gồm tất cả các lớp ngoại lệ ném ra các tham số của nó để giải quyết vấn đề:

interface Plugin {
  fun init()
  /** @throws IOException if sending failed */
  @Throws(IOException::class)
  fun send(event: Event)
  fun close()
}
  • Java names, @JvmName
    • Properties Bây giờ chúng tôi đã bổ sung và chúng tôi muốn có một khả năng để kiểm tra nếu có được cài đặt. Hãy thêm một thuộc tính như sau:
    
    val hasPlugins get() = plugins.isNotEmpty()
    

Chúng ta biết rằng Kotlin tôn trọng công ước đặt tên Bean Java, vì vậy bất cứ thuộc tính nào được gọi là name sẽ chuyển thành cặp phương thức getName () / setName () (đối với trường hợp var). Ngoài ra chúng ta biết rằng nếu thuộc tính được gọi là isName nó sẽ chuyển thành isName () / setName () cặp. Nó thực sự tuyệt vời, điều này giúp chúng tôi ở phía người gọi Java! Nhưng còn về hasPlugins? Rõ ràng, nó sẽ trở thành getHasPlugins () phương pháp đó là không hay. Sẽ tốt hơn khi gọi là hasPlugins ()? Một lần nữa, Kotlin có một giải pháp cho chú thích này - @JvmName, và vì chúng ta cần phải đổi tên getter, chú thích này nên được áp dụng cho chính nó:

val hasPlugins @JvmName("hasPlugins") get() = plugins.isNotEmpty()

Hoặc cách khác (thêm chú thích):

@get:JvmName("hasPlugins") val hasPlugins get() = 
  plugins.isNotEmpty()

Nhưng đây không phải là trường hợp duy nhất của @JvmName.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí