在這個程式碼研究室中,您將學習如何編寫或調整 Kotlin 程式碼,讓這些程式碼能透過 Java 程式碼順利呼叫。
您將會瞭解的內容
- 如何使用
@JvmField
、@JvmStatic
和其他註解。 - 從 Java 程式碼存取特定 Kotlin 語言功能的限制。
注意事項
這個程式碼研究室是專為程式設計人員編寫,並具備基本的 Java 和 Kotlin 知識。
這個程式碼研究室會模擬遷移採用 Java 程式設計語言撰寫的大型專案,以納入新的 Kotlin 程式碼。
為了簡化作業,我們會建立一個名為 UseCase.java
的單一 .java
檔案,代表現有的程式碼集。
我們以 Kotlin 編寫的新版本來重新編寫了原本以 Java 編寫的某些功能,而必須整合所有功能。
匯入專案
您可以從這裡的 GitHub 專案複製專案的程式碼:GitHub
或者,您也可以從這裡的 ZIP 封存檔下載及擷取專案:
如果您使用的是 IntelliJ IDEA,請選取 [匯入專案]。
如果您使用的是 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(...)
的錯誤訊息都相同:「&tt 無法從靜態內容存取。」
現在讓我們來看看其中一個 Kotlin 檔案。開啟檔案 Repository.kt
。
我們發現,我們使用物件關鍵字來宣告我們的存放區。這個問題的原因是 Kotlin 是在我們的類別中產生靜態執行個體,而不是以靜態屬性和方法的形式來公開。
舉例來說,您可以透過 Repository.INSTANCE.getNextGuestId()
參照 Repository.getNextGuestId()
,不過這樣是更妥善的方式。
我們可透過 Kotlin 產生靜態方法和屬性,方法是使用 @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)
}
}
使用 IDE 將 @JvmStatic 註解新增至您的程式碼。
如果我們切換回 UseCase.java
,Repository
的屬性和方法不會再造成錯誤 (Repository.BACKUP_PATH
除外)。我們之後會再回來看看。
現在,我們來修正 registerGuest()
方法中的下一個錯誤。
假設以下情境:我們有一個 StringUtils
類別,其中包含多個字串函式,用於字串作業。它轉換成 Kotlin 時,我們將這些方法轉換成擴充函式。Java 沒有擴充功能函式,因此 Kotlin 會將這些方法編譯為靜態函式。
但很抱歉,如果我們參考 UseCase.java
中的 registerGuest()
方法,就會發現這樣的結果不太準確。
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
我們可以使用 @file:JvmName
註解,讓 Kotlin 針對套件層級方法使用不同的名稱。我們使用這個註解為類別 StringUtils
命名。
@file:JvmName("StringUtils")
package com.google.example.javafriendlykotlin
現在,我們回頭查看 UseCase.java
後,發現「StringUtils.nameToLogin()
」的錯誤已經解決。
很抱歉,這項錯誤已由新傳送至 User
建構函式的參數取代。請繼續進行下一個步驟,並在 UseCase.registerGuest()
內修正最後一項錯誤。
Kotlin 支援參數的預設值。我們來看看 Repository.kt
的 init
區塊內,瞭解它們的使用方式。
Repository.kt:
_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
可以看到,針對使用者「暖」,我們可以跳過 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 程式設計語言不支援預設值。如要修正這個問題,請運用 @JvmOverloads 註解,要求 Kotlin 為建構函式產生超載。
首先,我們必須稍微更新 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
函式中沒有其他錯誤!
下一步是修正「UseCase.getSystemUsers()
」中 user.hasSystemAccess()
的無效通話。請繼續下一步,或是繼續閱讀說明,瞭解 @JvmOverloads
如何修正錯誤。
@JvmOverloads
如要進一步瞭解 @JvmOverloads
的用途,讓我們在 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);
}
我們可以只使用 id
和 username
這兩個參數來建構 User
:
User syrinx = new User(1001, "syrinx");
我們還可以建構 User
,方法是加入 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 會針對函式 (包括建構函式) 產生超載,但只會為各參數使用一個預設值建立一個超載。
讓我們回顧一下「UseCase.java
」並解決下一個問題:透過「UseCase.getSystemUsers()
」方法呼叫 user.hasSystemAccess()
:
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;
}
這是個有趣的錯誤!如果您在 User
類別中使用 IDE' 的自動完成功能,您會發現 hasSystemAccess()
已重新命名為 getHasSystemAccess()
。
為解決這個問題,我們想讓 Kotlin 針對 val
屬性 hasSystemAccess
產生不同的名稱。如要執行此動作,請使用 @JvmName
註解。回到 User.kt
看看該從哪裡套用。
註解的套用方式有兩種。首先是直接套用到 get()
方法,如下所示:
val hasSystemAccess
@JvmName("hasSystemAccess")
get() = "sys" in groups
如此可讓 Kotlin 將明確定義的 getter 簽章變更為您提供的名稱。
或者,您也可以使用以下的 get:
前置字元,將其套用至資源:
@get:JvmName("hasSystemAccess")
val hasSystemAccess
get() = "sys" in groups
替代方法特別適用於使用隱含定義預設 getter 的屬性。例如:
@get:JvmName("isActive")
val active: Boolean
這樣一來,不需要明確定義 getter,就能變更 getter 的名稱。
儘管如此,您還是可以根據自己的需求進行調整。兩者都會讓 Kotlin 建立名稱為 hasSystemAccess()
的 getter。
如果我們切換回 UseCase.java
,即可確認「getSystemUsers()
」現在是無錯誤!
下一個錯誤為 formatUser()
,但如果您想進一步瞭解 Kotlin getter 命名慣例,請繼續閱讀本文,再繼續進行下一個步驟。
getter 和 setter 命名
我們編寫 Kotlin 程式碼時,很容易忘記這些撰寫程式碼,例如:
val myString = "Logged in as ${user.displayName}")
實際上是呼叫函式來取得 displayName
的值。如要驗證這一點,請前往選單中的 [工具 > Kotlin > [顯示 Kotlin Bytecode],然後按一下 [解譯] 按鈕:
String myString = "Logged in as " + user.getDisplayName();
當我們想要從 Java 存取這些項目時,就必須明確寫入 getter 的名稱。
在大多數情況下,Kotlin 屬性的 getter 的 Java 名稱只是 get
+ 屬性名稱,如同我們在 User.getHasSystemAccess()
和 User.getDisplayName()
中看到的名稱。唯一的例外是名稱以「is」開頭的屬性。在這種情況下,getter 的 Java 名稱是 Kotlin 屬性的名稱。
例如,使用 User
的屬性,例如:
val isAdmin get() = //...
您可以透過 Java 存取下列應用程式:
boolean userIsAnAdmin = user.isAdmin();
透過 @JvmName
註解,Kotlin 會為已加註的項目產生具有指定名稱的位元組碼,而非預設的註解。
這個做法與設定者相同,其產生的名稱一律為 set
+ 屬性名稱。例如,請執行下列類別:
class Color {
var red = 0f
var green = 0f
var blue = 0f
}
我們假設你想將成名者名稱從「setRed()
」改成「updateRed()
」,但只保留客戶開發者名稱。我們可以使用 @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 中,屬性通常會透過 getter 和 setter 公開。包括 val
資源。
您可以使用 @JvmField
註解變更這項行為。如果對類別中的屬性套用此設定,Kotlin 將略過產生 getter (和 var
屬性的 setter) 方法,並可直接存取支援欄位。
由於 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
註解可提升效能,同時保留以欄位形式存取該值的功能。
讓我們在 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.kt
中發現 Repository.saveAs
的程式碼,發現該程式碼確實符合例外狀況。這是怎麼回事?
Java 的概念為「檢查例外狀況」。以下列舉幾種可能可以還原的例外情況,例如使用者輸入錯誤的檔案名稱,或是網路暫時無法使用。系統完成檢查後,開發人員可為使用者提供意見回饋,並提供修正問題的方法。
由於已勾選例外狀況會在編譯時間檢查,因此您必須在方法的簽名中宣告這些例外狀況:
public void openFile(File file) throws FileNotFoundException {
// ...
}
而 Kotlin 並沒有檢查例外狀況,這就是問題發生的原因。
解決方法是要求 Kotlin 將可能導入至 Repository.saveAs()
簽名的 IOException
,讓 JVM 位元組程式碼納入檢查中做為檢查的例外情況。
我們會透過 Kotlin @Throws
註解執行此作業,因為該註解有助於 Java/Kotlin 互通性。在 Kotlin 中,例外情況與 Java 類似,但 Java 與 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
中的所有編譯器錯誤皆已修正!訂閱成功!
你可能想知道現在透過 Kotlin 呼叫 saveAs()
時,是否需要使用 try
和 catch
區塊。
不對!請記住,Kotlin 並沒有檢查例外狀況,將 @Throws
新增至方法並不會改變:
fun saveFromKotlin(path: String) {
Repository.saveAs(path)
}
即使能夠處理例外情況,還是能找出例外情況,但 Kotlin 不會強制您處理這些錯誤。
在本程式碼研究室中,我們說明瞭撰寫 Kotlin 程式碼的基本知識,這些程式碼同樣也支援編寫 Java 程式碼。
我們討論如何使用註解來變更 Kotlin 產生 JVM 位元組碼的方式,例如:
@JvmStatic
:產生靜態成員和方法。@JvmOverloads
:為具有預設值的函式產生超載的方法。@JvmName
:變更 getter 和 setter 的名稱。@JvmField
會以欄位的形式公開屬性,而不是透過 getter 和 setter 提供。@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
}