Kotlin学习笔记16——集合概述

前言

上一篇我们学习了Kotlin中的委托,今天继续来学习Kotlin中的集合。集合的内容包含的比较多,分为三篇来学习,今天是学习的集合概述,包括集合分类,定义,以及简单使用。

基本集合类型

Kotlin 标准库提供了基本集合类型的实现: set、list 以及 map。 一对接口代表每种集合类型,其中每种集合类型包含了两种实现:

  • 只读集合 (List,Map,Set)
  • 可变集合(MutableList,MutableMap,MutableSet)

请注意,更改可变集合不需要它是以 var 定义的变量:写操作修改同一个可变集合对象,因此引用不会改变。如下代码所示:

val numbers = mutableListOf("one", "two", "three", "four") //用val修饰的可变集合
numbers.add("five")   // 这是可以的

只读集合类型是型变的。 这意味着,如果类 Rectangle 继承自 Shape,则可以在需要 List <Shape> 的任何地方使用 List <Rectangle>。 换句话说,集合类型与元素类型具有相同的子类型关系。 map 在值(value)类型上是型变的,但在键(key)类型上不是。但是可变集合不是型变的;否则将导致运行时故障。

为了对Kotlin中基本集合有个更直观了解,我们可以看下Kotlin 基本集合接口的图表:
在这里插入图片描述

Collection

Collection<T> 是集合层次结构的根。此接口表示一个只读集合的共同行为:检索大小、检测是否为成员等等。 Collection 继承自 Iterable 接口,它定义了迭代元素的操作。可以使用 Collection 作为适用于不同集合类型的函数的参数。但是从源码中我们可以看到Collection是一个接口,所以具体使用集合时请使用 Collection 的继承者: List 与 Set。

...
public interface Collection<out E> : Iterable<E> {
	//...
}
...

同样MutableCollection也是一个接口类型,但是除了具有Collection的特点,还具有add和remove等写操作。

List

List<T> 以指定的顺序存储元素,并提供使用索引访问元素的方法。索引从 0 开始 – 第一个元素的索引 – 直到 最后一个元素的索引 即 (list.size - 1)。定义和常用用法如下:

//声明一个只读list
val numbers = listOf("one", "two", "three", "four")
//或者可以加入泛型声明
//val numbers = listOf<String>("one", "two", "three", "four")
//或者可以接收一个大小用来初始化
//val numbers = List(3, { it * 2 })  // 如果你想操作这个集合,应使用 MutableList
println(doubled)
println("Number of elements: ${numbers.size}")//list的大小
println("Third element: ${numbers.get(2)}")//获取第三个元素
println("Fourth element: ${numbers[3]}")//获取第四个元素
println("Index of element \"two\" ${numbers.indexOf("two")}")//通过元素获取元素下标

结果:

Number of elements: 4
Third element: three
Fourth element: four
Index of element "two" 1

List 元素(包括空值)可以重复:List 可以包含任意数量的相同对象或单个对象的出现。 如果两个 List 在相同的位置具有相同大小和相同结构的元素,则认为它们是相等的(需要注意的是这里说的相等时值相等,并不是引用地址相等)。

val bob = Person("Bob", 31)
val people = listOf<Person>(Person("Adam", 20), bob, bob)
val people2 = listOf<Person>(Person("Adam", 20), Person("Bob", 31), bob)
println(people == people2)
bob.age = 32
println(people == people2)

打印结果:

true
false

MutableList

MutableList 是可以进行写操作的 List,例如用于在特定位置添加或删除元素。在 Kotlin 中,List 的默认实现是 ArrayList,可以将其视为可调整大小的数组。关于对MutableList的写操作我们放到后面再进行统一学习。这里只给出定义:

//不指定size
val mutableList = mutableListOf<String>()
//或者指定size
val mutableList = mutableListOf(5)

Set

Set<T> 存储唯一的元素;null 元素也是唯一的:一个 Set 只能包含一个 null。当两个 set 具有相同的大小并且对于一个 set 中的每个元素都能在另一个 set 中存在相同元素,则两个 set 相等。

//只读set定义,同样可以通过泛型来定义,这里将不做展示
val numbers = setOf(1, 2, 3, 4)
println("Number of elements: ${numbers.size}")
if (numbers.contains(1)) println("1 is in the set")

val numbersBackwards = setOf(4, 3, 2, 1)
println("The sets are equal: ${numbers == numbersBackwards}")

打印结果:

Number of elements: 4
1 is in the set
The sets are equal: true

Set的默认实现 - LinkedHashSet – 保留元素插入的顺序。 因此,依赖于顺序的函数,例如 first() 或 last(),会在这些 set 上返回可预测的结果。

val numbers = setOf(1, 2, 3, 4)  // LinkedHashSet is the default implementation
val numbersBackwards = setOf(4, 3, 2, 1)

println(numbers.first() == numbersBackwards.first()) //第一个是1,第二个是4,不相等结果为false
println(numbers.first() == numbersBackwards.last())//第一个是1,第二个是1,相等结果为true

另一种实现方式 – HashSet – 不声明元素的顺序,所以在它上面调用这些函数会返回不可预测的结果。但是,HashSet 只需要较少的内存来存储相同数量的元素。

