集合的创建与遍历

Kotlin没有采用它自己的集合类,而是采用标准的Java集合类。大部分Kotlin的标准库是由Java类的拓展函数组成的。

创建集合

Kotlin中对集合增加了一个新的接口MutableList,实现该接口的集合是可变集合。Kotlin中,集合分为可变集合和不可变集合。

public interface MutableList<E> : List<E>, MutableCollection<E> {

    override fun add(element: E): Boolean

    override fun remove(element: E): Boolean

    override fun addAll(elements: Collection<E>): Boolean

    public fun addAll(index: Int, elements: Collection<E>): Boolean

    override fun removeAll(elements: Collection<E>): Boolean
    override fun retainAll(elements: Collection<E>): Boolean
    override fun clear(): Unit

    public operator fun set(index: Int, element: E): E

    public fun add(index: Int, element: E): Unit

    public fun removeAt(index: Int): E

    override fun listIterator(): MutableListIterator<E>

    override fun listIterator(index: Int): MutableListIterator<E>

    override fun subList(fromIndex: Int, toIndex: Int): MutableList<E>
}

MutableList接口提供了增加和删除集合元素的能力。

创建不可变集合
val list = listOf<String>("a", "b", "c")
val letter = list[0]
var list1 = listOfNotNull<Int>(1, 4, 8)
创建可变集合
val list2 = arrayListOf<Int>(1, 2, 3, 4)
list2.set(0, 10)
list2[0] = 10
list2.add(5)
println("list2 = $list2")

val list3 = mutableListOf("a", "b", "c")
list3.add("d")
println("e = $list3")
println("last element = ${list3.last()}")

val list4 = mutableMapOf<String, String>("1" to "A", "2" to "B")
val list5 = mutableSetOf<String>("B", "C", "D")

参数

Kotlin的函数比Java函数强大的地方之一是入参可以有默认值,即默认参数;

在Kotlin调用函数时,可以指定入参的名称,即命名参数;

与Java不同,Koltin表示可变参数,不是参数后面加三个点,,而是在入参前加vararg关键词即可。Kotlin中存在一个展开运算符 – *(星号),它和可变参数搭配使用;作用是把一个数组展开成可变参数传入

详细说明,可看这篇文章Kotlin里的输入参数

顶层函数与属性

  1. 很多代码并不能归属到任何一个类中,有时一个操作对应两个不同的类的对象,而且重要性相差无几。
  2. 有时存在一个基本的对象,但不想通过实例函数来添加操作,让它的API继续膨胀。

在Java里,我们使用静态方法。Kotlin里没有static修饰词,它一种方式,使用顶层函数来实现相同的效果。

顶层函数

实现一个功能,把集合中元素添加前缀,后缀,用分隔符间隔展示

在kt类里直接写

const val counter: Int = 0

fun <T> joinToString(
        collection: Collection<T>,
        separator: String,
        prefix: String,
        postfix: String
                                ): String {
    val sb = StringBuffer(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) {
            sb.append(separator)
        }
        sb.append(element)
    }
    sb.append(postfix)

    return sb.toString()
}

顶层函数是包内成员,包内直接访问。若包外访问,需要import(IDEA等开发工具会为你自动import)

顶层函数是都是静态函数,默认函数所在的文件名加KT作为容器类,比如上述joinToString方法是在Example3_2文件名下的顶层函数,在Java里调用是时

Example3_2Kt.joinToString(collection, ",", "[", "]");

如果我想更改调用静态方法的容器类的类名为StringUtils,则需要在kotlin文件里添加

@file:JvmName("StringUtils")

这时候调用形式如下:

StringUtils.joinToString(collection, "," , "[", "]");

顶层属性

counter就是顶层属性,等效于容器类的静态成员变量,即Java里如下写法

public static final int counter = 0;

拓展函数与属性

拓展函数基本使用

StringUtils.joinToString(collection, "," , "[", "]");

每次调用上述实现的joinToString方法传入四个参数,有点多,我希望减少入参数量。StringUtils是工具类类名,工具类类名在整个调用过程中是不够高效的。达到优雅的途径之一就是做到高效而简洁。这个工具类具体是什么名字并不会影响这个函数的输入和输出,这个类名的意义是作为joinToString容器的标示,如果能把其中一个入参名作为类名,这个入参同时做两件事:传入自身到函数体里,作为调用的句柄名。

