当前位置: 首页 > 科技观察

Kotlin重载一个方法,有两个面,节省代码的同时也带来一个深坑-Kotlin原理

时间:2023-03-22 15:54:03 科技观察

1.前言在今年5月的GoogleI/O上,Google正式向全世界宣布了Kotlin-First。一个重要的概念,Kotlin将成为Android开发者的首选语言。新语言有新特性,开发者还是保持Java编程习惯去写Kotlin,这也不是不可以,但总感觉几乎没有意义。最近公众号「GoogleDevelopers」连载了一个系列文章《实用 Kotlin 构建 Android 应用 | Kotlin 迁移指南》,其中举例说明了一些Kotlin编码技巧。既然是指导性文章,自然会在“多而广”的基础上,故意省略一些细节,同时,例子中可能会有一些不恰当的场景。在这里,我将填写这些详细信息。今天就说说利用Kotlin的方法默认参数的特性,来实现类似Java的方法重载的效果。全面分析这个特性的用法和原理,以及使用过程中的一个深坑。2.Kotlin的简单方法重载2.1Kotlin是如何简化方法重载的?在Java中,我们可以在同一个类中定义多个同名的方法,只要每个方法的参数类型或参数个数不同即可,这就是Java的方法重载。classHello{publicstaticvoidhello(){System.out.println("Hello,world!");}publicstaticvoidhello(Stringname){System.out.println("Hello,"+name+"!");}publicstaticvoidhello(Stringname,intage){if(age>0){System.out.println("你好,"+name+"("+age+")!");}else{System.out.println("你好,"+name+"!");}}}在这个例子中,我们定义了三个同名的hello()方法,但具有不同的逻辑细节。在Kotlin中,因为它支持在相同的方法中,传递“?”标记可为空的参数,并通过“=”给出参数的默认值。那么这三个方法在Kotlin中可以软化为一个方法。objectHelloDemo{funhello(name:String="world",age:Int=0){if(age>0){System.out.println("你好,${name}(${age})!");}else{System.out.println("Hello,${name}!");}}}是在Kotlin类中调用的,和前面的Java实现一致。HelloDemo.hello()HelloDemo.hello("呈祥魔影")HelloDemo.hello("呈祥魔影",16)但是这个方法声明的是Kotlin方法参数的默认值,在Java类中使用时,有一些区别.因为HelloDemo类被声明为对象,所以需要在Java中使用INSTANCE来调用它的方法。HelloDemo.INSTANCE.hello("呈象魔影",16);在Kotlin中调用hello()方法非常方便,可以选择性的忽略参数,但在Java中使用时,必须显式地完整赋值参数。Kotlin和Java分别是这样声明使用参数默认值的方法的。接下来我们来看一下原理。2.2Kotlin方法参数指定默认值的原则用Kotlin编写的代码之所以能在基于Java的虚拟机中运行,主要是因为在编译过程中会被编译成虚拟机可以识别的Java字节码.所以我们可以通过两次转换(ShowKotlinBytecode+Decompile)得到对应的Kotlin生成的Java代码。publicfinalvoidhello(@NotNullStringname,intage){Intrinsics.checkParameterIsNotNull(name,"name");if(age>0){System.out.println("Hello,"+name+'('+age+")!");}else{System.out.println("Hello,"+name+'!');}}//$FF:syntheticmethodpublicstaticvoidhello$default(HelloDemovar0,Stringvar1,intvar2,intvar3,Objectvar4){if((var3&1)!=0){var1="world";}if((var3&2)!=0){var2=0;}var0.hello(var1,var2);}会在这里生成一个hello()方法,同时也会有一个合成方法(syntheticmethod)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的时候。3.自定义View的构造方法满足Kotlin3.1也是一种方法。回到上面提到的谷歌开发者的《实用 Kotlin 构建 Android 应用 | Kotlin 迁移指南》系列文章,给出的例子其实是很不恰当的。这里的例子中使用了View这个词,重载的方法都是View的构造方法。我们在自定义View的时候,经常会和这三个方法打交道。但是谷歌工程师在这里给出的例子很容易被误解。其实如果在自定义View的时候这样写,肯定会报错。比如我们自定义一个DemoView,它继承自EditView。classDemoView(context:Context,attrs:AttributeSet?=null,defStyleAttr:Int=0):EditText(context,attrs,defStyleAttr){}这个自定义的DemoView,用在xml布局的时候,虽然编译不会出错,但是运行时,您将获得NoSuchMethodException。Causedby:java.lang.NoSuchMethodException:[classandroid.content.Context,interfaceandroid.util.AttributeSet]什么问题?LayoutInflater创建控件时,找不到DemoView(Context,AttributeSet)重载方法,所以报错。这其实很好理解。如上所述,Kotlin采用了使用带有默认值的方法的原则。事实上,Kotlin在编译之后最终会生成一个额外的合成方法来处理方法参数的默认值。它类似于Java方法。超载就不一样了。有了它生成的方法,确实不会出现多重方法重载。所以理解Kotlin的方法指定默认参数并不等同于Java的方法重载。只能说它们在某些场景下具有相似的特点。3.2使用@JvmOverloads那么回到这里的问题,在自定义View或者其他需要保留Java方法重载的场景下,如何让Kotlin在编译时真正生成对应的重载方法呢?这里就需要使用@JvmOverloads了。当Kotlin使用默认值方法并使用@JvmOverloads注解时,其含义是在编译时维护和暴露该方法的多个重载方法。其实在我们自定义View的时候,AS已经给了我们足够的提示,它会自动为我们生成一个带有@JvmOverloads的构造函数。AS帮我们完成的代码如下:classDemoView@JvmOverloadsconstructor(context:Context,attrs:AttributeSet?=null,defStyleAttr:Int=0):AppCompatEditText(context,attrs,defStyleAttr){}然后使用“KotlinBytecode+反编译”查看编译后的代码,验证@JvmOverloads的效果。@JvmOverloadspublicDemoView(@NotNullContextcontext,@NullableAttributeSetattrs,intdefStyleAttr){Intrinsics.checkParameterIsNotNull(context,"context");super(context,attrs,defStyleAttr);}//$FF:syntheticmethodpublicDemoView(Contextvar1,AttributeDevartContext4,Markstructor3){if((var4&2)!=0){var2=(AttributeSet)null;}if((var4&4)!=0){var3=0;}this(var1,var2,var3);}@JvmOverloadspublicDemoView(@NotNullContextcontext,@NullableAttributeSetattrs){this(context,attrs,0,4,(DefaultConstructorMarker)null);}@JvmOverloadspublicDemoView(@NotNullContextcontext){this(context,(AttributeSet)null,0,6,(DefaultConstructorMarker)null);}可以看得懂@JvmOverloads生效后,会按照我们的预期生成对应的重载方法,而合成方法会保留,完成在Kotlin中使用默认参数的需求。你认为它就在这里吗?不行,如果你在自定义View的时候按照AS给的提示生成代码,虽然程序不会崩溃,但是会出现一些未知的错误。3.3不要在View中直接使用AS生成代码。自定义View时,依赖AS提示生成代码,会遇到一些未知的错误。比如本文的例子,我们要实现一个EditView的子类,代码生成时带有AS提示。会发生什么?在EditView场景中,你会发现焦点没有了,点击后软键盘不会自动弹出。那为什么会出现这种问题呢?原因在于AS自动生成代码时对参数默认值的处理。在自定义View时,通过AS生成重载方法时,其对参数默认值的处理规则如下。遇到的对象,默认值为空。当遇到原始数据类型时,默认值是原始数据类型的默认值。例如,Int为0,Boolean为false。在这个场景中,参数defStyleAttr的类型是Int,所以默认值会被赋值为0,但这并不是我们需要的。在Android中,View通过XML文件进行布局使用时,会调用两个参数(Context上下文,AttributeSetattrs)的构造方法,内部会调用三个参数的构造方法,默认调用一个defStyleAttr通过。注意不是0,既然问题找到了,那好解决了。下面看看自定义View的父类中二参数构造函数是如何实现的,传入defStyleArrt作为默认值即可。那我们先看看AppCompatEditText中的实现。publicAppCompatEditText(Contextcontext,AttributeSetattrs){this(context,attrs,R.attr.editTextStyle);}然后在DemoView中修改defStyleAttr的默认值。classDemoView@JvmOverloadsconstructor(context:Context,attrs:AttributeSet?=null,defStyleAttr:Int=R.attr.editTextStyle):AppCompatEditText(context,attrs,defStyleAttr){}这里,自定义View中,使用默认参数构造方法过载问题也解决了。在自定义View的场景下,当然也可以通过重写多个构造函数方法来实现类似的效果,不过既然明白了它的原理,那就大胆使用吧。4.小结至此,我们将弄清楚在Kotlin中使用默认参数减少方法重载代码的技巧和原则,以及注意事项。弄清楚其中的原则和需要注意的地方,可以帮助我们更好的利用Kotlin的特性。最后总结一下本文的知识点:Kotlin可以通过为方法的参数指定默认值来实现类似Java中“方法重载”的效果。如果想保留Java的重载方法,可以使用@JvmOverloads注解标记,它会自动生成该方法的所有重载方法。自定义View时需要注意指定参数defStyleAttr的默认值,而不是0。【本文为专栏作者“张扬”原创稿件,转载请微信联系作者♂获得授权转载】点此查看该作者更多好文