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:
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()
và 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ố: id
và username
:
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()
và 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
, float
và String
. 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 try
và catch
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
}