Kotlin语法高阶

385 阅读10分钟

一、Lambda表达式

Kotlin中使用lambda表达式,可以以更加简洁易懂的语法实现功能,是一种语法糖。

1.1 Lambda理解

Lambda本质

Lambda表达式的本质其实是匿名函数,其底层实现中还是通过匿名函数来实现的。

// 定义一个匿名函数,赋值给func变量
val func: () -> String = fun(): String {
    return "Hello Kotlin"
}
//将上述匿名函数转换为lambda

val func = {
    "HelloKotlin"
}

Lambda本质是匿名函数,表达式最后一行是其返回值,无需写return

在Kotlin中,可把lambda分为两类:

  • 一是普通lambda表达式: () -> R
  • 一是带接收者的lambda表达式:T.() -> R

Lambda语法

lambda的标准基本声明满足三个条件:

  1. 含有实际参数

  2. 含有函数体

  3. 包含在花括号内

所有的lambda表达式简化形式都是从此演化而来

{ variable -> body_of_function}

关于Lambda表达式,有几个注意点:

  1. 如果Lambda表达式自动推断的返回类型不是Unit,会把最后一条表达式的值当做是返回值。
  2. 如果写了return语句,则会把它作为外层的函数返回值直接结束外层函数。

1.2 Lambda使用

只有一个参数的Lambda

Lambda表达式的参数只有一个的时候可以使用it来使用此参数。it可表示为单个参数的隐式名称

val getInt: (Int) -> Int = { x -> x + 1 }
val int = getInt(2)	//3

//只有一个参数,可以不写,默认参数名为it
val sum: (Int) -> Int = { it + 1 }	
val int = sum(2)	//3
fun test(num1 : Int, bool : (Int) -> Boolean) : Int{
   return if (bool(num1)){ num1 } else 0
}

//只有一个参数,可以不写,默认参数名为it
println(test(10,{it > 5}))	//10
println(test(4,{it > 5}))	//0

部分参数不处理的Lambda

使用Lambda表达式时,可以用下划线(_)表示未使用的参数,表示不处理这个参数。

val map = mapOf("key1" to "value1","key2" to "value2","key3" to "value3")
map.forEach{
    key , value -> println("$key \t $value")
}

// 不需要key的时候
map.forEach{
    _ , value -> println("$value")
}

/*************输出结果**********/
key1 	 value1
key2 	 value2
key3 	 value3
value1
value2
value3

匿名函数

val test1 = fun(x : Int , y : Int) = x + y  // 当返回值可以自动推断出来的时候,可以省略,和函数一样
val test2 = fun(x : Int , y : Int) : Int = x + y
val test3 = fun(x : Int , y : Int) : Int{
  return x + y
}

println(test1(3,5))	//8
println(test2(4,6))	//10
println(test3(5,7))	//12

关于匿名函数与Lambda的区别:

  1. 匿名函数总是在小括号内参数传值,而Lambda可省略小括号
  2. return语句不带标签,匿名函数返回自身函数值,而Lambda从包含它的函数中返回

**匿名函数如何一步步转换成Lambda?**这里已点击回调示例。

//java
public interface OnClickListener {
  void onClick(View v);
}
public void setOnClickListener(OnClickListener listener) {
  this.listener = listener;
}

//使用
view.setOnClickListener(new OnClickListener() {
  @Override
  void onClick(View v) {
    switchToNextPage();
  }
});

上述示例是一个典型的Java匿名函数使用,看一下其在Kotlin中如何实现

//实际就是传入OnClickListener接口中onClick方法的类型
fun setOnClickListener(onClick: (View) -> Unit) {
  this.onClick = onClick
}

//传入一个onClick的匿名实现函数
view.setOnClickListener(fun(v: View): Unit) {
  switchToNextPage()
})

下面看一下该匿名函数的后续简化过程:

//前述的匿名函数可以简化成Lambda形式
view.setOnClickListener({ v: View ->
  switchToNextPage()
})

//Lambda是函数的最后一个参数,可以将其写在括号外
view.setOnClickListener() { v: View ->
  switchToNextPage()
}

//因为该Lambda是函数的唯一参数,所以可以直接将括号去掉
view.setOnClickListener { v: View ->
  switchToNextPage()
}

//这个Lambda只有一个参数,该参数亦可省略不写
view.setOnClickListener {
  switchToNextPage()
}

赋值给变量的Lambda

前述匿名函数转Lambda的省略写法是因为可以通过上下文推断知道自己的参数类型和返回值类型,其调用的函数声明处有明确参数信息,如:

fun setOnClickListener(onClick: (View) -> Unit) {
  this.onClick = onClick
}

但是,如果将匿名函数赋值给变量而不是作为函数参数传递时,简写成Lambda表达式时,不能省略参数类型

val b = fun(param: Int): String {
  return param.toString()
}
//Lambda简写
val b = { param: Int ->
  return param.toString()
}
//亦或者给变量指明类型,简写为
val b: (Int) -> String = {
  return it.toString() // it 可以被推断出是 Int 类型
}

带接收者的Lambda

带有接收者的函数类型即为A.(B) -> C,它允许你在函数体内访问接收者对象的成员。这部分内容在语法基础的函数调用章节已介绍过。

其他

携带状态:

fun test(b : Int): () -> Int{
    var a = 3
    return fun() : Int{
        a++
        return a + b
    }
}
val t = test(3)
println(t())	// 7
println(t())	// 8
println(t())	// 9

