Kotlin集合库的设计总结(部分)

一、集合的继承关系

Iterable为Kotlin集合库的顶层接口。

每一个集合分为两种,一种为带Mutable前缀的,另一种则是不带的。比如常见的列表分为MutableList和List,List实现了Collection接口,MutableList实现了MutableCollection和 List接口,MutableList表示可变的 List,而List表示只读List。其实Kotlin集合都是以 Java的集合库为基础来构建的,只是Kotlin通过扩展函数增强了它。

Kotlin中集合到继承关系如下图:

1.List

List表示一个有序的可重复的列表,其中元素的存储方式是线性存储的,以保证元素的有序性,另外,List中的元素是可以重复的。

fun main() {
    val listOf = listOf<Int>(1, 2, 3, 4, 4, 3, 3, 6)
    println(listOf)
}

2.Set

Set表示一个不可重复的集合。Set常用的具体实现方式有两种,分别为HashSet和TreeSet。HashSet是用散列来存放数据的,不能保证元素的有序性。而TreeSet的底层结构是二叉树,它能保证元素的有序性。在不指定Set的具体实现时,一般说Set是无序的。Set中的元素不能重复。

fun main() {
    val set = setOf<Int>(1, 2, 3, 4, 4, 5)
    println(set)//Set会将重复的元素过滤掉
}
[1, 2, 3, 4, 5]

3.Map

Kotlin中的Map与其他集合有点不同,它没有实现Iterable或者Collection。Map用来表示键值对元素集合。

val mapOf = mapOf(1 to 1, 2 to 2, 3 to 3)
println(mapOf)
{1=1, 2=2, 3=3}

注意了:Map 中的键值对,键是不能重复的

二、可变集合与只读集合

1.1可变集合

可以改变的集合,可变集合都有一个前缀修饰——“Mutable”,比如MutableList。这里的改变是指集合中的元素。

val mutableListOf = mutableListOf(1, 2, 3, 4, 5)
mutableListOf[0] = 0
println(mutableListOf)
[0, 2, 3, 4, 5]

1.2只读集合

只读集合中的元素一般情况下是不可修改的。

val listOf = listOf(1, 2, 3, 4, 5)
listOf[0]=1 // 这是错误写法

上面的方法中实际调用的是set方法,但是Kotlin的只读集合中没有这个方法,所以不能修改其中的值。其实可以发现,Kotlin中将可变集合中的修改、添加、删除等方法移除去后,原来的可变集合就变成了只读集合。

在Kotlin中,我们将List称为只读列表而不是不可变列表是有原因的。因为在某些情况下只读列表确实是可以改变的。

val writeList: MutableList<Int> = mutableListOf(1, 2, 3, 4)
val readList: List<Int> = writeList
writeList[0] = 0
println(readList)

[0, 2, 3, 4]

在上面代码中,首先定义了一个可变列表 writeList,然后定义一个人只读列表readList,该列表与writeList指向了同一个集合对象,因为MutableList 是List的子集,所以可以这样做。上面代码可以发现writeList发生改变后,readList也发生了改变,在这种情况下确实修改了只读集合,所以只能说只读类表在某些情况下是安全的,但却不是总是安全的。

另一种情况下,只读集合也是可以被修改的。

Kotlin的集合都是基于Java来进行构建的,并且Kotlin与java是兼容的。这意味着我们在Kotlin的集合操作中,调用java 中定义的方法。又因为Java中是不区分只读集合与可变集合的。所以可以通过一个java方法改变只读集合。

例子:

public class JavaUtils {
    public static List<Integer> payFoo(List<Integer> list) {
        for (int i = 0; i < list.size(); i++) {
            list.set(i, list.get(i) * 2);
        }
        return list;
    }
}


val readList: List<Int> = listOf(1, 2, 3, 4)
val payFoo = JavaUtils.payFoo(readList)
println(payFoo)

[2, 4, 6, 8]

由于Java不区分只读集合与可变集合,所以在Java与Kotlin互相操作的时候要考虑到这种情况。

三、惰性集合

1.通过序列提高开发效率

例:

fun main() {
    val list = listOf<Int>(1, 2, 3, 4, 5)
    val map = list.filter {
        it > 2
    }.map {
        it * 2
    }
    println(map)
}

[6, 8, 10]

 当上面的list的集合元素非常多的时候(超过10万),上面的操作在处理集合的时候就会比较低效。

产生这种情况的原因:

