使用 Kotlin Bytecode 學習、Debug

Andy Lu
6 min readMar 14, 2023
Image by MustangJoe from Pixabay

Kotlin 最重要的特點之一:能 100% 與 Java 交互使用,所有在 Kotlin 內所寫的程式碼都能夠轉成 Kotlin Bytecode,接著再將 Bytecode 反組譯(Decompile)成 Java Code。透過這樣子的動作,我們就可以將 Kotlin 的程式碼以 Java 來呈現,如果你對於 Java 比較熟悉一點,這樣的方法對於了解、學習 Kotlin 有很大的幫助。

Kotlin 內部有許多語法糖,在 Java 端需要花好幾行完成的程式碼,在 Kotlin 甚至只要一行就能夠寫完了,對於一開始從 Java 轉入 Kotlin 的開發者來說,或許會感覺跳太快而感到無所適從,筆者是從 Java 開始學習的,一開始學習 Kotlin 的語法糖時,同樣也感到驚訝,並好奇使用 Java 會是怎麼實作的,同樣的,我們也可以使用將 Kotlin 程式碼轉換成 Kotlin Bytecode,再將 Kotlin Bytecode 反組譯成 Java Code。如此就能將不熟悉的程式碼轉成 Java 之後,大概就能夠了解它的概念。

將 Kotlin 程式碼透過 Kotlin Bytecode 轉換成 Java 程式碼,除了可以方便原 Java 開發者學習 Kotlin 之外,它還是一項 Debug 的利器,因為 Kotlin 內的語法糖可能會自動產生一些 Boilerplate Code,並將其隱藏起來,所以在 Kotlin 開發時,就不需要花費很多時間去寫這些繁雜的內容。但是既然是自動產生的項目,難免會出現錯誤,假設我們無法從編譯後的錯誤訊息查出問題所在,或許將 Kotlin 程式碼轉換成 Java 程式碼也是一種不錯的選擇。(因為這些 Boilerplate Code 沒有被隱藏起來,所以我們可以清楚的知道是那邊發生問題。

用 Kotlin Bytecode Debug

前幾天在社群內有一個問題:在一個資料類別中,有兩個屬性名稱類似,當這兩個屬性都存在的時候,一開始會編譯失敗,有時候又可以。

按照這個描述,我們嘗試建立一個類別 `MayBeConclict`,而在這個類別中,有兩個型別為 Boolean 的屬性(isLike、like),並且將這兩個屬性宣告成 `var` (指定數值後,還可以被修改)


data class MayBeConflict(
var isLike: Int,

var like: Int
)

經過編譯之後,果然發生了錯誤(Platform declaration clash)

e: /Users/andylu/development/IntelliJProject/kotlin_serialization/src/main/kotlin/MayBeConflict.kt: (6, 5): Platform declaration clash: The following declarations have the same JVM signature (setLike(I)V):
fun <set-isLike>(<set-?>: Int): Unit defined in com.futuredial.functiontest.model.MayBeConflict
fun <set-like>(<set-?>: Int): Unit defined in com.futuredial.functiontest.model.MayBeConflict

如果將這段程式碼轉換成 Java 程式碼來看看,會出現什麼樣的內容呢?
我們透過 IDE 的工具(Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile),將其轉換成 Java 程式碼。如下:

public final class MayBeConflict { 
private int isLike;
private int like;

public final int isLike() {
return this.isLike;
}

public final void setLike(int var1) { //error
this.isLike = var1;
}

public final int getLike() {
return this.like;
}

public final void setLike(int var1) { //error
this.isLike = var1;
}

public MayBeConflict(int isLike, int like) {
this.isLike = isLike;
this.like = like;
}

而在這段 Java 程式碼中,我們可以發現錯誤的記號標記在兩個相同的函式 *setLike* 。那麼為什麼會出現兩個 `setLike` 函式呢?我們分別將兩個屬性註解掉,並產生出其 Java 程式碼。

var isLike: Int

data class MayBeConflict( 
var isLike: Int,

// var like: Int
)

轉換成 Java 程式碼後:


public final class MayBeConflict {
private int isLike;

public final int isLike() {
return this.isLike;
}

public final void setLike(int var1) {
this.isLike = var1;

var like: Int

data class MayBeConflict( 
// var isLike: Int,

var like: Int
)

轉換成 Java 程式碼後:

public final class MayBeConflict { 
private int like;

public final int getLike() {
return this.like;
}

public final void setLike(int var1) {
this.like = var1;
}

我們可以分別從這兩段 Java 程式碼找出它們的規則:若屬性使用 var 宣告,表示這個屬性是可以被修改的,所以會自動建立一個 setter 函式,而這個 setter 函式的命名方式會是在屬性名稱的前面加上 set 前綴字,如果屬性是 `is` 開頭的,那麼就會自動將 is 刪除,並加上 set 前綴字。

所以當這兩個屬性都同時存在時,Kotlin 並沒有取另外的名稱,而是直接使用原本的邏輯,所以也就會造成命名衝突的情況。

解決方法

如果我們將 var 改成使用 val,因為 val 是不可以被修改的,所以 Kotlin 就不會自動產生其 setter 函式,所以也就不會發生命名衝突的現象。

當然,這邊假如一定需要使用 var 宣告,那解決的方式就只剩下一項,那就是改名。

結論

透過 Kotlin Bytecode 能夠將程式碼轉換成 Java 程式碼,而透過 Java 程式碼我們可以將隱藏的部分顯露出來,如此我們就能夠輕鬆的學習、Debug。

--

--

Andy Lu

Android/Flutter developer, Kotlin Expert, like to learn and share.