关于Lambda表达式的常用使用场景,主要有

  1. 配合集合一起使用,进行各种筛选、映射、变换操作等
  2. 替代原有匿名内部类(只能替代含单抽象方法的类)
  3. 函数作为参数进行传递

1.3 变量捕获

Java中,在函数内部定义一个匿名内部类或者lambda,内部类访问的函数局部变量必须用final修饰,即无法修改函数局部变量的值。(JDK8后该局部变量无需用final修饰了,底层会自动帮你加上final

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_demo);
    final int count = 0;//函数局部变量,需用final修饰
    findViewById(R.id.btn_click).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //在匿名OnClickListener类内部访问count必须要是final修饰
            System.out.println(count);
        }
    });
}

这里加final修饰,实际是为了保护数据的一致性,即保障变量值的一致性。

引用类型变量本质是存入一个引用地址。如果不用final修饰,该局部变量可以发生变化,而匿名内部类是不知道其是否发生了变化(匿名内部类只拷贝了局部变量的值,而不是直接使用局部变量)。

例如:原先局部变量指向的是对象A,在创建匿名内部类后,匿名内部类中的成员变量也指向A对象。但过了一段时间局部变量的值指向另外一个B对象,但此时匿名内部类中还是指向原先的A对象。那么程序再接着运行下去,可能就会导致程序运行的结果与预期不同。

final修饰后,该引用变量的地址值不能改变,无法再指向其他对象。

Kotlin中,函数内部定义Lambda或内部类,既可访问final修饰的变量,亦可访问非final修饰的变量

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_demo2)
    var count = 0//函数局部变量,这里为非final类型
    btn_click.setOnClickListener {
        //Lambda内直接访问和修改非final类型的变量
        println(count++)
    }
}

实现原理

Lambda表达式在其函数体内可以访问外部的变量,称外部变量被Lambda表达式捕获了。

函数的局部变量生命周期属于这个函数,随着函数执行完毕而销毁。如果该局部变量被Lambda捕获了,使用该局部变量的代码会被存储起来等待稍后再次执行,即被捕获的局部变量可以延迟生命周期

  • 捕获final修饰的局部变量原理:该局部变量值与使用该值的Lambda代码会被一起存储起来
  • 捕获非final修饰的局部变量原理:该局部变量被一个特殊包装器包装起来,通过包装器类实例去修改这个非final的变量,该包装器实例引用是final的,与Lambda代码一起存储。

所以实质上Kotlin的Lambda也只能捕获final修饰的变量,只是在语法层面做了一个桥接包装,将非final变量用一个Ref包装类包装起来,外部保留着Ref包装器的引用是final的。

对于Lambda表达式内部修改局部变量的值,只会在这个Lambda表达式被执行的时候触发。

1.4 函数引用

Lambda可将一个代码块作为一个参数传递给函数,若该代码块已作为一个命名函数存在,这时只需进行函数引用替代即可。

val persons = listOf(Person(name = "Alice", age = 18), Person(name = "Mikyou", age = 20), Person(name = "Bob", age = 16))

println(persons.maxBy({ p: Person -> p.age }))
//使用函数引用
println(persons.maxBy(Person::age))

::的作用就是创建一个和函数具有相同功能的对象

使用场景

属性或函数

val persons = listOf(Person(name = "Alice", age = 18), Person(name = "Mikyou", age = 20), Person(name = "Bob", age = 16))
println(persons.maxBy(Person::age))

顶层函数

package com.mikyou.kotlin.lambda

fun salute() = print("salute")

run(::salute)

扩展函数

fun Person.isChild() = age < 18

val isChild = Person::isChild
println(isChild)

二、高阶函数

2.1 高阶函数理解

高阶函数:将函数用作参数或返回值的函数。

在Kotlin里,函数的参数可以是函数类型,需要指明它有几个参数,参数类型是什么、返回类型是什么相关信息

//函数作为函数参数
fun a(funParam: (Int) -> String): String {
  return funParam(1)
}

//函数作为函数返回值类型
fun c(param: Int): (Int) -> Unit {
  ...
}

函数作为函数参数

public inline fun CharSequence.sumBy(selector: (Char) -> Int): Int {
    var sum: Int = 0
    for (element in this) {
        sum += selector(element)
    }
    return sum
}

val testStr = "abc"
val sum = testStr.sumBy { it.toInt() }
println(sum)	// 294

该函数返回一个Int类型的值。并且接受了一个selector()函数作为该函数的参数

函数作为函数返回值

// 返回函数类型
fun myFunc(): ()-> Long {
	return { System.currentTimeMillis() }
}

自定义高阶函数

//函数作为函数参数
private fun resultByOpt(num1 : Int , num2 : Int , result : (Int ,Int) -> Int) : Int{
    return result(num1,num2)
}

val result1 = resultByOpt(1,2){
    num1, num2 ->  num1 + num2
}
println("result1 = $result1")	// result1 = 3

是把它作为参数传递给函数,还是把它赋值给变量,都要在函数名左边加上双冒号才行。

2.2 常用高阶函数

这里介绍几个较为常用的高阶函数

函数名描述
letval r = x.let { x-> R }
runval r = x.run {this:X -> R}
alsoval x = x.also { x -> Unit }
applyval x = x.apply { this:x -> Unit }
useval r = Closeable.use { c -> R }

