Kotlin 源码 | 降低代码复杂度的法宝

简介: Kotlin 源码 | 降低代码复杂度的法宝

随着码龄增大,渐渐意识到团队代码中的最大的敌人是“复杂度”。不合理的复杂度是降低代码质量,增加沟通成本的元凶。


Kotlin 在降低代码复杂度方面有着诸多法宝。这一篇就以两个常见的业务场景来剖析下简单和复杂的关系。若要用一句话概括这关系,我最喜欢这一句:“一切简单的背后都蕴藏着复杂”。


启动线程和读取文件容是 Android 开发中两个颇为常见的场景。分别给出 Java 和 Kotlin 的实现,在惊叹两种语言表达力上悬殊的差距的同时,逐层剖析 Kotlin 语法简单背后的复杂。


启动线程


先看一个简单的业务场景,在 java 中用下面的代码启动一个新线程:


 Thread thread = new Thread() {
     @Override
     public void run() {
         doSomething() // 业务逻辑
         super.run();
     }
 };
 thread.setDaemon(false);
 thread.setPriority(-1);
 thread.setName("thread");
 thread.start();


启动线程是一个常用操作,其中除了 doSomething() 之外的其他代码都具有通用性。难道每次启动线程时都复制粘贴这一坨代码吗?不优雅!得抽象成一个静态方法以便到处调用:


public class ThreadUtil {
    public static Thread startThread(Callback callback) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                if (callback != null) callback.action();
                super.run();
            }
        };
        thread.setDaemon(false);
        thread.setPriority(-1);
        thread.setName("thread");
        thread.start();
        return thread;
    }
    public interface Callback {
        void action();
    }
}


仔细分析下这里引入的复杂度,一个新的类ThreadUtil及静态方法startThread(),还有一个新的接口Callback


然后就可以像这样构建线程了:


ThreadUtil.startThread( new Callback() {
    @Override
    public void action() {
        doSomething();
    }
})


对比下 Kotlin 的解决方案thread()


public fun thread(
    start: Boolean = true,
    isDaemon: Boolean = false,
    contextClassLoader: ClassLoader? = null,
    name: String? = null,
    priority: Int = -1,
    block: () -> Unit
): Thread {
    val thread = object : Thread() {
        public override fun run() {
            block()
        }
    }
    if (isDaemon)
        thread.isDaemon = true
    if (priority > 0)
        thread.priority = priority
    if (name != null)
        thread.name = name
    if (contextClassLoader != null)
        thread.contextClassLoader = contextClassLoader
    if (start)
        thread.start()
    return thread
}


thread()方法把构建线程的细节全都隐藏在方法内部。


然后就可以像这样启动一个新线程:


thread { doSomething() }


这简洁的背后是一系列语法特性的支持:


1. 顶层函数


Kotlin 中把定义在类体外,不隶属于任何类的函数称为顶层函数thread()就是这样一个函数。这样定义的好处是,可以在任意位置,方便地访问到该函数。


Kotlin 的顶层函数被编译成 java 代码后就变成一个类中的静态函数,类名是顶层函数所在文件名+Kt 后缀。


2. 高阶函数


若函数的参数或者返回值是 lambda 表达式,则称该函数为高阶函数


thread()方法的最后一个参数是 lambda 表达式。在 Kotlin 中当调用函数只传入一个 lambda 类型的参数时,可以省去括号。所以就有了thread { doSomething() }这样简洁的调用。


3. 参数默认值 & 命名参数


thread()函数包含了 6 个参数,为啥在调用时可以只传最后一个参数?因为其余的参数都在定义时提供了默认值。这个语法特性叫参数默认值


当然也可以忽略默认值,重新为参数赋值:


thread(isDaemon = true) { doSomething() }


当只想重新为某一个参数赋值时,不用将其余参数都重写一遍,只需用参数名 = 参数值,这个语法特性叫命名参数


逐行读取文件内容


