Kotlin之集合操作符

kotlin中集合操作基本跟Java的api类似,不过它比Java多了很多扩展方法。这些扩展方法很像Java中Rx中的操作符,可以对原集合做各种变换。这些扩展方法在Kotlin中是标准库函数中的一部份,当你了解之后配合lambda表达式写代码时,你会感觉太爽了,代码就应该这样写。下面就我的理解做一个记录。

成员引用

Kotlin中允许你去将表达式当作参数传递,你也可以直接传递函数,跟Java8一样,如果你把函数转化为一个值的话,可以用成员引用

val getAge = Person::age

这个表达式叫做 成员引用 。它为创建一个直接调用方法或访问属性的函数值提供了一种简短的语法。双冒号将类名从你需要引用的成员(方法或属性)名中分隔出来。它所做的事情跟下面其实是一样的,但是更简洁(少了一个中间变量):

val getAge = { person: Person -> person.age }

这个成员引用是lambda中的知识点,还有很多用法,需要去理解。

maxBy

找出这个集合中根据某个字段排序最大的值,可以仔细观察下面代码,它们的结果和意思完全一样。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* list操作符之maxby
*/
fun listOperatorMaxBy(){

//当lambda表达式是最后一个参数的时候可以不要(),直接用{}
val people = listOf(Person2("Bob",24), Person2("Alice",42))
println(people.maxBy { it.age })
println(people.maxBy { person2 -> person2.age })
people.maxBy(Person2::age)

}

transform/joinString

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* list操作符之transform/joinToString
*/
fun listOperatorTransform(){

val people = listOf(Person2("Bob",24), Person2("Alice",42))

val names = people.joinToString(separator = ",",postfix = ": ",transform = {p: Person2 -> p.name})
val names1 = people.joinToString("","","",1) { p -> p.name }
println(names) //Bob,Alice:
println(names1) //Bob

}

filter

根据条件过滤操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* list操作符之filter
*/

fun listOperatorFilter(){

val people = listOf(Person2("Bob",24), Person2("Alice",42),Person2("Ervin",34))

//但是要注意,这份代码为每个人重复了最大年龄的查找步骤。所以,如果集合中有100个人,最大年龄的搜索将会执行100次!
people.filter { it.age == people.maxBy(Person2::age)!!.age }
//people.filter { it.age == people.maxBy{person2 -> person2.age}!!.age }


//只计算了一次最大值
var maxAge = people.maxBy(Person2::age)?.age
people.filter { it.age == maxAge }

}

all,any,count,find等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/***
* list操作符all,any
* list是否包含了所有这个条件
*/
fun listOperatorAllAny(){
val people = listOf(Person2("Bob",24), Person2("Alice",42),Person2("Ervin",34))

val conditions = {p: Person2 -> p.age > 25}
//全部匹配条件
people.all(conditions) // false

people.all { p:Person2 -> p.age > 23 } // true

//有一个匹配条件的
people.any(conditions) // true

//有多少个符合条件的
people.count(conditions) // 2

// 查找哪个是符合条件的(如果有多个元素,函数将返回第一个匹配的元素。
// 如果没有满足的元素,函数返回 null 。 find 的一个同义词是 firstOrNull 。
// 如果如能够更加清晰的表达你对想法,你可以使用)
people.find { p:Person2 -> p.age > 23 } //Alice,Ervin
people.firstOrNull(conditions)

}

groupBy

分组操作符,想象一下,你需要根据你某些特性来将所有元素分割成不同的组。例如,你想把年龄相同的人放在一组。把这个特性直接作为一个参数进行传递非常方便!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* list操作符groupBy
*/
fun listOperatorGroupBy(){
val people = listOf(Person2("Bob",24),
Person2("Alice",42),
Person2("Ervin",34),
Person2("Anny",34))

people.groupBy { it.age }

//每一组都被保存成一个列表。所以结果的类型为 Map<Int, List<Person>> 。
// 你可以使用像 mapKeys 和 mapValues 这样的函数对这个映射做更多的修改。
print(people.groupBy { it.age }[34])

}

groupBy之后返回结果是一个Map集合,最后print(people.groupBy { it.age }[34])中[34]其实是输出以34为key的Person。

map和flatMap

这两个操作符在RxJava中是使用频率很高的操作符,map是对一个基本类型做一个转换,例如string -> int,而flagMap则是将上游的observerable变为另外一个observerable。Kotlin中,map其实也是类型转换的意思,flatmap则是将集合中每个元素映射(map),然后把多个列表合并成一个。最终,其实它是一个集合

flatMap 函数做了两件事:首先它根据作为参数而给定的函数把每一个元素都变换(或映射)到一个集合中。然后它把多个列表合并为一个。有一个处理字符串的案例很好的解析了这个概念

1
2
3
4
5
6
7
8
9
10
11
12

/**
* list操作符flatMap
*/

fun listOperatorMap(){

val people = listOf(Person2("Bob",24), Person2("Alice",42),Person2("Ervin",34))
print(people.map { it.name + "'s" }) //[Bob's, Alice's, Ervin's]
print(people.flatMap { it.name.toList() }) //[B, o, b, A, l, i, c, e, E, r, v, i, n]

}