TODO函数

这个函数不是一个高阶函数,它只是一个抛出异常以及测试错误的一个普通函数。

fun main(args: Array<String>) {
    TODO("测试TODO函数,是否显示抛出错误")
}

如果调用TODO()时,不传参数的,则会输出An operation is not implemented.

run()函数

run函数仅仅是执行了我们的block(),即一个Lambda表达式,而后返回了执行的结果。

val index = 3
val num = run {
    when(index){
        0 -> "kotlin"
        1 -> "java"
        2 -> "php"
        3 -> "javaScript"
        else -> "none"
    }
}.length
println("num = $num")	//num = 10

T.run()函数

public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
val str = "kotlin"
str.run {
    println( "length = ${this.length}" )	//length = 6
    println( "first = ${first()}")	//first = k
    println( "last = ${last()}" )	//last = n
}
val mTvBtn = findViewById<TextView>(R.id.text)
mTvBtn.run{
    text = "kotlin"
    textSize = 13f
    ...
}

with()函数

with函数的返回值指定了receiver为接收者

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}
val str = "kotlin"
with(str) {
    println( "length = ${this.length}" )	//length = 6
    println( "first = ${first()}")	//first = k
    println( "last = ${last()}" )	//last = n
}

T.apply()函数

public inline fun <T> T.apply(block: T.() -> Unit): T {
      contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
val mTvBtn = findViewById<TextView>(R.id.text)
mTvBtn.apply{
    text = "kotlin"
    textSize = 13f
    ...
}.apply{
    // 这里可以继续去设置属性或一些TextView的其他一些操作
}.apply{
    setOnClickListener{ .... }
}

T.also()函数

public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}
"kotlin".also {
    println("结果:${it.plus("-java")}")	//结果:kotlin-java
}.also {
    println("结果:${it.plus("-php")}")	//结果:kotlin-php
}

"kotlin".apply {
    println("结果:${this.plus("-java")}")	//结果:kotlin-java
}.apply {
    println("结果:${this.plus("-php")}")	//结果:kotlin-php
}

T.also中只能使用it调用自身,而T.apply中只能使用this调用自身

T.let()函数

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
"kotlin".let {
    println("原字符串:$it")         // kotlin
    it.reversed()
}.let {
    println("反转字符串后的值:$it")     // niltok
    it.plus("-java")
}.let {
    println("新的字符串:$it")          // niltok-java
}

"kotlin".also {
    println("原字符串:$it")     // kotlin
    it.reversed()
}.also {
    println("反转字符串后的值:$it")     // kotlin
    it.plus("-java")
}.also {
    println("新的字符串:$it")        // kotlin
}

"kotlin".apply {
    println("原字符串:$this")     // kotlin
    this.reversed()
}.apply {
    println("反转字符串后的值:$this")     // kotlin
    this.plus("-java")
}.apply {
    println("新的字符串:$this")        // kotlin
}

T.takeIf()函数

传入一个你希望的一个条件,如果对象符合你的条件则返回自身,反之,则返回null

val str = "kotlin"

val result = str.takeIf {
    it.startsWith("ko") 
}
println("result = $result")	//result = kotlin

T.takeUnless()函数

作用与T.takeIf(),只是逻辑与其相反

repeat()函数

根据传入的重复次数去重复执行一个我们想要的动作(函数)

repeat(5){
      println("我是重复的第${it + 1}次,我的索引为:$it")
  }
  /*******输出结果********/
  我是重复的第1次,我的索引为:0
  我是重复的第2次,我的索引为:1
  我是重复的第3次,我的索引为:2
  我是重复的第4次,我的索引为:3
  我是重复的第5次,我的索引为:4

三、内联函数

使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包。即那些在函数体内会访问到的变量。 内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。

val array = intArrayOf(1, 2, 3, 4)
array.forEach {
	println("Hello $it")
}

这里看一下forEach源码,其被标记为inline

inline fun IntArray.forEach(action: (Int) -> Unit):Unit {
	for (element in this) action (element)
}

forEach函数有一个函数类型的参数,通过Lambda表达式向forEach函数传入参数值,Kotlin编译器会为Lambda表达式单独创建一个对象,再将Lambda表达式转换为相应的函数并调用。如果这种情况出现比较多的时候,就会很消耗资源。

通过inline关键字,我们可以把一个函数标记成内联函数,这样编译器会在编译时,对内联函数做优化,将内联函数替换到调用处。

前述示例等价于

val array = intArrayOf(1, 2, 3, 4)
for (element in array) {
	println("Hello $it")
}

其中虚函数或局部函数不能声明为内联。

关于内联函数内部的使用有一些限制:

  1. 不支持局部类,内部嵌套类,局部函数的声明
  2. 不支持函数表达式

禁用内联

通过inline关键字,编译器将Lambda函数内联到调用处,消除了运行时消耗。但内联可能导致生成的代码增加,所以需要避免内联比较大的Lambda表达式;并且内联函数的函数类型参数将不再可以作为对象类型使用。只能使用它的invoke方法。

inline fun cost(block: ()->Unit) : () -> Unit {
	block() // Right
	block.invoke() // Right
	block.toString() // Error!
	return block // Error!
}

可以给内联函数的函数类型参数加上noinline关键字,表示该函数类型的参数不参与内联优化。