MutableSet

MutableSet 是一个带有来自 MutableCollection 的写操作接口的 Set。例如用于在特定位置添加或删除元素。关于对MutableSet 的写操作我们放到后面再进行统一学习。这里只给出定义:

//不指定size
val mutableSet = mutableSetOf<String>()
//指定size
val mutableSet = mutableSetOf(5)

Map

Map<K, V> 不是 Collection 接口的继承者;但是它也是 Kotlin 的一种集合类型。 Map 存储 键-值 对(或 条目);键是唯一的,但是不同的键可以与相同的值配对。Map 接口提供特定的函数进行通过键访问值、搜索键和值等操作。

//这里通过中缀表达式来定义只读map
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)
println("All keys: ${numbersMap.keys}")//打印key集合
println("All values: ${numbersMap.values}")//打印value结合

打印结果:

All keys: [key1, key2, key3, key4]
All values: [1, 2, 3, 1]

需要注意的是无论键值对的顺序如何,包含相同键值对的两个 Map 是相等的。

MutableMap

MutableMap 是一个具有写操作的 Map 接口,可以使用该接口添加一个新的键值对或更新给定键的值。关于对MutableMap 的写操作我们放到后面再进行统一学习。这里只给出定义:

//不指定size
val mutableMap = mutableMapOf<String,String>()
//指定初始元素
val numbersMap = mutableMapOf("one" to 1, "two" to 2)

空集合

还有用于创建没有任何元素的集合的函数:emptyList()、emptySet() 与 emptyMap()。 创建空集合时,应指定集合将包含的元素类型。

//空集合示例
val emptyList = emptyList<String>()
val emptySet = emptySet<String>()
val emptyMap = emptyMap<String,String>()

复制

在特定时刻通过集合复制函数,例如toList()、toMutableList()、toSet() 等等。创建了集合的快照。 结果是创建了一个具有相同元素的新集合 如果在源集合中添加或删除元素,则不会影响副本。副本也可以独立于源集合进行更改。

val sourceList = mutableListOf(1, 2, 3)
val copyList = sourceList.toMutableList()
val readOnlyCopyList = sourceList.toList()
sourceList.add(4)
println("Copy size: ${copyList.size}")  //3,向原List中添加元素,并不能影响通过复制函数得到的副本
println("Read-only copy size: ${readOnlyCopyList.size}") //4

迭代器

Iterable<T> 接口的继承者(包括 Set 与 List)可以通过调用 iterator() 函数获得迭代器。 一旦获得迭代器它就指向集合的第一个元素;调用 next() 函数将返回此元素,并将迭代器指向下一个元素(如果下一个元素存在)。 一旦迭代器通过了最后一个元素,它就不能再用于检索元素;也无法重新指向到以前的任何位置。要再次遍历集合,请创建一个新的迭代器。

val numbers = listOf("one", "two", "three", "four")
val numbersIterator = numbers.iterator()
while (numbersIterator.hasNext()) {
    println(numbersIterator.next())
}

你还可以使用for循环遍历:

val numbers = listOf("one", "two", "three", "four")
for (item in numbers) {
    println(item)
}

或者forEach()函数遍历:

val numbers = listOf("one", "two", "three", "four")
numbers.forEach {
    println(it)
}

List 迭代器

对于列表,有一个特殊的迭代器实现: ListIterator 它支持列表双向迭代:正向与反向。 反向迭代由 hasPrevious() 和 previous() 函数实现。 此外, ListIterator 通过 nextIndex() 与 previousIndex() 函数提供有关元素索引的信息。

val numbers = listOf("one", "two", "three", "four")
val listIterator = numbers.listIterator()
println("Iterating nextwards:")
while (listIterator.hasNext()) {
    print("Index: ${listIterator.nextIndex()}")
    println(", value: ${listIterator.next()}")
}
println("Iterating backwards:")
while (listIterator.hasPrevious()) {
    print("Index: ${listIterator.previousIndex()}")
    println(", value: ${listIterator.previous()}")
}

打印结果:

Iterating nextwards:
Index: 0, value: one
Index: 1, value: two
Index: 2, value: three
Index: 3, value: four
Iterating backwards:
Index: 3, value: four
Index: 2, value: three
Index: 1, value: two
Index: 0, value: one

可变迭代器

为了迭代可变集合,于是有了 MutableIterator 来扩展 Iterator 使其具有元素删除函数 remove() 。因此,可以在迭代时从集合中删除元素。

val numbers = mutableListOf("one", "two", "three", "four") 
val mutableIterator = numbers.iterator()
mutableIterator.next()
mutableIterator.remove()    
println("After removal: $numbers")

打印结果:

After removal: [two, three, four]

除了删除元素, MutableListIterator 还可以在迭代列表时插入和替换元素。

val numbers = mutableListOf("one", "four", "four") 
val mutableListIterator = numbers.listIterator()

mutableListIterator.next()
mutableListIterator.add("two")
mutableListIterator.next()
mutableListIterator.set("three")   
println(numbers)

打印结果:

[one, two, three, four]

