新语言有新特性,开发者还保持 Java 的编程习惯去写 Kotlin,也不是不行,但是总感觉差点意思。

最近公众号「谷歌开发者」连载了一个《实用 Kotlin 构建 Android 应用 | Kotlin 迁移指南》的系列文章,就举例了一些 Kotlin 编码的小技巧。

既然是一种指南性质的文章,自然在「多而广」的基础上,有意去省略一些细节,同时举例的场景,可能还有一些不恰当的地方。

这里我就来补齐这些细节,今天聊聊利用 Kotlin 的方法默认参数的特性,完成类似 Java 的方法重载的效果。完全解析这个特性的使用方式和原理,以及在使用过程中的一个深坑。

二. Kotlin 的简易方法重载

2.1 Kotlin 如何简化方法重载?

在 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 中,被柔和成一个方法。

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 来调用它的方法。

HelloDemo.INSTANCE.hello("承香墨影",16);

Kotlin 中调用 hello() 方法很方便,可以选择性的忽略参数,但是在 Java 中使用,必须全量的显式的去做参数赋值。

这就是使用了参数默认值的方法申明时,分别在 Kotlin 和 Java 中的使用方式,接下来我们看看原理。

2.2 Kotlin 方法参数指定默认值的原理

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 时。

三. 自定义 View 遇上 Kotlin

3.1 构造方法也是方法

再回到前面提到的谷歌开发者的《实用 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 的方法重载,并不等价。只能说它们在某些场景下,特性是类似的。

3.2 使用 @JvmOverloads

那么回到这里的问题,在自定义 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);
}
## 结尾

**如何才能让我们在面试中对答如流呢?**

答案当然是平时在工作或者学习中多提升自身实力的啦,那如何才能正确的学习,有方向的学习呢?有没有免费资料可以借鉴。为此我整理了一份Android学习资料路线:

![](https://img-blog.csdnimg.cn/img_convert/7e2f88c9c29f35877720cccce7a89e9d.png)

这里是一份BAT大厂面试资料专题包:

![](https://img-blog.csdnimg.cn/img_convert/34e06d4130ea19f07a8aa1b46611e03d.png)

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划。来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

55)]

这里是一份BAT大厂面试资料专题包:

[外链图片转存中...(img-m6y4gvTf-1630579959856)]

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划。来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

**[CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》](https://codechina.csdn.net/m0_60958482/android_p7)**
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