inline fun printName(name1: (str1: String) -> String
                     , noinline name2: (str2: String) -> String): String {
    var str = "${name1("Name:")}${name2("Czh")}"
    return str
}

内联函数的return问题

关于内联函数的return问题,分为非本地退出和本地退出

例1:None-Local-Return(非本地退出)

val intArray = intArrayOf(1, 2, 3, 4)
intArray.forEach {
    if (it == 3) return
    println("Hello $it")
}

//结果输出
Hello 1
Hello 2

在内联函数中的returnreturn的是调用内联函数的那个函数。因为它退出是外部调用它的那个函数,所以也叫None-Local-Return

例2:Local-Return(本地退出)

val intArray = intArrayOf(1, 2, 3, 4)
intArray.forEach {
	if (it == 3) return@forEach
	println("Hello $it")
}
//结果输出
Hello 1
Hello 2
Hello 4

return@forEach的方式,只能跳出一次内联函数调用。因为它退出的是内联函数本身,所以也叫Local-Return

例3:不能使用None-Local-Return的场景

inline fun createRunnable(block: ()->Unit) : Runnable {
	return object: Runnable {
		override fun run() {
			block()
		}
	}
}

fun main() {
	createRunnable {
		return
	}
}

这种情况下,编译器会报错,因为block的实际调用处和定义处不在一个调用栈上。

kotlin提供了crossinline关键字来主动告诉编译器禁止None-Local-Return

int fun createRunnable(crossinline block: ()->Unit) : Runnable {
	return object: Runnable {
		override fun run() {
			block()
		}
	}
}

加上crossinline后,block函数中将不再允许直接return

四、集合基础

4.1 数组

Kotlin数组类型不是集合中的一种,其操作方式又和集合相似,简单演示其常用方法

val arr = arrayOf("1",2,3,4)

// lastIndex属性,该属性用于返回数组最后一个元素的索引值。
println(arr.size-1==arr.lastIndex)	//true

//使用数组的withIndex()方法来同时访问数组的索引和元素,该方法返回一个Iterable对象,该对象的所有元素都是IndexedValue。
for((index, value) in arr.withIndex()) {
    println("索引为${index}的元素是:${value}")		//索引为0的元素是:1 ,后续打印类似
}

//获取数据元素
println(arr.component1())	// 1
println(arr.component3())	// 3

//数组元素反转
arr.reverse()

4.2 List、Set、Map等

集合概述

Kotlin中,集合类型包含三种类型:它们分别是:ListSetMap,特点是:

  1. 它们都是接口,并不是实际的类。
  2. 它们只实现了isEmpty()、size、contains()等函数以及属性

List类型

  • 声明并初始化List的集合:使用listOf(..)函数
  • 声明并初始化MutableList的集合:使用mutableListOf(..)函数
//使用listOf()初始化不可变的List类型集合
val arr = arrayOf("1","2",3,4,5)   
val list1 = listOf(1,2,"3",4,"5")                // 随意创建         
val list2 = listOf<String>("1","2","3","4","5")  // 确定元素的值类型
val list3 = listOf(arr)                          // 可传入一个数组

//以下代码是错误的。因为List<E>只能是不可变集合。而add、remove、clear等函数是MutableList中的函数
list1.add() 
list1.remove
...

// 遍历
for(value in list1){
    print("$value \t")
}
//使用mutableListOf()初始化可变的List类型集合

val arr = arrayOf("1",2,3,4)
val mutableList1 = mutableListOf(1,2,"3",4,"5")                // 随意创建         
val mutableList2 = mutableListOf<String>("1","2","3","4","5")  // 确定元素的值类型
val mutableList3 = mutableListOf(arr)                          // 可传入一个数组
val mutableList : ArrayList<String>  // 这里的ArrayList<>和Java里面的ArrayList一致

mutableList1.add("6")  // 添加元素
mutableList1 += "8"	//也是添加元素操作,-=等于remove操作
mutableList1.add("7")
mutableList1.remove(1)   // 删除某一元素
mutableList1.clear()   // 清空集合

kotlin相比Java多了个特性,即支持协变,可以把子类的List赋值给父类的List变量。

// kotlin
val strs: List<String> = listOf("a", "b", "c")
val anys: List<Any> = strs
//Java
List<String> strList = new ArrayList<>();
List<Object> objList = strList;	//报错
//Java
stringList.set(5,"HelloWorld");
String valueAt5 = stringList.get(5);
//kotlin
stringList[5] = "HelloWorld"
val valueAt5 = stringList[5]

Set类型

  • 声明并初始化Set的集合:使用setOf(..)函数
  • 声明并初始化MutableSet的集合:使用mutableSetOf(..)函数
val set1 = setOf(1,2,"3","4","2",1,2,3,4,5)
val mutableSet1 = mutableSetOf(1,2,"3","4","2",1,2,3,4,5)
val mutableSet2 : HashSet<String>  // 这里的HashSet<>和Java里面的HashSet<>一致

for(value in set1){
    print("$value \t")	// 1 	2 	3 	4 	2 	3 	4 	5
}

Set类型集合会把重复的元素去除掉

Map类型

  • 不可变的Map类型集合的初始化使用:mapOf()函数
  • 可变的Map类型集合的初始化使用:mutableMapOf()函数