强大的Kotlin为我们实现了这样的能力:扩展函数。把上述方法生命为一个拓展函数,如下所示:

fun <T> Collection<T>.joinToString(separator: String = ",", prefix: String = "(", postfix: String = ")"){
    val sb = StringBuilder()
    sb.append(prefix)
    for((index, element) in this.withIndex()){
        if(index > 0){
            sb.append(separator)
        }
        sb.append(element.toString())
    }
    sb.append(postfix)
}

接收者类型:函数名前的类,上例Collection就是该扩展函数的接收者类型

接收者对象:接收者类的实例,上例this.withIndex方法的this指代的就是Collection对象,是该扩展函数接收者对象

这时候我们要使用joinToString方法,变成这样用了

val list3 = mutableListOf("a", "b", "c")
list3.joinToString("_", "[", "]")

导入范围

需要进行导入扩展函数才能生效,好在开发工具为我们自动导入了。如果定义的扩展函数所在的类和其接收者类型的类在一个包下,可以不需要显式导入。

有一种情况,如果你定义的扩展函数名和其他包里定义的函数名相同,你需要导入全类名以示区分。还有另一种方式,通过as在导入的地方重命名扩展函数名

import sugarya.chapter3.joinToString as jts

这时候,就可以调用jts就相当于调用joinToString方法

扩展函数的本质和特性

其实,Kotlin把上述代码翻译到JVM上运行时,就是把扩展函数转成静态方法的。拓展函数的本质是:把接收者对象作为第一个入参的静态方法接收者对象看成静态方法的一个入参。因此,扩展函数并没有改变接收者类里的代码,扩展函数并不是类的一部分,它是声明在类之外的,却能像成员变量那般使用。

像成员变量那般使用,扩展函数和成员变量不是一回事,它们之间是有区别的

  1. 扩展函数不能访问私有或者受保护的成员,因为接收者对象只是静态方法的一个入参,这个入参有大的访问能力,扩展函数就是多大访问能力。
  2. 扩展函数不能被接收者类的子类重写/继承。前面说了,扩展函数只是静态方法,并不是真实的接收者里的成员,自然也就无法重写了。

对于第2点的理解,我们举一个例子

class Person(name: String, var age: Int) : Animal(name)

//拓展定义是写在Example2_4.Kt文件里
fun Animal.move(){
    println("animal move")
}

fun Person.move(){
    println("Person move")
}

val animal: Animal = Person("Kotlin", 5)
animal.move()

输出结果:

animal move

animal.move是拓展函数,转化为静态方法是Example2_4.move(animal),所以,move方法调用的就是Animal类下的move。

扩展属性

扩展属性是对扩展函数能力的弱化/简化使用。相当于Java里第一个参数是接收者对象的静态getter方法和setter方法。扩展函数和扩展属性搭配使用,在扩展函数里访问扩展属性。举个例子

val Animal.length: Int get() = this.name.length * 10

fun Animal.move(){
    println("animal move ${this.length}")
}

扩展函数的应用

看几个扩展函数的应用例子

分割字符串

有一个字符串“ab.cd12.ef”,需要分割成三部分:ab, cd12, ef

使用Java,我们很容易写成这样

String msg = "ab.cd12.ef";
String[] strings = msg.split(".");

java里split()方法入参的字符串表示的正则表达式,在正则表达式里“.”表示任意字符,所以,如果照上面所写,返回为空,找不到字符。

使用Java正确实现是:

String msg = "ab.cd12.ef";
String[] strings = msg.split("\\.");

Kotlin在此基础上,通过扩展函数扩展字符串方法,通过默认参数实现重载效果。

/**
 * Splits this char sequence to a list of strings around occurrences of the specified [delimiters].
 *
 * @param delimiters One or more strings to be used as delimiters.
 * @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`.
 * @param limit The maximum number of substrings to return. Zero by default means no limit is set.
 *
 * To avoid ambiguous results when strings in [delimiters] have characters in common, this method proceeds from
 * the beginning to the end of this string, and matches at each position the first element in [delimiters]
 * that is equal to a delimiter in this instance at that position.
 */
