Kotlin容器,泛型及高阶函数应用

1.容器类型
Kotlin号称全面兼容Java,于是乎Java的容器类仍可在Kotlin中正常使用,包括大家熟悉的队列ArrayList、映射HashMap等等。不过Kotlin作为一门全新的语言,肯定还是要有自己的容器类,不然哪天Java跟Kotlin划清界限,那麻烦就大了。与Java类似,Kotlin也拥有三类基本的容器,分别是集合Set、队列List、映射Map,然后每类容器又分作只读与可变两种类型,这是为了判断该容器能否进行增删改等变更操作。Kotlin对修改操作很慎重,比如变量用val前缀表示不可修改,用var前缀表示允许修改;类默认是不允许继承的,只有添加open前缀才允许该类被继承;至于容器默认为只读容器,如果需要进行修改则需加上Mutable形成新的容器,比如MutableSet表示可变集合,MutableList表示可变队列,MutableMap表示可变映射。

既然Set/List/Map都属于容器,那么必定拥有相同的基本容器方法,具体说明如下:

isEmpty() : 判断该容器是否为空。
isNotEmpty() : 判断该容器是否非空。
clear() : 清空该容器。
contains() : 判断该容器是否包含指定元素。
iterator() : 获取该容器的迭代器。
count() : 获取该容器包含的元素个数,也可通过size属性获得元素数量。

初始化赋值 : Kotlin允许在声明容器变量之时进行初始赋值,这点很方便比Java先进,当然不同容器的初始化方法有所区别,具体的对应关系见下表:

名称:                  初始化方法:
只读集合 Set           setOf()
可变集合 MutableSet    mutableSetOf()
只读队列 List          listOf()
可变队列 MutableList   mutableListOf()
只读映射 Map           mapOf()
可变映射 MutableMap    mutableMapOf()

以上是Kotlin容器的基本方法,更具体的增删改查等用法则有所不同,下面分别介绍这三类六种容器的详细用法。

2.List类型
List是一种元素之间按照顺序排列的容器,它与集合的最大区别,便是多了个次序管理。正因为List建立了秩序规则,所以它比Set多提供了如下功能(注意凡是涉及到增删改的,都必须由MutableList来完成):
1、队列的get方法能够获取指定位置的元素,也可直接通过下标获得该位置的元素。
2、MutableList的add方法每次都是把元素添加到队列末尾,也可指定添加的位置;
3、MutableList的set方法允许替换或者修改指定位置的元素;
4、MutableList的removeAt方法允许删除指定位置的元素;
5、MutableList提供了sort系列方法用于给队列中的元素重新排序,其中sortBy方法表示按照升序排列,sortByDescending方法表示按照降序排列;

//空值判断
List<Int>?:List 本身可空,List 的元素不可空
List<Int?> :List 本身不可空,List 的元素可空
List<Int?>? :List 本身可空,List 的元素也可空

对于List的遍历操作:for循环,迭代器循环,forEach循环,以及下标遍历。用法如下:

fun main(args: Array<String>) {
val mylist:MutableList<String> = mutableListOf("联想","联想 Y520","惠普","戴尔","IBM","长城","方正","联想 Y720","联想 Y7000");
print("电脑畅销榜已添加,当前共有${mylist.size}款电脑\n")

print("for-in 电脑畅销榜包含以下${mylist.size}款电脑\n")
for(item in mylist){
print("名称:${item}\n")
}

//删除第二个元素
if(mylist.size>=2){
mylist.removeAt(1);
}
print("removeAt电脑畅销榜已更新,当前包含以下${mylist.size}款电脑\n")
print("for-in 电脑畅销榜包含以下${mylist.size}款电脑\n")
for(item in mylist){
print("名称:${item}\n")
}

mylist.sortBy { it.first() }
print("for-in sort电脑畅销榜已按照it升序重新排列,包含以下${mylist.size}款电脑\n")
for(item in mylist){
print("名称:${item}\n")
}

mylist.sortByDescending { it.first() }
print("for-in sort电脑畅销榜已按照it降序重新排列,包含以下${mylist.size}款电脑\n")
for(item in mylist){
print("名称:${item}\n")
}

print("iterator-while 电脑畅销榜包含以下${mylist.size}款电脑\n")
var iter = mylist.iterator()
while(iter.hasNext()){
print("名称:${iter.next()}\n")
}

print("foreach 电脑畅销榜包含以下${mylist.size}款电脑\n")
mylist.forEach {
print("名称:${it}\n")
}

print("indices电脑畅销榜包含以下${mylist.size}款电脑\n")
for(i in mylist.indices){
print("名称:${mylist[i]}\n")
}
}