// 以键值对的形式出现,键与值之间使用to
val map1 = mapOf("key1" to 2 , "key2" to 3)
val map2 = mapOf<Int,String>(1 to "value1" , 2 to "value2")
val mutableMap = mutableMapOf("key1" to 2 , "key1" to 3)
val hashMap = hashMapOf("key1" to 2 , "key1" to 3)   // 同Java中的HashMap

map2.forEach{
    key,value -> println("$key \t $value")
}
val value1 = map1.get("key1")

如果键存在重复时,集合会过滤掉之前重复的元素

//java
HashMap<String,Integer> map = new HashMap<>();
map.put("Hello",10);
System.out.println(map,get("Hello"));
//kotlin
val map = HashMap<String, Int>()
map["Hello"] = 10
println(map["Hello"])

注意

mutable前缀的函数创建的是可变的集合,不可变的集合可以通过toMutable()函数转换成可变集合

val strList = listOf("a", "b", "c")
strList.toMutableList()

4.3 集合类型协变

当一个集合赋值给另外一个集合时,如果类型不同的情况,当E继承自M时。你就可以把List<E>赋值给List<M>了。这种情况称之为协变

//父类
open class Person(val name : String , val age : Int){
    override fun toString(): String {
        return "Person(name='$name', age=$age)"
    }
}
//子类
class Student(name: String, age : Int, cls : String) : Person(name, age)

// Any是kotlin中的超类,故而Student类也是继承自Any的。这里你可以换成Person类结果是相同的
var listPerson: List<Any>
val listStudent : List<Student> = listOf(Student("张三",12,"一班"),Student("王五",20,"二班"))
listPerson = listStudent

listPerson.forEach { println(it.toString()) }
/*********输出结果************/
Person(name='张三', age=12)
Person(name='王五', age=20)
var mutableListPerson: MutableList<Person>
val mutableListStudent : List<Student> = listOf(Student("张三",12,"一班"),Student("王五",20,"二班"))
//集合类型协变
mutableListPerson = mutableListStudent.toMutableList()
mutableListPerson.add(Person("a",15))
mutableListPerson.add(Person("b",45))

mutableListPerson.forEach { println(it.toString()) }
/***********输出结果*************/
Person(name='张三', age=12)
Person(name='王五', age=20)
Person(name='a', age=15)
Person(name='b', age=45)

五、泛型

参数化类型使用尖括号声明<>,泛型主要用于集合。这里简单介绍Kotlin的泛型,后面再单独详细的介绍

//Java
public class Box<T> {
    public T value;

    public Box(T t) {
        value = t;
    }
}

new Box<String>("123");
new Box<Integer>(1);
//Kotlin
class Box<T>(t: T) {
    var value = t
}
var box: Box<String> = Box("123")
var box2: Box<Int> = Box(123)

Java中有泛型通配符

  1. 上界通配符? extends,例如? extends E表示此方法接受 E 或者 E 的 一些子类型对象的集合,而不只是 E 自身。其中extends限制了这个未知类型的上界。
  2. 下界通配符? super,例如 ? super E表示此方法接受 E 或者 E 的 一些父类型对象的集合,而不只是 E 自身。其中super限制了这个未知类型的下界
List<? extends TextView> textViews = new ArrayList<TextView>(); // 本身
List<? extends TextView> textViews = new ArrayList<Button>(); // 直接子类
List<? extends TextView> textViews = new ArrayList<RadioButton>(); // 间接子类

List<? super Button> buttons = new ArrayList<Button>(); // 本身
List<? super Button> buttons = new ArrayList<TextView>(); // 直接父类
List<? super Button> buttons = new ArrayList<Object>(); // 间接父类
List<? extends TextView> textViews = new ArrayList<Button>();
TextView textView = textViews.get(0); // get可以
textViews.add(textView); // add 报错: no suitable method found for

List<? super Button> buttons = new ArrayList<TextView>();
Object object = buttons.get(0); // get 出来的是 Object 类型
Button button = ...
buttons.add(button); // add 操作是可以的

