Kotlin 朱涛-8 实战 inline 函数式编程 词频统计
目录
08 | 实战:用Kotlin写一个英语词频统计程序
迭代过程:
- 1.0 版本:使用
命令式风格
的代码实现基础功能 - 2.0 版本:利用
扩展函数
、高阶函数
优化代码,实现函数式风格
的代码 - 3.0 版本:使用
inline
提升软件的性能,并分析高阶函数的实现原理
命令式风格
统计步骤:
- 文本清洗:将标点符号替换成空格
- 文本分割:以空格作为分隔符,对文本进行分割
- 词频统计:遍历 List,将
单词
与频率
存储在 Map 中 - 词频排序:将高频词放在前面,低频词放在后面
data class WordFreq(val word: String, val frequency: Int) // 数据类
class TextProcessorV1 {
fun processText(text: String): List<WordFreq> {
val cleaned = clean(text)
val words = cleaned.split(" ")
val map = getWordCount(words)
val list = sortByFrequency(map)
return list
}
// 统计文件当中的单词词频
fun processFile(file: File): List<WordFreq> {
val text = file.readText(Charsets.UTF_8) // 从文件里读取文本
return processText(text)
}
}
文本清洗
fun clean(text: String): String {
return text.replace(".", " ")
.replace("!", " ")
.replace(",", " ")
.replace("?", " ")
.replace("'", " ")
.replace("*", " ")
.replace("#", " ")
.replace("@", " ")
.trim()
}
使用正则表达式
,将所有不是英文字母的字符都替换成空格:
fun clean(text: String) = text.replace("[^A-Za-z]".toRegex(), " ").trim()
词频统计
用一个 Map 来存储单词和它对应频率:
fun getWordCount(list: List<String>): Map<String, Int> {
val map = hashMapOf<String, Int>()
for (word in list) {
if (word == "") continue
val trimWord = word.trim()
val count = map.getOrDefault(trimWord, 0)
map[trimWord] = count + 1
}
return map
}
词频排序
将无序的数据结构换成有序的。
fun sortByFrequency(map: Map<String, Int>): MutableList<WordFreq> {
val list = mutableListOf<WordFreq>()
for (entry in map) {
if (entry.key == "") continue
val freq = WordFreq(entry.key, entry.value)
list.add(freq)
}
list.sortByDescending { it.frequency } // 以词频降序排序
return list
}
函数式风格
Kotlin 既有命令式的一面,也有函数式的一面,它们有着各自擅长的领域。
借助函数式编程
的思想来优化代码:
fun processText(text: String): List<WordFreq> {
return text
.clean()
.split(" ")
.getWordCount()
.sortByFrequency { WordFreq(it.key, it.value) }
}
转为扩展方法
fun clean(t: String) = t.replace("[^A-Za-z]".toRegex(), " ").trim() // 普通函数
fun String.clean() = replace("[^A-Za-z]".toRegex(), " ").trim() // 扩展方法
真实项目中,String.clean() 使用
顶层扩展
并不合适,顶层扩展只适用于通用的逻辑
,否则不清楚的人看着 idea 提示的扩展函数也是一脸懵逼。
private fun List<String>.getWordCount(): Map<String, Int> {
val map = HashMap<String, Int>()
for (element in this) { // 原本的参数 list 集合变成了 this
if (element == "") continue
val trim = element.trim()
val count = map.getOrDefault(trim, 0)
map[trim] = count + 1
}
return map
}
private fun Map<String, Int>.sortByFrequency(): MutableList<WordFreq> {
val list = mutableListOf<WordFreq>()
for (entry in this) { // 将参数变成 this
val freq = WordFreq(entry.key, entry.value)
list.add(freq)
}
list.sortByDescending { it.frequency }
return list
}
对 sortByFrequency() 的逻辑进行拆分:
private fun <T> Map<String, Int>.mapToList(transform: (Map.Entry<String, Int>) -> T): MutableList<T> {
val list = mutableListOf<T>()
for (entry in this) {
val freq = transform(entry) // 调用 transform,将 Map.Entry 类型的参数转换为 T
list.add(freq)
}
return list
}
优化后的效果
fun processText(text: String): List<WordFreq> = text
.clean()
.split(" ")
.getWordCount()
.mapToList { WordFreq(it.key, it.value) }
.sortedByDescending { it.frequency }
进一步优化
上面的 clean、getWordCount、mapToList 等方法,可以借助 Kotlin 标准库函数来实现:
fun processText(text: String): List<WordFreq> = text
.replace("[^A-Za-z]".toRegex(), " ")
.trim()
.split(" ")
.filter { it != "" }
.groupBy { it }
.map { WordFreq(it.key, it.value.size) }
.sortedByDescending { it.frequency }
使用 inline 优化
这个版本,我们只需要为 mapToList 这个高阶函数增加一个 inline
关键字即可。
private inline fun <T> Map<String, Int>.mapToList(
transform: (Map.Entry<String, Int>) -> T
): MutableList<T> {
//...
}
高阶函数的实现原理
// HigherOrderExample.kt
fun foo(block: () -> Unit) {
block()
}
fun main() {
var i = 0
foo { i++ }
}
反编译成 Java 后:
public final class HigherOrderExampleKt {
public static final void foo(Function0 block) { // 参数是一个接口
block.invoke();
}
public static final void main() {
int i = 0;
foo((Function0) (new Function0() { // 参数是一个实现 Function0 接口的【匿名内部类】
public final void invoke() { // 接口中定义的方法,这个方法【没有参数】
i++;
}
}));
}
}
可以看到,Kotlin 高阶函数当中的函数类型
参数,变成了 Function0
,而 main()
函数当中的高阶函数调用,也变成了匿名内部类
的调用方式。
那么,Function0
又是个什么东西?
public interface Function0<out R> : Function<R> {
public operator fun invoke(): R
}
Function0
是 Kotlin 标准库当中定义的接口,它代表没有参数
的函数类型。Kotlin 一共定义了 23 个类似的接口,从 Function0
一直到 Function22
,分别代表了无参数的函数类型
到22 个参数的函数类型
。
inline 的实现原理
使用 inline
优化过的高阶函数:
inline fun fooInline(block: () -> Unit) { // 唯一的区别:多了一个关键字 inline
block()
}
fun main() {
var i = 0
fooInline { i++ }
}
反编译后的 Java 后:
public final class HigherOrderInlineExampleKt {
public static final void fooInline(Function0 block) { // 没有变化
block.invoke();
}
public static final void main() {
int i = 0; // 区别:匿名内部类不见了,方法调用也不见了
int i = i + 1; // 原理:将 inline 函数当中的代码拷贝到调用处
}
}
inline 的作用其实就是:将 inline 函数当中的代码拷贝到调用处。
使用 JMH 测试 inline 性能
不使用 inline
时:
main()
中需要调用foo()
,多了一次函数调用的开销foo()
中需要创建了匿名内部类对象,这也是额外的开销
为了验证使用 inline
前后的性能差异,我们可以使用 JMH
(Java Microbenchmark Harness)对这两组代码进行性能测试。JMH 可以最大程度地排除外界因素的干扰(比如内存抖动、虚拟机预热),从而判断出我们这两组代码执行效率的差异。它的结果不一定非常精确,但足以说明一些问题。
下面以两组测试代码为例,来探究下 inline 到底能为我们带来多少性能上的提升:
fun foo(block: () -> Unit) = block() // 不用 inline 的高阶函数
inline fun fooInline(block: () -> Unit) = block() // 使用 inline 的高阶函数
// 测试无 inline 的代码
@Benchmark
fun testNonInlined() {
var i = 0
foo { i++ }
}
// 测试有 inline 的代码
@Benchmark
fun testInlined() {
var i = 0
fooInline { i++ }
}
最终的测试结果如下,分数越高性能越好:
Benchmark Mode Score Error Units
testInlined thrpt 3272062.466 ± 67403.033 ops/ms
testNonInlined thrpt 355450.945 ± 12647.220 ops/ms
从上面的测试结果我们能看出来,是否使用 inline
的效率相差 10
倍。在一些复杂的代码场景下,多个高阶函数嵌套执行,它们之间的执行效率会相差上百倍。
测试多层嵌套的性能差异
为了模拟复杂的代码结构,我们可以简单地将上面这两个函数分别嵌套 10 个层级,然后看看它们之间的性能差异:
@Benchmark
fun testNonInlined() {
var i = 0
foo {
foo {
foo {
foo {
foo {
foo {
foo {
foo {
foo {
foo {
i++
}
}
}
}
}
}
}
}
}
}
}
@Benchmark
fun testInlined() {
var i = 0
fooInline {
fooInline {
fooInline {
fooInline {
fooInline {
fooInline {
fooInline {
fooInline {
fooInline {
fooInline {
i++
}
}
}
}
}
}
}
}
}
}
}
Benchmark Mode Score Error Units
testInlined thrpt 3266143.092 ± 85861.453 ops/ms
testNonInlined thrpt 31404.262 ± 804.615 ops/ms
可以看到,在嵌套了 10 个层级以后,testInlined 的性能几乎没有什么变化;而 testNonInlined 的性能比 1 层嵌套差了 10
倍。
在这种情况下,testInlined() 与 testNonInlined() 之间的性能差异就达到了 100
倍。
如果反编译成 Java 代码,能看到:
- 对于 testNonInlined(),由于 foo() 嵌套了 10 层,它反编译后的代码也嵌套了 10 层函数调用,中间还伴随了 10 次匿名内部类的创建
- 而对于 testInlined(),则只有简单的两行代码,完全没有任何嵌套的痕迹
inline 的使用限制
Kotlin 官方建议:仅将 inline 用于修饰高阶函数。而且,也不是所有高阶函数都可以用 inline
IntelliJ IDEA 会使用 inline 修饰的普通函数发出警告:
Expected 期望 performance impact 性能影响 of inlining is insignificant 微不足道. Inlining works best for functions with parameters of
functional types
函数类型参数的函数
另外,在 processText() 方法的内部,getWordCount() 和 mapToList() 这两个方法还会报错:
Public-API inline function cannot access non-public API xxx
报错的原因是:inline 的作用其实就是将 inline 函数当中的代码拷贝
到调用处,而只有 public 的方法才可以被拷贝(访问))。
2016-06-01
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/5549261.html