3.Set类型
Set集合是一种简单的容器,它具有以下特性:
1、容器内部的元素不按顺序排列,因此无法按照下标进行访问;
2、容器内部的元素存在唯一性,通过哈希值校验是否存在相同的元素,如果存在则覆盖之;
因为Set是只读集合,初始化赋值后便不可更改,所以元素变更的方法只适用于可变集合MutableSet,但MutableSet的变更操作尚有以下限制:
1、MutableSet的add方法仅仅往集合中添加元素,由于集合是无序的,因此不知道添加的具体位置;
2、MutableSet没有修改元素值的方法,一个元素一旦被添加,就不可被修改;
3、MutableSet的remove方法用于删除指定对象,但无法删除某个位置的元素,这是因为集合内的元素不是按顺序排列的;

对于Set的遍历操作:for循环,迭代器循环,forEach循环,三种循环遍历的用法如下:

private val goodsA:String="惠普"
private val goodsB:String="联想"
private val goodsC:String="戴尔"
private val goodsD:String="IBM"
private val goodsE:String="长城"
private val goodsF:String="方正"
private val myset:MutableSet<String> = mutableSetOf();
fun main(args: Array<String>) {
myset.add(goodsA)
myset.add(goodsB)
myset.add(goodsC)
myset.add(goodsD)
myset.add(goodsE)
myset.add(goodsF)
myset.remove(goodsA)
print("电脑畅销榜已添加,并且remove goodsA商品惠普,当前共有${myset.size}款电脑\n")
print("for-in 电脑畅销榜包含以下${myset.size}款电脑\n")
print("for-in测试\n")
for(item in myset){
print(item+"\n")
}

print("iterator-while 电脑畅销榜包含以下${myset.size}款电脑\n")
print("iterator测试\n")
var iter = myset.iterator()
while(iter.hasNext()){
print(iter.next()+"\n")
}

print("foreach 电脑畅销榜包含以下${myset.size}款电脑\n")
print("foreach测试\n")
myset.forEach {
print(it+"\n")
}

print("foreach\$拼接表达式电脑畅销榜包含以下${myset.size}款电脑\n")
var dec=""
myset.forEach {
dec="${dec}名称:${it}\n"
}
print("$dec")
}

4.Map类型
映射内部保存的是一组键值对(Key-Value),也就是说,每个元素都由两部分构成,第一部分是元素的键,相当于元素的名字;第二部分是元素的值,存放着元素的详细信息。元素的键与值是一一对应的关系,相同的键名指向的值对象是唯一的,所以映射中每个元素的键名各不相同,这个特性使得映射的变更操作与队列存在以下不同之处(注意增删操作必须由MutableMap来完成):
1、映射的containsKey方法判断是否存在指定键名的元素,containsValue方法判断是否存在指定值对象的元素;
2、MutableMap的put方法不单单是添加元素,而是智能的数据存储;每次调用put方法,映射会先根据键名寻找同名元素,如果找不到就添加新元素,如果找得到就用新元素替换旧元素;
3、MutableMap的remove方法,是通过键名来删除元素的;
4、调用mapOf和mutableMapOf方法初始化映射之时,有两种方式可以表达单个键值对元素。其一是采取“键名 to 值对象”的形式,其二是采取Pair配对方式形如“Pair(键名, 值对象)”,下面是这两种初始化方式的代码例子:

//to方式初始化映射
var goodsMap = mapOf("苹果" to goodsA, "华为" to goodsB, "小米" to goodsC, "欧珀" to goodsD, "步步高" to goodsE, "魅族" to goodsF)
//Pair方式初始化映射
var goodsMutMap = mutableMapOf(Pair("苹果", goodsA), Pair("华为", goodsB), Pair("小米", goodsC), Pair("欧珀", goodsD), Pair("步步高", goodsE), Pair("魅族", goodsF))

