在使用 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,需要使用标签限定符。
编程技巧提示:可以新建一个公共源文件,把自定义的扩展属性和扩展函数都放到包中,作为一个通用工具类来使用。