Kotlin – 扩展函数

在使用 Java 的时候,我们经常使用诸如 StringUtil、DateUtil 等工具类,代码写起来比较冗长。举个例子,获取一个字符串的第一个字符值、最后一个字符值。如果我们用 Java 代码来写,通常是要先声明一个 StringUtil 类,然后在里面写相应的工具方法,代码可以是下面这样:

public class StringUtil {

    /**
     * 获取 str 的第一个字符值
     *
     * @param str
     * @return
     */
    public static String firstChar(String str) {
        if (str != null && str.length() > 0) { // 非空判断
            return str.charAt(0) + "";
        }

        return "";
    }

    /**
     * 获取 str 的最后一个字符值
     *
     * @param str
     * @return
     */
    public static String lastChar(String str) {
        if (str != null && str.length() > 0) {
            return str.charAt(str.length() - 1) + "";
        }

        return "";
    }

    public static void main(String[] args) {
        String str = "abc";

        System.out.println(StringUtil.firstChar(str)); // 返回 a
        System.out.println(StringUtil.lastChar(str)); // 返回 c
    }
}
复制代码

我们可以看到 StringUtil.firstChar(str) 这样的调用方式不够简单直接。能不能这样调用呢?

"abc".firstChar()
"abc".lastChar()
复制代码

非常遗憾的是,在 Java 中我们无法给 String 类添加一个自定义方法。因为 String 类是 JDK 内置的基础类,而且为 final,不能修改。所以,Java 程序员通常使用一个变通的方法:开发一个 StringUtil 类,在里面封装所需要的 String 操作的方法,而不是修改或继承 String 类。

而在 Koltin 里,情况就完全不一样了——我们完全可以自由扩展任何类的方法和属性。在不修改原类的情况下,Kotlin 能给一个类扩展新功能而无须继承该类。

给 String 类扩展两个函数

现在给 String 类扩展两个函数 firstChar() 和 lastChar(),实现代码如下:

// 目标类型.扩展函数名 : 函数返回类型
fun String.firstChar(): String { // Kotlin 中给 String 扩展一个 firstChar 函数
    if (this.isEmpty()) {
        return ""
    }
    
    return this[0].toString()
}

fun String.lastChar(): String { // kotlin 中给 String 扩展一个 lastChar 函数
    if (this.isEmpty()) {
        return ""
    }
    
    return this[this.length - 1].toString()
}
复制代码

然后就可以在代码中直接调用了:

fun main() {
    println("abc".firstChar()) // 返回 a
    println("abc".lastChar()) // 返回 c
}
复制代码

给 List 类扩展一个过滤函数

首先来看在 Java 中实现一个 List 的 filter() 函数。我们回去声明一个 ListUtil 类,里面实现一个 List filter(List list, Predicate p) 方法,代码如下:

public class ListUtil<T> {

    /**
     * 根据谓词 p 过滤 list 中的元素
     *
     * @param list
     * @param p
     * @return
     */
    public List<T> filter(List<T> list, Predicate<T> p) {

        List<T> result = new ArrayList<>();

        for (T t : list) {
            if (p.test(t)) {
                result.add(t);
            }
        }

        return result;
    }
}
复制代码

其中,Predicate 接口声明如下:

@FunctionalInterface
public interface PredicateA<T> {
    boolean test(T t); // 返回布尔值的谓词函数
}
复制代码

然后,我们在代码中这样使用这个 filter() 方法:

public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
    ListUtil<Integer> listUtil = new ListUtil<>();
    List<Integer> result = listUtil.filter(list, (it) -> it % 2 == 1);
    System.out.println(result); //输出 [1, 3, 5, 7]
}
复制代码

为了调用 filter() 方法,我们还要声明一个 ListUtil() 对象,这样显得比较麻烦。能不能直接像下面这样调用呢?

list.filter{ it % 2 === 1}
复制代码

答案是肯定的,只不过必须在 Kotlin 中,而不是在 Java 中。下面我们就来使用 Kotlin 中的扩展函数为 List 扩展一个 filter() 函数,代码如下:

// 目标类型.扩展函数名[函数入参] : 函数返回类型
fun <T> List<T>.filter(predicate: (T) -> Boolean): MutableList<T> {
    val result = ArrayList<T>()

    this.forEach {
        if (predicate(it)) {
            result.add(it)
        }
    }
    
    return result
}
复制代码

然后我们在代码中只需要这样调用即可:

