今年五月的 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。今天就到这里,对本文的内容你有什么问题嘛?欢迎留言讨论。
本文对你有帮助吗?留言、转发、收藏是最大的支持,谢谢!
公众号后台回复成长『 成长』,将会获得我准备的学习资料。