再看一个稍复杂的业务场景:“读取文件中每一行的内容并打印”,用 Java 实现的代码如下:


File file = new File(path)
BufferedReader bufferedReader = null;
try {
    bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
    String line;
    // 循环读取文件中的每一行并打印
    while ((line = bufferedReader.readLine()) != null) {
        System.out.println(line);
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    // 关闭资源
    if (bufferedReader != null) {
        try {
            bufferedReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


对比一下 Kotlin 的解决方案:


File(path).readLines().foreach { println(it) }


一句话搞定,就算没学过 Kotlin 也能猜到这是在干啥,语义是如此简洁清晰。这样的代码写的时候畅快,读的时候悦目。


之所以简单,是因为 Kotlin 通过各种语法特性将复杂度分层并隐藏在了后背。


1. 扩展方法


拨开简单的面纱,探究背后隐藏的复杂:


// 为 File 扩展方法 readLines()
public fun File.readLines(charset: Charset = Charsets.UTF_8): List<String> {
    // 构建字符串列表
    val result = ArrayList<String>()
    // 遍历文件的每一行并将内容添加到列表中
    forEachLine(charset) { result.add(it) }
    // 返回列表
    return result
}


扩展方法是 Kotlin 在类体外给类新增方法的语法,它用类名.方法名()表达。


把 Kotlin 编译成 java,扩展方法就是新增了一个静态方法:


final class FilesKt__FileReadWriteKt {
   // 静态函数的第一个参数是 File
   public static final List readLines(@NotNull File $this$readLines, @NotNull Charset charset) {
      Intrinsics.checkNotNullParameter($this$readLines, "$this$readLines");
      Intrinsics.checkNotNullParameter(charset, "charset");
      final ArrayList result = new ArrayList();
      FilesKt.forEachLine($this$readLines, charset, (Function1)(new Function1() {
         public Object invoke(Object var1) {
            this.invoke((String)var1);
            return Unit.INSTANCE;
         }
         public final void invoke(@NotNull String it) {
            Intrinsics.checkNotNullParameter(it, "it");
            result.add(it);
         }
      }));
      return (List)result;
   }
}


静态方法中的第一个参数是被扩展对象的实例,所以在扩展方法中可以通过this访问到类实例及其公共方法。


File.readLines() 的语义简单明了:遍历文件的每一行,将其添加到列表中并返回。


复杂度都被隐藏在了forEachLine(),它也是 File 的扩展方法,此处应该是this.forEachLine(charset) { result.add(it) },this 通常可以省略。

forEachLine()是个好名字,一眼看去就知道是在遍历文件的每一行。


public fun File.forEachLine(charset: Charset = Charsets.UTF_8, action: (line: String) -> Unit): Unit {
    BufferedReader(InputStreamReader(FileInputStream(this), charset)).forEachLine(action)
}


forEachLine()中将 File 层层包裹最终形成一个 BufferReader 实例,并且调用了 Reader 的扩展方法forEachLine()


public fun Reader.forEachLine(action: (String) -> Unit): Unit = 
    useLines { it.forEach(action) }


forEachLine()调用了同是 Reader 的扩展方法useLines(),从名字细微的差别就可以看出uselines()完成了文件所有行内容的整合,而且这个整合的结果是可以被遍历的。


2. 泛型


哪个类能整合一组元素,并可以被遍历?沿着调用链继续往下:


public inline fun <T> Reader.useLines(block: (Sequence<String>) -> T): T =
    buffered().use { block(it.lineSequence()) }


Reader 在useLines()中被缓冲化:


public inline fun Reader.buffered(bufferSize: Int = DEFAULT_BUFFER_SIZE): BufferedReader =
    // 如果已经是 BufferedReader 则直接返回,否则再包一层
    if (this is BufferedReader) this else BufferedReader(this, bufferSize)


紧接着调用了use(),使用 BufferReader:


// Closeable 的扩展方法
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    var exception: Throwable? = null
    try {
        // 触发业务逻辑(扩展对象实例被传入)
        return block(this)
    } catch (e: Throwable) {
        exception = e
        throw e
    } finally {
        // 无论如何都会关闭资源
        when {
            apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
            this == null -> {}
            exception == null -> close()
            else ->
                try {
                    close()
                } catch (closeException: Throwable) {}
        }
    }
}


这次的扩展函数不是一个具体类,而是一个泛型,并且该泛型的上界是Closeable,即为所有可以被关闭的类新增一个use()方法。


use()扩展方法中,lambda 表达式block代表了业务逻辑,扩展对象作为实参传入其中。业务逻辑在try-catch代码块中被执行,最后在finally中关闭了资源。上层可以特别省心地使用这个扩展方法,因为不再需要在意异常捕获和资源关闭。


3. 重载运算符 & 约定


读取文件内容的场景中,use() 中的业务逻辑是将BufferReader转换成LineSequence,然后遍历它。这里的遍历和类型转换分别是怎么实现的?


// 将 BufferReader 转化成 Sequence
public fun BufferedReader.lineSequence(): Sequence<String> =
    LinesSequence(this).constrainOnce()


还是通过扩展方法,直接构造了LineSequence对象并将BufferedReader传入。这种通过组合方式实现的类型转换和装饰者模式颇为类似(关于装饰者模式的详解可以点击使用组合的设计模式 | 美颜相机中的装饰者模式


LineSequence 是一个 Sequence:


// 序列
public interface Sequence<out T> {
    // 定义如何构建迭代器
    public operator fun iterator(): Iterator<T>
}
// 迭代器
public interface Iterator<out T> {
    // 获取下一个元素
    public operator fun next(): T
    // 判断是否有后续元素
    public operator fun hasNext(): Boolean
}


Sequence是一个接口,该接口需要定义如何构建一个迭代器iterator。迭代器也是一个接口,它需要定义如何获取下一个元素及是否有后续元素。


2 个接口中的 3 个方法都被保留词operator修饰,它表示重载运算符,即重新定义运算符的语义。Kotlin 中预定义了一些函数名和运算符的对应关系,称为约定。当前这个约定就是iterator() + next() + hasNext()for循环的约定。


for 循环在 Kotlin 中被定义为“遍历迭代器提供的元素”,需要和in保留词一起使用:


public inline fun <T> Sequence<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}


Sequence 有一个扩展法方法forEach()来简化遍历语法,内部就使用了“for + in”来遍历序列中所有的元素。


所以才可以在Reader.forEachLine()中用如此简单的语法实现遍历文件中的所有行。


public fun Reader.forEachLine(action: (String) -> Unit): Unit = 
    useLines { it.forEach(action) }


关于 Sequence 的用法实例可以点击Kotlin 基础 | 望文生义的 Kotlin 集合操作


LineSequence 的语义是 Sequence 中每一个元素都是文件中的一行,它在内部实现iterator()接口,构造了一个迭代器实例:


// 行序列:在 BufferedReader 外面包一层 LinesSequence
private class LinesSequence(private val reader: BufferedReader) : Sequence<String> {
    override public fun iterator(): Iterator<String> {
        // 构建迭代器
        return object : Iterator<String> {
            private var nextValue: String? = null // 下一个元素值
            private var done = false // 迭代是否结束
            // 判断迭代器中是否有下一个元素,并顺便获取下一个元素存入 nextValue
            override public fun hasNext(): Boolean {
                if (nextValue == null && !done) {
                    // 下一个元素是文件中的一行内容
                    nextValue = reader.readLine()
                    if (nextValue == null) done = true
                }
                return nextValue != null
            }
            // 获取迭代器中下一个元素
            override public fun next(): String {
                if (!hasNext()) {
                    throw NoSuchElementException()
                }
                val answer = nextValue
                nextValue = null
                return answer!!
            }
        }
    }
}


LineSequence 内部的迭代器在hasNext()中获取了文件中一行的内容,并存储在nextValue中,完成了将文件中每一行的内容转换成 Sequence 中的一个元素。


当在 Sequence 上遍历时,文件中每一行的内容就一个个出现在迭代中。这样做的好处是对内存更加友好,LineSequence 并没有持有文件中所有行的内容,它只是定义了如何获取文件中下一行的内容,所有的内容只有等待遍历时,才一个个地浮现出来。


用一句话总结 Kotlin 逐行读取文件内容的算法:用缓冲流(BufferReader)包裹文件,再用行序列(LineSequence)包裹缓冲流,序列迭代行为被定义为读取文件中一行的内容。遍历序列时,文件内容就一行行地被添加到列表中。


总结


顶层函数、高阶函数、默认参数、命名参数、扩展方法、泛型、重载运算符,Kotlin 利用了这些语法特性隐藏了实现常用业务功能的复杂度,并且在内部将复杂度分层。


分层是降低复杂度的惯用手段,它不仅让复杂度分散,使得同一时刻只需面对有限的复杂度,并且可以通过对每一层取一个好名字来概括本层的语义。除此之外,它还有助于定位问题(缩小问题范围)并增加代码可复用性(每层单独复用)。


是不是也可以效仿这种分层的思想方法,在写代码之前,琢磨一下,复杂度是不是太高了?可以运用那些语言特性实现合理的抽象将复杂度分层?以避免复杂度在一个层次被铺开。


推荐阅读











目录
相关文章
|
2月前
|
XML 编译器 Android开发
Kotlin DSL 实战:像 Compose 一样写代码
Kotlin DSL 实战:像 Compose 一样写代码
91 0
|
2月前
|
JSON 监控 数据挖掘
使用Kotlin代码简化局域网监控软件开发流程
使用Kotlin简化局域网监控软件开发,通过获取网络设备的IP和MAC地址,实现实时监控网络流量。示例代码展示了如何创建Kotlin项目,获取网络设备信息,监控网络流量以及进行数据分析和处理。此外,还演示了如何使用HTTP库将数据提交到网站,为网络管理提供高效支持。
110 0
|
20天前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
2月前
|
Java Kotlin
java调用kotlin代码编译报错“找不到符号”的问题
java调用kotlin代码编译报错“找不到符号”的问题
69 10
|
9月前
|
缓存 API Android开发
Kotlin 学习笔记(七)—— Flow 数据流学习实践指北(三)冷流转热流以及代码实例(下)
Kotlin 学习笔记(七)—— Flow 数据流学习实践指北(三)冷流转热流以及代码实例(下)
94 0
|
9月前
|
缓存 Java Kotlin
Kotlin 学习笔记(七)—— Flow 数据流学习实践指北(三)冷流转热流以及代码实例(上)
Kotlin 学习笔记(七)—— Flow 数据流学习实践指北(三)冷流转热流以及代码实例(上)
71 0
|
11月前
|
IDE 安全 Java
使用 kotlin.Deprecated,优雅废弃你的过时代码
使用 kotlin.Deprecated,优雅废弃你的过时代码
226 0
|
SQL XML 安全
写更易懂的代码,Kotlin 是这样隐藏复杂度的(一)
写更易懂的代码,Kotlin 是这样隐藏复杂度的(一)
171 0
|
Java Android开发 Kotlin
kotlin查看编译后的Java代码
kotlin查看编译后的Java代码
DHL
|
算法 前端开发 安全
影响性能的 Kotlin 代码(一)
Kotlin 高级函数的特性不仅让代码可读性更强,更加简洁,而且还提高了生产效率,但是简洁的背后是有代价的,隐藏着不能被忽视的成本,特别是在低端机上,这种成本会被放大,因此我们需要去研究 kotlin 语法糖背后的魔法,选择合适的语法糖,尽量避免这些坑。
DHL
190 0
影响性能的 Kotlin 代码(一)