上面的filter方法与map方法都会返回一个新的集合,也就是说上面的操作会产生两个临时集合,因为list会先调用filter方法,然后产生的集合会再次调用map方法,如果此时list中的元素非常多,这将会是一笔不小的开销。

使用序列来处理上面的问题

val toList = list.asSequence().filter { it > 2 }.map { it * 2 }.toList()
println(toList)

[6, 8, 10]

首先通过asSequence()方法将一个列表转化为序列,然后在这个序列上进行相应的操作,最后通过toList()方法将序列转换为列表。将list转换为序列,在很大程度上就提高了上面操作集合的效率。这是因为在使用序列的时候,filter方法和map方法的操作都没有创建额外的集合,这样当集合中元素数量巨大的时候,就减少了大部分的开销。

在Kotlin中,序列中元素的求值是惰性的,这就意味着在利用序列进行链式求值的时候,不需要像操作普通集合那样,每次进行一次求值操作,就产生一个新的集合保存中间数据。

在编程语言理论中,惰性求值表示一种在需要时才进行求值的计算方式。在使用惰性求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用时才去求值。通过这种方式,不仅能得到性能上的提升,还有一个重要的好处是它可以构造出一个无限的数据类型。所以惰性求值有两个好处,一个是性能优化,另一个就是能够构造出无限的数据类型。

序列的操作方式

序列中元素的求值方式采用惰性求值。那么惰性求值在序列中如何体现的呢?

例子:

val toList = list.asSequence().filter { it > 2 }.map {
    it * 2
}.toList()

上面的代码中主要执行了两类操作。

第一类:

filter { it > 2 }.map { it * 2 }

 filter 和map的操作返回的都是序列,我们称这类操作为中间操作

第二类:

toList()

这类操作将序列转换为了list,称为末端操作。

1.中间操作

在对普通集合进行链式操作的时候,有些操作会产生中间集合,当用这类操作来对序列进行求值的时候,它们就被称为中间操作,比如上面的filter和map。每一次中间操作返回的都是一个序列,产生的新序列内部知道如何去变换为原来序列中的元素。中间操作都是采用惰性求值的。

val list = listOf<Int>(1, 2, 3, 4, 5)
list.asSequence().filter {
    println("filter{$it}")
    it > 2
}.map {
    println("map{$it}")
    it * 2
}

从上面的代码可以发现:上面的printlin方法根本没雨被执行,说明filter 方法和map方法的执行被延迟了,这就是惰性求值的体现。

2.末端操作

末端操作就是一个返回结果的操作,它的返回值不能是一个序列,必须是一个明确的结果。

val list = listOf<Int>(1, 2, 3, 4, 5)
list.asSequence().filter {
    println("filter{$it}")
    it > 2
}.map {
    println("map{$it}")
    it * 2
}.toList()

filter{1}
filter{2}
filter{3}
map{3}
filter{4}
map{4}
filter{5}
map{5}

 上面的代码通过toList() ,所有的中间操作都被执行了。

下面观察不使用序列的表现:

val list = listOf<Int>(1, 2, 3, 4, 5)
val map = list.filter {
    println("filter{$it}")
    it > 2
}.map {
    println("map{$it}")
    it * 2
}

filter{1}
filter{2}
filter{3}
filter{4}
filter{5}
map{3}
map{4}
map{5}

 通过对比上面的结果可以发现:普通集合在进行链式调用的时候会现在list上调用filter,然后产生一个结果列表,接下来这个map就在这个结果列表上进行操作。而序列则不同,序列在进行链式调用的时候,会将所有的操作都应用在一个元素上,也就是说,第一个元素执行完所有的操作之后,第二个元素再去执行所有的操作。通过上面的例子发现:当使用序列的时候,如果map和filter可以互相交换的话,应该优先使用filter,这样可以减少一部分开销。

2.序列可以是无限的

惰性求值最大好处是可以构造出一个无限的数据类型。

例:自然数数列就是一个无限的数列。

val generateSequence = generateSequence(0) { it + 1 }
val toList = generateSequence.takeWhile { it <= 10 }.toList()
println(toList)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

对于无限数列,我们不能将一个无限的数据结构通过穷举的方式呈现出来,而只是实现一种表示无限的状态,让我们使用

时感觉它就是无限的。

参考Kotlin核心编程

 

 

 

 

 

 

 

发布了179 篇原创文章 · 获赞 175 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/zhangying1994/article/details/104586945