List<? extends TextView> get出来的肯定是TextView 的子类型,根据多态特性,能够赋值给TextView。但是其具体类型未知,编译器无法确定类型,所以无法进行add操作。所以使用了 ? extends 泛型通配符的 List,只能够向外提供数据被消费。(向外提供数据的一方称为 生产者 Producer

List<? super Button>中,Button一定是这个未知类型的子类型,根据多态特性,可以使用add进行Button添加操作;Java中所有任何对象都是 Object 的子类,所以可以将其赋值给Object。所以使用 ? super 的泛型 List,通常只能够用来添加数据,称之为消费者Consumer

Kotlin泛型没有提供通配符,以outin修饰符替代

  1. 使用关键字out来支持协变,等同于Java中的上界通配符 ? extends,只能读不能写
  2. 使用关键字in来支持逆变,等同于Java中的下界通配符 ? super,只能写不能读
// 定义一个生产者
class Producer<T> {
    fun produce(): T {
        ...
    }
}

// 使用out关键字,支持协变,即可使用子类泛型赋值
val producer: Producer<out TextView> = Producer<Button>()
val textView: TextView = producer.produce() // 相当于 List 的 get
// 定义一个消费者
class Consumer<T> {
    fun consume(t: T) {
        ...
    }
}

// 使用in关键字,支持逆变,即可使用父类泛型赋值
val consumer: Consumer<in Button> = Consumer<TextView>()
consumer.consume(Button(context)) // 相当于 List 的 add

声明处的outin

前述示例声明中,需要每次在使用的时候加上out TextView来支持协变,可以直接在声明类的时候,给泛型符合加上out关键字,表明泛型参数只用来输出,不用每次使用时额外加上out

class Producer<out T> {    
    fun produce(): T {        
        ...    
    }
}

val producer: Producer<TextView> = Producer<Button>() // 这里不写 out 也不会报错val producer: Producer<out TextView> = Producer<Button>() // out 可以但没必要

inout同理

class Consumer<in T> {
    fun consume(t: T) {
        ...
    }
}

val consumer: Consumer<Button> = Consumer<TextView>() // 这里不写 in 也不会报错
val consumer: Consumer<in Button> = Consumer<TextView>() // in 可以但没必要

泛型函数

//泛型函数语法格式
<Type> methodName(parameter: classType<Type>)

fun <T> singletonList(item: T): List<T> {
    // ……
}
val l = singletonList<Int>(1)
singletonList(l)

类似Java的泛型方法

泛型约束

泛型约束能够限制泛型参数允许使用的类型

fun <T : Comparable<T>> sort(list: List<T>) {
}

sort(1) //编译错误
sort(listOf(1)) //编译通过

前述代码将泛型参数允许使用的类型限制为List

冒号之后指定的类型是上界:只有 Comparable 的子类型可以替代 T

*

Java中单个?能作为泛型通配符使用,相当于 ? extends Object,Kotlin中等效写法,*号相当于out Any

var list: List<*>

如果你的类型定义里已经有了 out 或者 in,那这个限制在变量声明时也依然在,不会被 * 号去掉。(比如你的类型定义里是 out T : Number 的,那它加上 <*> 之后的效果就不是 out Any,而是 out Number

where关键字

Kotlin和java一样,可以设置多个泛型边界,用where关键字

class Monster<T : Animal>	//单个边界,等价于javaextends

class Monster<T> where T : Animal, T : Food	//多个边界,即T必须同时是AnimalFood的子类

reified关键字

由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。

Java的解决方案通常是额外传递一个Class<T>类型参数,然后通过Class#isInstance 方法来检查:

// java
<T> void check(Object item, Class<T> type) {
    if (type.isInstance(item)) {
        System.out.println(item);
    }
}

Kotlin也可以这么解决,通过关键字reified 配合 inline 来解决

//Kotlin
inline fun <reified T> printIfTypeMatch(item: Any) {
    if (item is T) { //  这里就不会在提示错误了
        println(item)
    }
}

六、扩展

扩展:Kotlin可以扩展一个类的新功能而无需继承该类或使用像装饰者这样的设计模式,这种声明叫做扩展。

6.1 扩展函数

扩展函数是静态解析的,并未对原类增添函数或者属性,对类本身没有影响。

把扩展的类或者接口,放置即将要添加的函数名前面,这个类或者名称就叫做接收者类型。类的名称与函数之间用.调用连接。this指代这个接收者对象,可以访问扩展的这个类可访问的方法和属性

接收者类型由扩展函数定义,接收者对象是这个接收者类型的对象实例,这个对象实例就可以访问这个类中成员方法和属性,一般将扩展函数当作成员函数使用。

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // “this”对应该列表
    this[index1] = this[index2]
    this[index2] = tmp
}

val mutableList = mutableListOf(1, 2, 3)
mutableList.swap(1, 2) //调用扩展函数swap()
println(mutableList)	// [1,3,2]

上面代码用MutableList作为接受者类型,对其进行扩展,为其添加一个 swap 函数

与内部成员函数冲突

class User {
    //内部成员函数
    fun print() {
      println("内部成员函数")
    }
}

//扩展函数,这里与内部成员函数发生冲突
fun User.print() {
    println("扩展函数")
}

//调用时若发生冲突,优先调用内部成员函数
User().print()	// 内部成员函数

如果扩展函数的函数名跟内部成员函数的函数名冲突,会优先调用内部成员函数

可空接收者

//扩展函数
fun Any?.toString(): String {
  if (this == null) return "null"
    return toString()
}

//调用
var a = null
a.toString()
println(a)	// null
var b = "not null"
b.toString()
println(b)	// not null

6.2 扩展属性

与函数类似,Kotlin 支持扩展属性

class User {
    //必须声明为public(Kotlin默认是public)
    //否则扩展属性无法访问该变量
    var mValue = 0
}

//扩展属性
var User.value: Int
    get() = mValue
    set(value) {
        mValue = value
    }

//调用扩展函数
var user = User()
user.value = 2
println(user.value)

扩展属性必须定义get()方法。Kotlin中类中的属性都是默认添加get()方法的,但是由于扩展属性并不是给现有库中的类添加额外的属性,就没实现

6.3 扩展伴生对象

class User {
    //伴生对象
    companion object {
    }
}

//扩展伴生对象
fun User.Companion.foo() {
    println("伴生对象扩展")
}

//调用
User.foo()	// 伴生对象扩展

6.4 扩展作用域

在一个类内部可以为另一个类声明扩展

//定义User类,添加一个printUser()函数
class User {
  fun printUser(){
        println("User")
    }
}
//定义User2类,在里面对User类进行扩展
class User2 {
    fun printUser2() {
        println("User2")
    }

    //扩展函数
    fun User.print() {
        printUser()
        printUser2()
    }

    fun getUser(user: User) {
        //调用扩展函数
        user.print()
    }
}

//调用
User2().getUser(User()) 	// User		User2
  1. 扩展声明所在的类的实例称为 分发接收者,即前述的User2()
  2. 扩展方法调用所在的接收者类型的实例称为 扩展接收者,即前述User()
  3. 分发接收者和扩展接收者的成员名字起冲突时,扩展接收者优先

如果要引用分发接收者的成员

//User类不变
class User {
    fun printUser(){
        println("User")
    }
}
class User2 {
  fun printUser() {
      println("User2")
  }

  fun User.print() {
      //名字冲突,优先扩展接收者
      printUser()
      //表示调用User2的printUser()函数
      this@User2.printUser()
  }

  fun getUser(user: User) {
      //调用扩展方法
      user.print()
  }
}

6.5 实质原理

//扩展函数定义
fun TextView.isBold() = this.apply { 
    paint.isFakeBoldText = true
}

对于前述这个扩展函数,进行decompile

public final class ExtendsionTextViewKt {//这个类名就是顶层文件名+“Kt”后缀
   @NotNull
   public static final TextView isBold(@NotNull TextView $receiver) {//扩展函数isBold对应实际上是Java中的静态函数,并且传入一个接收者类型对象作为参数
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      $receiver.getPaint().setFakeBoldText(true);//设置加粗
      return $receiver;//最后返回这个接收者对象自身,以致于我们在Kotlin中完全可以使用this替代接收者对象或者直接不写。
   }
}

扩展函数实际上就是一个对应Java中静态函数,这个静态函数参数为接收者类型对象,利用这个对象可以访问类中成员属性和方法

如果要在java环境调用kotlin中定义的扩展函数,实际就是调用该静态函数

ExtendsionTextViewKt.isBold(activity.findViewById(R.id.course_comment_tv_score));//直接调用静态函数

关于扩展函数的注意点:

  1. 扩展函数实际上是一个静态函数,处于类的外部
  2. 扩展函数不可以被重写
  3. 扩展函数不能访问内部私有有函数和属性

七、委托

操作的对象不用自己去执行,而是将任务交给另一个对象操作,这样的模式就叫委托模式,被操作的对象叫委托。简单来说就是有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。通过关键字by实现委托。

7.1 类委托

即一个类中定义的方法实际是调用另一个类的对象的方法来实现

//创建接口
interface Base {
    fun print()
}
//实现此接口被委托的类
class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

//通过关键字 by 建立委托类
class Derived(b: Base) : Base by b
fun main() {
    val b = BaseImpl(10)
    Derived(b).print()	//输出10
}
  1. 派生类 Derived 继承了接口 Base 所有方法,并且委托一个传入的 Base 类的对象来执行这些方法
  2. by子句表示:将b保存在Derived对象实例内部,而且编译器将会生成继承自Base接口的所有方法,并将调用转发给b

对于类委托,编译器会使用override 覆盖的实现而不是委托对象中的。

interface Base {
    fun printMessage()
    fun printMessageLine()
}
//实现此接口的被委托的类
class BaseImpl(val x: Int) : Base {
    override fun printMessage() { print(x) }
    override fun printMessageLine() { println(x) }
}

class Derived(b: Base) : Base by b {
    //覆盖被委托类中的实现
    override fun printMessage() { print("abc") }
}
fun main() {
    val b = BaseImpl(10)
    Derived(b).printMessage()		//abc
    Derived(b).printMessageLine()	//10
}

7.2 属性委托

属性委托:一个类的某个属性值不是在类中直接进行定义,而是将其托付给一个代理类,从而实现对该类的属性统一管理,该属性称为委托属性

语法格式:

val/var <属性名>: <类型> by <表达式>

by关键字之后就是委托,属性的 get() 方法(以及set() 方法)将被委托给这个对象的 getValue()setValue() 方法。

属性委托不必实现任何接口,但需提供getValue()函数(对于 var属性,还需要 setValue() 函数)

被委托的类的定义

下面通过一个示例演示如何定义一个被委托的类

// 定义包含属性委托的类
class Example {
    var p: String by Delegate()
}
//thisRef:委托属性所在类的引用
//property:委托属性的元数据,包括:var 还是 val、类型、名称等等
class Delegate {
    //获取Example中p的值时会调用此方法
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, 这里委托了 ${property.name} 属性"
    }

    //设置Example中p的值时会调用此方法
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef${property.name} 属性赋值为 $value")
    }
}
val e = Example()
println(e.p)
e.p = "Runoob"
println(e.p)