遍历方式与Set相似,如下
fun main(args: Array<String>) {
var goodsMutMap=mutableMapOf<String, String>(Pair("苹果", goodsA),Pair("华为", goodsB),Pair("戴尔", goodsC),Pair("华硕", goodsD),Pair("弘基",goodsE),Pair("联想", goodsE));
for(item in goodsMutMap){
print("厂家:${item.key},名称:${item.value}\n");
}
//迭代器循环
var iterator= goodsMutMap.iterator();
while(iterator.hasNext()){
var item=iterator.next();
print("厂家:${item.key}名称:${item.value}\n")
}
//for each循环
goodsMutMap.forEach{
print("厂家:${it.key},名称:${it.value}\n")
}
}

5.泛型函数
泛型的引入:函数的输入参数类型必须在定义函数时就要指定,可是有时候参数类型是不确定的,只有在函数调用时方能知晓具体类型,如此一来要怎样声明函数呢?
定义泛型函数时,得在函数名称前面添加“<T>”,表示以T声明的参数(包括输入参数和输出参数),其参数类型必须在函数调用时指定。
实例:

fun <T> appendString(tag:String, var otherInfo:T?):String{
var str:String = "$tag:"
for (item in otherInfo){
str="$str${item.toString()}"
}
return str
}
fun main(args: Array<String>){
var count = 0
when(count%3){
0 -> appendString<String>("古代的四大发明","造纸术","印刷术","火药","指南针")
1 -> appendString<Int>("小于10的素数",2,3,5,7)
else -> appendString<Double>("烧钱的日子",5.20,6.18,11.11,12.12)
}
}

6.内联函数
注意到前面定义泛型函数appendString,是把它作为一个全局函数,也就是在类外面定义,不在类内部定义。因为类的成员函数依赖于类,只有泛型类(又称模板类)才能拥有成员泛型函数,普通类是不允许定义泛型函数的,否则编译器会直接报错。不过有个例外情况,如果参数类型都是继承自某种类型,那么允许在定义函数时指定从这个基类泛化开,凡是继承自该基类的子类,都可以作为输入参数进行函数调用,反之则无法调用函数。
举个例子,Int、Float和Double都继承自Number,但是定义一个setArrayNumber(array:Array<Number>)函数,它并不接受Array<Int>或者Array<Double>的入参,如果要让该方法同时接受源自Number的数组入参,就得定义泛化自Number的泛型函数,即将<T>改为<reified T : Number>,同时在fun前面添加关键字inline,表示该函数也为内联函数。内联函数在编译之时,会在调用处把该函数的内部代码直接复制一份,调用十次就会复制十份,而非普通函数那样仅仅提供一个函数的访问地址。该例子的函数定义代码如下所示:

//该函数不接受Array<Int>,也不接受Array<Double>,只好沦为孤家寡人
fun setArrayNumber(array:Array<Number>) {
var str:String = "数组元素依次排列:"
for (item in array) {
str = str + item.toString() + ", "
}
tv_function_result.text = str
}

//只有内联函数才可以被具体化
inline fun <reified T : Number> setArrayStr(array:Array<T>) {
var str:String = "数组元素依次排列:"
for (item in array) {
str = str + item.toString() + ", "
}
tv_function_result.text = str
}

7.扩展函数
系统自带的类已经提供了许多方法,然而经常还是无法完全满足业务需求,此时开发者往往要写个工具类,比如StringUtil、DateUtil之类,来补充相关的处理功能,长此以往,工具类越来越多也越来越难以管理。
基于以上情况,Kotlin推出了扩展函数的概念,允许开发者给系统类补写新的方法,而无需另外编写额外的工具类。比如系统自带的数组Array提供了求最大值的max方法,提供了进行排序的sort方法,可是并未提供交换数组元素的方法。于是我们打算给Array增加新的交换方法,也就是添加一个扩展函数swap,与众不同的是要在函数名称前面加上“Array<Int>.”,表示该函数扩展自Array<Int>。swap函数的定义代码如下所示:

fun Array<Int>.swap(pos1:Int, pos2:Int){
val temp = this[pos1]
this[pos1] = this[pos2]
this[pos2] = temp
}