public fun CharSequence.split(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List<String> {
    if (delimiters.size == 1) {
        val delimiter = delimiters[0]
        if (!delimiter.isEmpty()) {
            return split(delimiter, ignoreCase, limit)
        }
    }

    return rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).asIterable().map { substring(it) }
}

Kotlin实现

"ab.cd12.ef"split(".")

Kotlin里用Regex类表示正则,使用正则实现如下

val regex = Regex("\\.")
val result = "ab.cd12.ef".split(regex.toPattern())

解析字符串在Kotlin变得更容易了,除了split,Kotlin还提供了其他方法,再看一个例子

解析文件路径

解析一个文件路径:“/Users/mine/Documents/MyDocument/Photoes/546294_308008399296566_779316797_n.jpg”,获取目录路径,文件名,文件拓展名

Kotlin代码实现

val msg = "/Users/mine/Documents/MyDocument/Photoes/546294_308008399296566_779316797_n.jpg"
val dirPath = msg.substringBeforeLast("/")
val filePath = msg.substringAfterLast("/")
val fileName = filePath.substringBeforeLast(".")
val extendName = filePath.substringAfterLast(".")

println("directory path = $dirPath, fileName = $fileName, extendName = $extendName")

输出:

directory path = /Users/mine/Documents/MyDocument/Photoes, fileName = 546294_308008399296566_779316797_n, extendName = jpg

局部属性

在Java里,函数的最小的作用域是在一个类里(private修饰的方法),而Kotlin引入局部函数–允许在函数里定义一个函数,让函数(方法)的最小作用域降到一个函数体里。提供更小粒度的复用,这样有什么意义呢?

这样是有意义的。

没有局部函数的特性的Java语言里,对方法最小作用域的组织方式是这样的:一个复杂的类里有很多方法,当方法A里的代码行数很多时,通常拆分出几个新的方法a1,a2,a3等等,这些新的方法之间如果存在整体的逻辑关系,就能组合成一个内部类,a1,a2,a3是该内部类的方法。直接在A里新建内部类并调用即可。外部类的其他方法比如方法B也能方便的调用。

Kotlin局部函数提供了比上述Java更细致的代码组织方式:如果我们只在一个方法A里多次用到,这时候在方法A里,定义a1,a2,a3,在方法A里多次使用方法a1,a2,a3。这种方式相较于上面的内部类组织方式,带来的益处是降低定义内部类带来的语法开销。

对于什么时候引入局部函数,我们有了下述认识:
当需要在方法粒度上多次调用一段逻辑时。具体的场景有,登录验证,表单数据校验。

中缀调用

  1. 对只有一个参数的函数使用中缀调用
  2. 中缀调用的函数,需要对其使用inflix修饰符
  3. 中缀不仅适用于成员函数也适用于扩展函数

举个中缀的例子

val pair: Pair<String, String> = "a" to2 "A"

上面的中缀调用是怎么定义呢?

infix fun <T, V> T.to2(v: V): Pair<T, V> = Pair(this, v)

三重引号的字符串

三重引号字符串不仅在于避免转义符,而且可以包含任何字符,包括换行符。

看一个佛祖镇楼的例子

val bless = """
                   _ooOoo_
                  o8888888o
                  88" . "88
                  (| -_- |)
                  O\  =  /O
               ____/`---'\____
             .'  \\|     |//  `.
            /  \\|||  :  |||//  \
           /  _||||| -:- |||||-  \
           |   | \\\  -  /// |   |
           | \_|  ''\---/''  |   |
           \  .-\__  `-`  ___/-. /
         ___`. .'  /--.--\  `. . __
      ."" '<  `.___\_<|>_/___.'  >'"".
     | | :  `- \`.;`\ _ /`;.`/ - ` : | |
     \  \ `-.   \_ __\ /__ _/   .-` /  /
======`-.____`-.___\_____/___.-`____.-'======
                   `=---='
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
         佛祖保佑       永无BUG
         """
    println(bless)

这样控制台按原样格式输出佛祖图

小结

这是Kotlin实战第三章涉及的所有知识点,结合自己的理解整理归纳成本篇文章。