/*****输出结果*****/
Example@433c675d, 这里委托了 p 属性
Example@433c675d 的 p 属性赋值为 Runoob
Example@433c675d, 这里委托了 p 属性

除了按照前述示例的方式进行定义被委托的类,Kotlin提供了 ReadWritePropertyReadOnlyProperty 接口类,来方便我们实现属性委托。

public fun interface ReadOnlyProperty<in T, out V> {
    public operator fun getValue(thisRef: T, property: KProperty<*>): V
}

public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
    public override operator fun getValue(thisRef: T, property: KProperty<*>): V

    public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}

这里简单演示该接口类的使用

object User{
    var userToken: String by TokenProperty()
}
class TokenProperty : ReadWriteProperty<User, String> {
    override fun getValue(thisRef: User, property: KProperty<*>): String {
        val token = loadFileToekn()
        return token
    }
   
    override fun setValue(thisRef: User, property: KProperty<*>, value: String) {
        saveToken(value)
    }

    private fun saveToken(value: String) {}

    private fun loadFileToekn():String{
        // 模拟从文件读取 token
        return "Kotlin is great"
    }
}
User.userToken = "阿文"
val toekn = User.userToken
print(toekn)	//Kotlin is great

7.3 标准委托

Kotlin 的标准库中已经内置了很多工厂方法来实现属性的委托,主要分为三类

  1. 延迟属性(lazy properties): 其值只在首次访问时计算;
  2. 延迟属性(lazy properties): 其值只在首次访问时计算;
  3. 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。