区间和数列

区间

区间实现了一个公共接口:ClosedRange<T>,表示一个数学意义上的闭区间,有两个端点start和end都包含在区间内,主要操作是contains,通常以in和!in操作符形式使用。

要为类创建一个区间,请在区间起始值上调用 rangeTo() 函数,并提供结束值作为参数。 rangeTo() 通常以操作符 … 形式调用。

//一个简单区间定义
val range = 1..100
println(range.contains(1))//true
println(10 in range)//true

//或者
val versionRange = Version(1, 11)..Version(1, 30)
println(Version(0, 9) in versionRange) //false
println(Version(1, 20) in versionRange) //true

数列

数列具有三个基本属性:first 元素、last 元素和一个非零的 step。 首个元素为 first,后续元素是前一个元素加上一个 step。 以确定的步长在数列上进行迭代等效于 Java中基于索引的 for 循环。

for (int i = first; i <= last; i += step) {
  // ……
}

通过迭代数列隐式创建区间时,此数列的 first 与 last 元素是区间的端点,step 为 1 。

for (i in 1..10) print(i)

要指定数列步长,请在区间上使用 step 函数。

for (i in 1..8 step 2) print(i)

数列的 last 元素是这样计算的:

  • 对于正步长:不大于结束值且满足 (last - first) % step == 0 的最大值。
  • 对于负步长:不小于结束值且满足 (last - first) % step == 0 的最小值。

因此,last 元素并非总与指定的结束值相同。

实用函数

  • 升序区间:rangeTo()
  • 降序区间:downTo()
  • 翻转区间:reversed()
  • 尾部不闭合:until()
  • 步长:step()
var ascRange = 0.rangeTo(10)  //相当于 0..10
var desRange = 10.downTo(0)  //相当于 10..0
desRange = desRange.reversed()    //翻转区间等同于ascRange
var dwonRange = 10.downTo(0).step(3) 
for(r in dwonRange){ //输出结果:10,7,4,1
   println(r)
}
println("-----------------")
var untilRange = 0.until(5)
for(r in untilRange ){//输出结果:0,1,2,3,4
   println(r)
}

也可以用中缀表达式:

for (i in 4 downTo 1) print(i)
for (i in 1..8 step 2) print(i)
for (i in 0 until 10)print(i)

reversed()函数不支持中缀表达式

序列

首先我们来了解一下什么是序列,序列其实类似集合的一个接口,只不过它的操作都是惰性集合操作,所有在集合上的操作符都适用于序列。既然所有在集合上的操作符都适用于序列,为啥还要弄出一个序列,它们有什么区别了?

  • 序列的操作都是惰性集合操作
  • 执行的顺序也不同

怎么理解第一点了?

当 Iterable 的处理包含多个步骤时,它们会优先执行:每个处理步骤完成并返回其结果——中间集合。 在此集合上执行以下步骤。反过来,序列的多步处理在可能的情况下会延迟执行:仅当请求整个处理链的结果时才进行实际计算。

怎么理解第二点了?

Sequence 对每个元素逐个执行所有处理步骤。 反过来,Iterable 完成整个集合的每个步骤,然后进行下一步。

因此,这些序列可避免生成中间步骤的结果,从而提高了整个集合处理链的性能。 但是,序列的延迟性质增加了一些开销,这些开销在处理较小的集合或进行更简单的计算时可能很重要。 因此,应该同时考虑使用 Sequence 与 Iterable,并确定在哪种情况更适合。

构造序列

//由元素
val numbersSequence = sequenceOf("four", "three", "two", "one")
//由集合(Iterable)
val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()
//由函数generateSequence()
val oddNumbers = generateSequence(1) { it + 2 } // `it` 是上一个元素
println(oddNumbers.take(5).toList())
//由组块
val oddNumbers = sequence {
    yield(1)
    yieldAll(listOf(3, 5))
    yieldAll(generateSequence(7) { it + 2 })
}
println(oddNumbers.take(5).toList())

使用

那么什么时候我们应该用序列了?举个栗子:

我们拿到了100W条用户数据,需要将这些用户年龄是偶数的姓名全打印出来。如果不使用序列,我们的做法为:

println(userList
   .filter { it.age % 2 != 0 }
   .map { User::age })

这个写法肯定没有错,对于少量数据来说也没什么影响,可是这里大家注意,是100W条数据,我们可以点击map和filter查看源码,每一步操作都会生成另外一个集合,对于大量数据来说,这可是一笔很大的消耗,在性能上是很不理想的。这时候就到了我们序列大显身手的时候了,来看看序列是如何减少性能消耗的:

println(userList.asSequence()
   .filter { it.age % 2 != 0 }
   .map { User::age }
   .toList())

这里我们先将集合转换为序列,在最后的操作中才将序列转换为集合的。序列在中间操作都是惰性的,不会创建额外的集合来保存过程中产生的中间结果,使用序列可以高效的对集合元素执行链式操作。

尾巴

今天的学习笔记就先到这里了,下篇我们将继续学习Kotlin中的集合公共操作
老规矩,喜欢我的文章,欢迎素质三连:点赞,评论,关注,谢谢大家!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值