sequence

  • 集合的序列操作,这个是很有用的东西。一般在集合的链式调用中会生成很多的中间集合来存放过程中的临时变量,当集合中元素过多,则明显会影响性能,这时候使用序列会是一种好的选择,举个例子:

    NOTE 注意 一般来说,无论何时,你在大型集合中有链式操作时,请使用序列。在8.2一节,我们将会讨论为什么在Kotlin中,常规集合的延迟操作是高效的,尽管它会创建中间的集合。但是如果集合包含大量的元素,中间的元素重拍耗时巨大,所以延迟计算更 加可取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* list的序列操作
*
* 一般在集合的链式调用中会生成很多的中间集合来存放过程中的临时变量,当集合中元素过多,则明显会影响性能,这时候使用序列会是一种好的选择
*/

fun listOperatorSequence(){

val people = listOf(Person2("Bob",24), Person2("Alice",42),Person2("Ervin",34))

//集合的链式调用
/**
* Kotlin标准库参考(文档)指出, filter 和 map 都返回一个列表。这意味着这个链式调用将会创建两个列表:一个保存 filter 函数的结果,另一个存储 map 函数的结果。
* 当原来的列表只有包含两个元素时,这不会有问题。但是如果你有百万个元素时,这会变得非常低效。 为了把它变得更加高效,你可以转换这个操作。
*/
people.map(Person2::name).filter { it.contains('A') }

//序列的操作
/**
* 没有保存元素的中间集合,对于元素比较大的集合来说,性能会有客观的改善
*/
people.asSequence() //初始集合转化为序列
.map(Person2::name)
.filter { it.contains('A') }
.toList() //序列转化为集合
}

集合本身的操作是实时的,而序列的操作是懒加载式的,下面会详细说明序列的中间操作和最终操作

  • 序列的中间和最终操作

看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* list的序列操作之不同点
*/

fun listOperatorSequence1(){

//sequence的操作
//map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)
listOf( 1 , 2 , 3 , 4 )
.asSequence()
.map { print( "map($it) " ); it * it }
.filter { print( "filter($it) " ); it % 2 == 0 }
.toList()//最终操作


//集合本身操作
//map(1) map(2) map(3) map(4) filter(1) filter(4) filter(9) filter(16)
listOf( 1 , 2 , 3 , 4 )
.map { print( "map($it) " ); it * it }
.filter { print( "filter($it) " ); it % 2 == 0 }
}

注意看sequence结果:map和filter是交替进行的,每个元素都是先map再filter,这证明了sequence是延迟计算的。而集合本身则是所有元素先map(中间集合),再用中间集合去filter。sequence中没有最后的toList(最终操作),则什么都不会输出。

最终操作导致所有的延迟计算都被执行了。 还有一个更重要的事要注意,在这个例子中,计算的执行顺序。原始的方法首先将会对每个元素调用 map 函数,然后对结果序列中的每个元素调用 filter 函数。这就是 map 和 filter 在集合上如何工作的。但序列并不是这样的。对于序列来说,所有的操作都会逐个应用于每个元素:处理完第一个元素(映射,然后过滤),然后处理第二个,以此类推。 这个方法意味着如果过早获取结果,某些元素根本不会被变换。我们来看一个有 map 和 find 操作的例子。首先,你把一个数映射为它的平方,之后你查找当中第一个大于3的元素(find操作符的作用):

1
2
3
4
5
6
7
fun listOperatorSequence2(){

print(listOf( 1 , 2 , 3 , 4 )
.asSequence()
.map { it * it }
.find { it > 3 })
}

如果同样的操作应用于一个集合而不是序列,那么首先会计算 map 的结果,变换初始集合中所有的元素。第二步,在中间集合中发现一个满足预言的元素。使用序列,惰性方法意味着你可以提阿偶偶处理某些元素。下图解释了(使用集合)提前和延迟(使用序列)方式执行这份代码的不同点。

集合提前计算对整个集合运行每一个操作,惰性求值则逐个计算(元素多的时候效率高,性能好)

再看一个例子啊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

fun listOperatorSequence2(){

println(listOf( 1 , 2 , 3 , 4 )
.asSequence()
.map { it * it }
.find { it > 3 })

val people = listOf(Person2("Bob",24),
Person2("Alice",42),
Person2("Ervin",34),
Person2("Dan",34)
)

/**
* 先filter有助于减少变换的总次数,
* 如果先进行 map ,每个元素都会进行变换。
* 但是如果你先进行 filter ,不合适的元素会尽快过滤掉,而且不会进行变换。
*/
//先map再过滤
println(people.asSequence().map(Person2::name).filter{ it.length < 4}.toList())
//println(people.asSequence().map{ person: Person2 -> person.name}.filter { it.length < 4 }.toList())

//先过滤再映射
println(people.asSequence().filter{it.name.length < 4}.map(Person2::name).toList())

}

先filter有助于减少变换的总次数。如果先进行 map ,每个元素都会进行变换。但是如果你先进行 filter ,不合适的元素会尽快过滤掉,而且不会进行变换。

流 vs 序列 如果你熟悉Java 8的流,你将会看到,(Kotlin的)序列是完全一样的概念。由于Java 8的流在使用旧版本的Java搭建的平台中无法使用,比如Android,所以Kotlin提供了它自己的轮子。如果你把Java 8作为目标平台,流会给你带来一个很大的好处。但是,Kotlin的集合与序列并未实现在多个CPU上并行执行流操作( map() 或者 filter() )的能力。你可以基于你面向的Java版本和你的具体要求来选择流和序列。

实例代码