延迟属性Lazy

lazy() 是一个函数, 接受一个 Lambda 表达式作为参数, 返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托。 第一次调用 get() 会执行已传递给 lazy() 的 lamda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

val lazyValue: String by lazy {
    println("computed!")     // 第一次调用输出,第二次调用不执行
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)   // 第一次执行,执行两次输出表达式
    println(lazyValue)   // 第二次执行,只输出返回值
}
//输出结果
computed!
Hello
Hello

可观察属性Observable

observable 可以用于实现观察者模式。

Delegates.observable() 函数接受两个参数:

  1. 第一个是初始化值,
  2. 第二个是属性值变化事件的响应器(handler)。

在属性赋值后会执行事件的响应器(handler),它有三个参数:被赋值的属性、旧值和新值:

import kotlin.properties.Delegates
class User {
    var name: String by Delegates.observable("初始值") {
        prop, old, new ->
        println("旧值:$old -> 新值:$new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "第一次赋值"
    user.name = "第二次赋值"
}
//结果输出
旧值:初始值 -> 新值:第一次赋值
旧值:第一次赋值 -> 新值:第二次赋值

把属性存储在映射中

常见的用例是在一个映射(map)里存储属性的值。

class Site(val map: Map<String, Any?>) {
    val name: String by map
    val url: String  by map
}
fun main(args: Array<String>) {
    // 构造函数接受一个映射参数
    val site = Site(mapOf(
        "name" to "菜鸟教程",
        "url"  to "www.runoob.com"
    ))

    // 读取映射值
    println(site.name)	//菜鸟教程
    println(site.url)	//www.runoob.com
}

如果使用 var 属性,需要把 Map 换成 MutableMap

Not Null

notNull 适用于那些无法在初始化阶段就确定属性值的场合

class Foo {
    var notNullBar: String by Delegates.notNull<String>()
}
foo.notNullBar = "bar"
println(foo.notNullBar)	//属性在赋值前就被访问的话则会抛出异常。

7.4 局部变量委托

可以将局部变量声明为委托属性,例如使一个局部变量惰性初始化

fun example(computeFoo: () -> Foo) {
      val memoizedFoo by lazy(computeFoo)

      if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
      }
}

memoizedFoo 变量只会在第一次访问时计算。 如果 someCondition 失败,那么该变量根本不会计算。

7.5 典型示例

class Preference<T>(val context: Context, val name: String, val default: T) : ReadWriteProperty<Any?, T> {

    val prefs by lazy { context.getSharedPreferences("default", Context.MODE_PRIVATE) }

    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return findPreference(name, default)
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        putPreference(name, value)
    }

    private fun <U> findPreference(name: String, default: U): U = with(prefs) {
        val res: Any = when (default) {
            is Long -> getLong(name, default)
            is String -> getString(name, default)
            is Int -> getInt(name, default)
            is Boolean -> getBoolean(name, default)
            is Float -> getFloat(name, default)
            else -> throw IllegalArgumentException("This type can be saved into Preferences")
        }

        res as U
    }

    private fun <U> putPreference(name: String, value: U) = with(prefs.edit()) {
        when (value) {
            is Long -> putLong(name, value)
            is String -> putString(name, value)
            is Int -> putInt(name, value)
            is Boolean -> putBoolean(name, value)
            is Float -> putFloat(name, value)
            else -> throw IllegalArgumentException("This type can be saved into Preferences")
        }.apply()
    }
}
class ExampleActivity : AppCompatActivity(){
    var a: Int by Preference(this, "a", 0)
    
    fun whatever(){
        println(a)//会从SharedPreference取这个数据
        a = 9 //会将这个数据写入SharedPreference
    }
}

八、协程初识

本质上,协程是轻量级的线程

从Android开发角度理解,协程:

  • 协程没有直接和操作系统关联,其也是跑在线程中,可以是单线程或多线程
  • 单线程中的协程总的执行时间并不会比不用协程少。
  • 主线程执行网络请求会抛异常,主线程上协程也不例外,这种场景需要切线程

协程主要是为了解决并发问题,是Kotlin中提供的一套线程封装API

简单从线程控制场景去理解协程。

Java中通过Thread开启并发操作

new Thread(new Runnable() {
    @Override
    public void run() {
        ...
    }
}).start();

当然,也可以通过线程池,AsyncTask解决线程通信,抑或是通过RxJava解决

Kotlin的协程使用示例片段:

launch({
    val user = api.getUser() // 网络请求(IO 线程)
    nameTv.text = user.name  // 更新 UI(主线程)
})

launch函数加上实现在{}中具体逻辑,就构成了一个协程,避免了一般的回调处理等。前述代码上下两个语句分别工作在两个线程里,写法和普通单线程一样。即用同步的方法写异步。

这里不对协程展开介绍了,在后面再针对协程详细进行解析介绍。