val list = mutableListOf(1, 2, 3, 4, 5, 6, 7)

val result = list.filter {
    it % 2 == 1
}

println(result) // [1, 3, 5, 7]
复制代码

扩展属性

除了扩展一个类的函数,还可以扩展类属性。例如,我们给 MutableList 扩展两个属性:firstElement 和 lastElement,实现代码如下:

// 属性可变性 类型参数 目标类型.扩展属性名 : 扩展属性的类型
var <T> MutableList<T>.filterElement: T
    get() { // 扩展属性 firstElement 的 get() 函数
        return this[0] // 返回第 1 个元素
    }
    set(value) { // 扩展属性 firstElement 的 set() 函数
        this[0] = value // 设置第 1 个元素为 value
    }

var <T> MutableList<T>.lastElement: T
    get() {
        return this[this.size - 1]
    }
    set(value) {
        this[this.size - 1] = value
    }
复制代码

然后就可以在代码中直接使用扩展的属性了:

fun main() {
    val list = mutableListOf(1, 2, 3, 4, 5, 6, 7)

    println("list = $list") // list = [1, 2, 3, 4, 5, 6, 7]

    println(list.firstElement) // 1
    println(list.lastElement) // 7

    list.firstElement = -1
    list.lastElement = -7

    println("list = $list") // list = [-1, 2, 3, 4, 5, 6, -7]
    println(list.firstElement) // -1
    println(list.lastElement) // -7

}
复制代码

扩展属性允许定义在类或者 Kotlin 文件中,不允许定义在函数中。

扩展的实现原理

扩展属性和扩展函数的本质是以静态导入的方式来实现的。其背后的实现原理可以通过 Kotlin 代码的 ByteCode 来理解。

fun String.firstChar(): String {
    if (this.isEmpty()) {
        return ""
    }

    return this[0].toString()
}
复制代码

它对应的 JVM 码如下:

// access flags 0x19
  public final static firstChar(Ljava/lang/String;)Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
    // annotable parameter count: 1 (visible)
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "$this$firstChar"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 2 L1
    ALOAD 0
    CHECKCAST java/lang/CharSequence
    ASTORE 1
   L2
    ICONST_0
    ISTORE 2
   L3
    ALOAD 1
    INVOKEINTERFACE java/lang/CharSequence.length ()I (itf)
    IFNE L4
    ICONST_1
    GOTO L5
   L4
    ICONST_0
   L6
   L5
    LINENUMBER 2 L5
    IFEQ L7
   L8
    LINENUMBER 3 L8
    LDC ""
    ARETURN
   L7
    LINENUMBER 6 L7
    ALOAD 0
    ICONST_0
    INVOKEVIRTUAL java/lang/String.charAt (I)C
    INVOKESTATIC java/lang/String.valueOf (C)Ljava/lang/String;
    ARETURN
   L9
    LOCALVARIABLE $this$firstChar Ljava/lang/String; L0 L9 0
    MAXSTACK = 2
    MAXLOCALS = 3
复制代码

直接看上面的 JVM 指令可能不直观,反编译成 Java 代码后会更清楚:

   @NotNull
   public static final String firstChar(@NotNull String $this$firstChar) {
      Intrinsics.checkNotNullParameter($this$firstChar, "$this$firstChar");
      CharSequence var1 = (CharSequence)$this$firstChar;
      boolean var2 = false;
      return var1.length() == 0 ? "" : String.valueOf($this$firstChar.charAt(0));
   }
复制代码

扩展中的 this 关键字

在前面的 List 扩展函数 filter() 的实现中,用到了一个 this 关键字:

this.forEach {
    if (predicate(it)) {
        result.add(it)
    }
}
复制代码

这里的 this 指的是接收者对象 (receive object),也就是调用扩展函数时,在点号 “,” 之前指定的对象实例。为了表示当前函数的接收者(receive),Kotlin 中使用 this 表达式:

  • 在类的成员函数中,this 指向这个类的当前对象实例
  • 在扩展函数中,或带接收者的函数字面值(function literal)中,this 代表调用函数时,在点号左侧传递的接收者参数
  • 如果 this 没有限定符,那么它指向包含当前代码的最内层范围。如果想要指向其它范围内的 this,需要使用标签限定符。

编程技巧提示:可以新建一个公共源文件,把自定义的扩展属性和扩展函数都放到包中,作为一个通用工具类来使用。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享