与泛型函数结合可以增强它的泛用性

fun <T> Array<T>.swap(pos1:Int, pos2:Int){
val temp = this[pos1]
this[pos1] = this[pos2]
this[pos2] = temp
}
实例:消除重复元素
fun main(args: Array<String>) {
var  city :MutableList<String> = mutableListOf( "Shanghai Job Offered", "Beijing Events", "Beijing Language","Beijing Massage & Escort" ,"Beijing Language","Beijing Events","Shanghai Job Offered")
var  testNum :MutableList<Int> = mutableListOf( 1,2,3,4,1,2,3,4,5)
testNum.sort()
for(item in testNum.deleteDuplication()){
println("$item")
}
city.sort()
for(item in city.deleteDuplication()){
println("$item")
}
}
//新建辅助数组
fun <T> MutableList<T>.deleteDuplication():MutableList<T>{
var res:MutableList<T> = mutableListOf()
for(i in this.indices){
if(i>0 && this[i]==this[i-1]) continue
else res.add(this[i])
}
return res
}

8.尾递归函数
Kotlin引入了扩展函数,还能反过来精简函数。具体地说,如果一个函数的表达式比较简单,一两行就可以搞定的话,Kotlin允许使用等号代替大括号。
例如:5!=5*4*3*2*1

fun factorial(n:Int):Int {
if (n <= 1) n
else n*factorial(n-1)
}

可以简化成:

fun factorial(n:Int):Int = if (n <= 1) n else n*factorial(n-1)

Kotlin体系还存在一种特殊的递归函数,名叫尾递归函数,它指的是函数末尾的返回值重复调用了自身函数。此时要在fun前面加上关键字tailrec,告诉编译器这是个尾递归函数,则编译器会相应进行优化,从而提高程序性能。

//如果函数尾部递归调用自身,则可加上关键字tailrec表示这是个尾递归函数,
//此时编译器会自动优化递归,即用循环方式代替递归,从而避免栈溢出的情况。
//比如下面这个求余弦不动点的函数就是尾递归函数
tailrec fun findFixPoint(x: Double = 1.0): Double
= if (x == Math.cos(x)) x else findFixPoint(Math.cos(x))

实例:

9.高阶函数
前面多次提到函数被Kotlin当作特殊变量,包括函数声明采取跟变量声明一样的形式“名称:类型”,以及简化函数允许直接用等号连接函数体等等,那么本节最后讲述的则是把A函数作为B函数的输入参数,就像普通变量一样参与B函数的表达式计算。此时因为B函数的入参内嵌了A函数,故而B函数被称作高阶函数,对应的A函数则为低阶函数。
实例:

//允许将函数表达式作为输入参数传进来,就形成了高阶函数,这里的greater函数就像是个变量
fun <T> maxCustom(array: Array<T>, greater: (T, T) -> Boolean): T? {
var max: T? = null
for (item in array)
if (max == null || greater(item, max))
max = item
return max
}
fun main(args: Array<String>){
var string_array:Array<String> = arrayOf("How", "do", "you", "do", "I'm   ", "Fine")
println("字符串数组的默认最大值为${string_array.max()}")
//因为高阶函数maxCustom同时也是泛型函数,所以要在函数名称后面加上<String>
println("字符串数组按长度比较的最大值为${maxCustom<String>(string_array, { a, b -> a.length > b.length })}")
//string_array.max()对应的高阶函数是maxCustom(string_array, { a, b -> a > b })
println("字符串数组的默认最大值(使用高阶函数)为${maxCustom(string_array, { a, b -> a > b })}")
//因为系统可以根据string_array判断泛型函数采用了String类型,故而函数名称后面的<String>也可以省略掉
println("字符串数组按去掉空格再比较长度的最大值为${maxCustom(string_array, { a, b -> a.trim().length > b.trim().length })}")
}

前述的高阶函数maxCustom同时结合了泛型函数的写法,其实还可以给它加上扩展函数的功能。

fun <T> Array<T>.maxCustom(array: Array<T>, greater: (T, T) -> Boolean): T? {
var max: T? = null
for (item in array)
if (max == null || greater(item, max))
max = item
return max
}

鸣谢:  Kotlin学习笔记 By Soul丶Knight

点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注