今年五月的 Google I/O 上,Google 正式向全球宣佈 Kotlin-First 這一重要概念,Kotlin 將成爲 Android 開發者的首選語言。java
新語言天然有新特性,還保持 Java 的編程習慣去寫 Kotlin,也不是不行,可是總感受差點意思。android
最近公衆號「谷歌開發者」連載了一個《實用 Kotlin 構建 Android 應用 | Kotlin 遷移指南》的系列文章,就舉例了一些 Kotlin 編碼的小技巧。既然是一種指南性質的文章,天然在「多而廣」的基礎上,有意去省略一些細節,同時舉例的場景,可能還有一些不恰當的地方。shell
這裏我就來補齊這些細節,今天聊聊利用 Kotlin 的方法默認參數的特性,完成相似 Java 的方法重載的效果。徹底解析這個特性的使用方式和原理,以及在使用過程當中的一個深坑。編程
在 Java 中,咱們能夠在同一個類中,定義多個同名的方法,只須要保證每一個方法具備不一樣的參數類型或參數個數,這就是 Java 的方法重載。佈局
class Hello { public static void hello() { System.out.println("Hello, world!"); } public static void hello(String name) { System.out.println("Hello, "+ name +"!"); } public static void hello(String name, int age) { if (age > 0) { System.out.println("Hello, "+ name + "(" +age +")!"); } else { System.out.println("Hello, "+ name +"!"); } } }
在這個例子中,咱們定義了三個同名的 hello()
方法,分別有不一樣的邏輯細節。學習
在 Kotlin 中,由於它支持在同一個方法裏,經過 「?」標出可空參數,以及經過「=」給出參數的默認值。那這三個方法就能夠在 Kotlin 中,被柔和成一個方法。this
object HelloDemo{ fun hello(name: String = "world", age: Int = 0) { if (age > 0) { System.out.println("Hello, ${name}(${age})!"); } else { System.out.println("Hello, ${name}!"); } } }
在 Kotlin 類中調用,和前面 Java 實現的效果是一致的。編碼
HelloDemo.hello() HelloDemo.hello("承香墨影") HelloDemo.hello("承香墨影", 16)
可是這個經過 Kotlin 方法參數默認值的特性申明的方法,在 Java 類中使用時,就有些區別了。由於 HelloDemo 類被聲明爲 object,因此在 Java 中須要使用 INSTANCE
來調用它的方法。spa
HelloDemo.INSTANCE.hello("承香墨影",16);
Kotlin 中調用 hello()
方法很方便,能夠選擇性的忽略參數,可是在 Java 中使用,必須全量的顯式的去作參數賦值。code
這就是使用了參數默認值的方法申明時,分別在 Kotlin 和 Java 中的使用方式,接下來咱們看看原理。
Kotlin 編寫的代碼,之因此能夠在 Java 系的虛擬機中運行,主要是由於它在編譯的過程當中,會被編譯成虛擬機可識別的 Java 字節碼。因此咱們經過兩次轉換的方式(Show Kotlin Bytecode + Decompile),就能夠獲得 Kotlin 生成的對應 Java 代碼了。
public final void hello(@NotNull String name, int age) { Intrinsics.checkParameterIsNotNull(name, "name"); if (age > 0) { System.out.println("Hello, " + name + '(' + age + ")!"); } else { System.out.println("Hello, " + name + '!'); } } // $FF: synthetic method public static void hello$default(HelloDemo var0, String var1, int var2, int var3, Object var4) { if ((var3 & 1) != 0) { var1 = "world"; } if ((var3 & 2) != 0) { var2 = 0; } var0.hello(var1, var2); }
在這裏會生成一個 hello()
方法,同時還會有一個合成方法(synthetic method)hello$default
,用來處理默認參數的問題。在 Kotlin 中調用 hello()
方法,會在編譯期間,有選擇性的自動替換成 hello()
的合成方法去調用。
// Kotlin 調用 HelloDemo.hello() HelloDemo.hello("承香墨影") HelloDemo.hello("承香墨影", 16) // 編譯後的 Java 代碼 HelloDemo.hello$default(HelloDemo.INSTANCE, (String)null, 0, 3, (Object)null); HelloDemo.hello$default(HelloDemo.INSTANCE, "承香墨影", 0, 2, (Object)null); HelloDemo.INSTANCE.hello("承香墨影", 16);
注意看示例的末尾,當使用 hello(name,age)
這個方法重載時,其實與 Java 中的調用,是一致的,這沒什麼好說的。
這就是 Kotlin 方法重載時,使用指定默認參數的方式,省去多個方法重載代碼的原理。
理解原理後,發現它確實減小了咱們編寫的代碼量,可是有沒有場景,是咱們就須要顯式的存在這幾個方法的重載的?天然是有的,例如自定義 View 時。
再回到前面提到的谷歌開發者的《實用 Kotlin 構建 Android 應用 | Kotlin 遷移指南》系列文章中,舉的例子其實很不恰當。
它這裏的例子中,使用了 View 這個詞,而且重載的幾個方法,都是 View 的構造方法,咱們在自定義 View 時,常常會和這三個方法打交道。
可是谷歌工程師在這裏舉的例子,很容易讓人誤會,實際上你若是在自定義 View 時,這麼寫必定是會報錯的。
例如咱們自定義一個 DemoView,它繼承自 EditView。
class DemoView( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : EditText(context, attrs, defStyleAttr) { }
這個自定義的 DemoView,當使用在 XML 佈局中時,雖然編譯不會出錯,可是運行時,你會獲得一個 NoSuchMethodException。
Caused by: java.lang.NoSuchMethodException: <init> [class android.content.Context, interface android.util.AttributeSet]
什麼問題呢?
在 LayoutInflater 建立控件時,找不到 DemoView(Context, AttributeSet)
這個重載方法,因此就報錯了。
這其實很好理解,在前面說到 Kotlin 在使用帶默認值的方法的原理,其實 Kotlin 最終會在編譯後,額外生成一個合成方法
,來處理方法的參數默認值的狀況,它和 Java 的方法重載還不同,用它生成的方法,確實不會存在多個方法的重載。
因此要明白,Kotlin 的方法指定默認參數與 Java 的方法重載,並不等價。只能說它們在某些場景下,特性是相似的。
那麼回到這裏的問題,在自定義 View 或者其餘須要保留 Java 方法重載的場景下,怎麼讓 Kotlin 在編譯時,真實的去生成對應的重載方法?
這裏就須要用到 @JvmOverloads
了。
當 Kotlin 使用了默認值的方法,被增長了 @JvmOverloads
註解後,它的含義就是在編譯時,保持並暴露出該方法的多個重載方法。
其實當咱們自定義 View 時,AS 已經給了咱們充分的提示,它會自動幫咱們生成帶 @JvmOverloads
構造方法。
AS 幫咱們補全的代碼以下:
class DemoView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AppCompatEditText(context, attrs, defStyleAttr) { }
再用「Kotlin Bytecode + Decompile」查看一下編譯後的代碼,來驗證 @JvmOverloads
的效果。
@JvmOverloads public DemoView(@NotNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { Intrinsics.checkParameterIsNotNull(context, "context"); super(context, attrs, defStyleAttr); } // $FF: synthetic method public DemoView(Context var1, AttributeSet var2, int var3, int var4, DefaultConstructorMarker var5) { if ((var4 & 2) != 0) { var2 = (AttributeSet)null; } if ((var4 & 4) != 0) { var3 = 0; } this(var1, var2, var3); } @JvmOverloads public DemoView(@NotNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0, 4, (DefaultConstructorMarker)null); } @JvmOverloads public DemoView(@NotNull Context context) { this(context, (AttributeSet)null, 0, 6, (DefaultConstructorMarker)null); }
能夠看到,@JvmOverloads
生效後,會按照咱們的預期生成對應的重載方法,同時保留合成方法,完成在 Kotlin 中使用時,使用默認參數的需求。
是否是覺得到這裏就完了?並非,若是你在自定義 View 時,徹底按照 AS 給你的提示生成代碼,雖然程序不會崩潰了,但你會獲得一些未知的錯誤。
在自定義 View 時,依賴 AS 的提示生成代碼,會遇到一些未知的錯誤。例如在本文的例子中,咱們想要實現一個 EditView 的子類,用 AS 提示生成了代碼。
會出現什麼問題呢?
在 EditView 的場景下,你會發現焦點沒有了,點擊以後軟鍵盤也不會自動彈出。
那爲何會出現這種問題?
緣由就在 AS 在自動生成的代碼時,對參數默認值的處理。
當在自定義 View 時,經過 AS 生成重載方法時,它對參數默認值的處理規則是這樣的。
而在這裏的場景下, defStyleAttr
這個參數的類型爲 Int,因此默認值會被賦值爲 0,可是它並非咱們須要的。
在 Android 中,當 View 經過 XML 文件來佈局使用時,會調用兩個參數的構造方法 (Context context, AttributeSet attrs)
,而它內部會調用三個參數的構造方法,並傳遞一個默認的 defStyleAttr
,注意它並非 0。
既然找到了問題,就很好解決了。咱們看看自定義 View 的父類中,兩個參數的構造方法如何實現的,將 defStyleArrt
當默認值傳遞進去就行了。
那咱們先看看 AppCompatEditText
中的實現。
public AppCompatEditText(Context context, AttributeSet attrs) { this(context, attrs, R.attr.editTextStyle); }
再修改 DemoView 中對 defStyleAttr
默認值的指定便可。
class DemoView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.editTextStyle ) : AppCompatEditText(context, attrs, defStyleAttr) { }
到這裏,自定義 View 中,使用默認參數的構造方法重載問題,也解決了。
在自定義 View 的場景下,固然也能夠經過重寫多個 constructor
方法來實現相似的效果,可是既然已經明白了它的原理,那就放心大膽的使用吧。
到這裏就弄清楚 Kotlin 中,使用默認參數來減小方法重載代碼的使用技巧和原理,以及注意事項了。
弄清楚原理以及須要注意的點,能夠幫助咱們更好的使用 Kotlin 的特性。咱們最後再總結一下本文的知識點:
@JvmOverloads
註解標記,它會自動生成該方法的所有重載方法。defStyleAttr
的默認值,而不該該是 0。今天就到這裏,對本文的內容你有什麼問題嘛?歡迎留言討論。
本文對你有幫助嗎?留言、轉發、收藏是最大的支持,謝謝!
公衆號後臺回覆成長『 成長』,將會獲得我準備的學習資料。