Kotlin杂记

1. 函数

当函数只有一行代码,Kotlin允许我们不必编写函数体

fun largerNum(num1: Int, num2: Int) = max(num1, num2)

return关键字可以省略,等号足以表达返回值的意思。

kotlin拥有出色的类型推导功能。max()函数返回一个Int值,largerNum()又使用等号连接了max(),因此kotlin可以推导出largerNum()必然返回一个Int值。


2. 逻辑控制

if条件

Kotlin中if语句有返回值,返回值就是if语句每个条件中最后一行代码的值。

fun largerNum(num1: Int, num2: Int) = if (num1 > num2) {
	num1
} else {
	num2
}

when条件

when跟switch类似,但比switch强大。和if语句类似,when语句也有返回值

fun getScore(name: String) = when (name) {
	"Tom" -> 86
	"Jim" -> 95
	"Jack" -> 100
	else -> 0
}

上面是精确匹配,下面再看一下类型匹配

fun checkNum(num: Number) {
	when (num) {
		is Int -> println("number is Int")
		is Double -> println("number is Double")
		else -> println("number not support")
	}
}

Number是kotlin的一个抽象类,Int,Long,Double等于数字相关的类都是它的子类。is类似Java的instanceOf

when还有一种不带参数的用法,这种写法把判断体写在了when结构体中。

fun getScore(name: String) = when() {
	name.startsWith("Tom") -> 86
	name == "Jim" -> 77
	name == "Jack" -> 95
	else -> 0
}

所有名字以Tom开头的人,他的分数都是86分


3. 循环

区间

val range = 0..10

“点点” 关键字kotlin中,用于创建一个区间,即 [0, 10]。有了区间,kotlin使用for-in循环进行遍历

fun main() {
	for (i in 0..10) {
		println(i)
	}
}

创建一个左闭右开的区间

val range = 0 until 10

step关键字可以设置步长

fun main () {
	for (i in 0 until 10 step 2) {
		println(i)
	}
}

不管是点点、还是until都只能创建升序区间。如果想创建一个降序区间,可以使用downTo关键字

fun main () {
	// 创建一个[10, 1]的降序区间
	for (i in 10 downTo 1) {
		println(i)
	}
}

4. 类,对象

主构造函数

主构造函数是最常用的构造函数,每个类默认有一个不带参数的主构造函数,也可以显示的指定参数。

主构造函数特点:没有函数体,直接定义在类名后面即可

open class Person {
	...
}

// 继承Person
class Student(val sno: String, val grade: Int) : Person() {

}

注意:主构造函数中声明成val或者var的参数,将自动成为该类的字段;也就是说,sno和grade已经成为Student类的字段了

这里我们将学号、年纪两个字段放到主构造函数中。这就表明:在实例化Student类时,必须要传入构造函数中要求的所有的参数

val student = Student("a123", 5)

问:既然主构造函数没有函数体,怎么在主构造函数中编写逻辑呢?
答:init 结构体

class Student(val sno: String, val grade: Int) : Person() {
	init {
		// 创建Student类实例后,这里一定会打印
		println("sno is $sno")
		println("grade is $grade")
	}
}

问:Student继承Person,Person后面的括号是什么意思?
答:根据继承特性的规定,子构造函数必须调用父构造函数,这对括号表示Student类的主构造函数在初始化的时候会调用Person类的无参构造函数。

更改Person类的主构造函数

open class Person(val name: String, val age: Int) {
	...
}

这是Person就没有无参构造函数了。Student就得添加对应字段

class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {
	init {
		// 创建Student类实例后,这里一定会打印
		println("sno is $sno")
		println("grade is $grade")
	}
}

val student = Student("a123", 5, "Jack", 19)

次构造函数

Kotlin规定,任何一个类只能有一个主构造函数,但可以有多个次构造函数。次构造函数也可以实例化一个类,并拥有函数体。

Kotlin规定,当一个类既有主构造函数,又有次构造函数,所有次构造函数必须调用主构造函数。

class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {

	// this调用主构造函数
	constructor(name: String, age: Int) : this("", 0, name, age) {
	}
	
	// this调用上面的次构造函数
	constructor() : this("", 0) {
	}
}

Kotlin允许类中只有次构造函数,没有主构造函数。

class Student : Person {
	constructor(name: String, age: Int) : super(name, age) {
	}
}

5. 数据类

data关键字,标记一个类为数据类,即 java bean

data class Cellphoe(val brand: String, val price: Double)

data关键字,kotlin会根据主构造函数中的参数帮你自动生成equals(),hashCode(),toString()等函数,大大减少开发工作量。


6. 单例

kotlin创建一个单例极其简单,去掉class关键字,使用object关键字即可。

// Singleton即为单例
object Singleton {
	fun test() {
		println("called")
	}
}

// 类似java静态方法调用
Singleton.test()

可以看到,Kotlin中的单例不需要私有化构造函数,也不需要提供getInstance()方法。单例调用也简单,类似java的静态方法调用。


7. 集合-Map

Kotlin中不建议使用put()和get()来操作map,而是推荐一种类似数组下标的语法结构。

// 构建map
val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3)
// 遍历map
for ((fruit, number) in map) {
	println("fruit is $fruit, number is $number")
}
// 读map
val number = map["Aplle"]

8. Lambda

lambda定义:
{参数1: 参数类型, 参数2: 参数类型 -> 表达式体}

举个栗子:查找集合中最长的字符串

val list = listOf("Apple", "Banana", "Watermelon", "Grape")
val lambda = {fruit: String -> fruit.length}
val maxLengthFruit = list.maxBy(lambda)

简化一:
不需要专门定义一个lambda变量

val list = listOf("Apple", "Banana", "Watermelon", "Grape")
val maxLengthFruit = list.maxBy({fruit: String -> fruit.length})

简化二:
kotlin规定,当lambda表达式为函数最后一个参数时,可以将lambda表达式移到括号外面

val maxLengthFruit = list.maxBy() {fruit: String -> fruit.length}

简化三:
如果lambda表达式是函数的唯一参数,可以省略括号

val maxLengthFruit = list.maxBy {fruit: String -> fruit.length}

简化四:
kotlin强大的类型推导机制,可以省略参数类型

val maxLengthFruit = list.maxBy {fruit -> fruit.length}

简化五:
当Lambda表达式只有一个参数,参数名也可以省略,用it关键字代替

val maxLengthFruit = list.maxBy {it.length}

9. 集合的any和all函数

any函数: 判断集合中是否至少存在一个元素满足指定条件
all函数:判断集合中所有元素是否都满足指定条件

val list = listOf("Apple", "Banana", "Watermelon", "Grape")
// 集合中是否存在长度在5以内的单词
val anyResult = list.any { it.length <= 5 }
// 集合中是否所有单词的长度都在5以内
val allResult = list.add { it.length <= 5 }

10. java函数式API

kotlin调用java方法,也可以使用Lambda表达式。前提:该方法接收一个java单抽象方法接口参数。
比如开线程方法:

new Thread(new Runnable() {
	@ovverride
	public void run() {
		// ...
	}
}).start()

kotlin匿名内部类写法:

Thread(object: Runnable {
	override fun run() {
		// ...
	}
}).start()

改造成lambda形式:

Thread {
	// ...
}.start()

Android中Button点击监听也是如此

button.setOnClickListener {
	// ...
}

11. kotlin中的空指针

可空的类型系统:Int?, String? …

kotlin编译器对非空判断并不总是那么智能,看如下代码

var content: String? = "hello"
fun main() {
	if (content != null) {
		printUpperCase()
	}
}
fun printUpperCase() {
	// 虽然上面做了非空判断,但这里编译还是不会通过。
	val upperCase = content.toUpperCase()
	println(upperCase)
}

?. 操作符
用于去掉if判断:

if (a != null) {
	a.doSomething()
}

a?.doSomething()

?: 操作符
左右两边接收一个表达式,如果左边表达式不空就返回左边的结果,否则返回右边表达式的结果

val c = if (a != null) {
	a
} else {
	b
}

c = a ?: b

let函数
将原始对象作为参数传递到lambda表达式中,let函数是kotlin的标准函数,示例代码:

obj.let { objs -> 
	// 编写逻辑
}

利用let函数可以结合?.操作,简化判空操作:

fun doStudy(study: Study?) {
	study?.readBooks()
	study?.doHomework()
}

// 利用let简化
fun doStudy(study: Study?) {
	study?.let {
		it.readBooks()
		it.doHomework()
	}
}

12. 标准函数with、run、apply

with函数

with函数接收两个参数,第一个参数是任意类型的对象,第二个参数是lambda表达式,lambda表达式最后一行为函数的返回值,with函数会在lambda表达式中提供第一个参数对象的上下文

val list = listOf("Apple", "Banana", "Pear", "Orange")
val builder = StringBuilder()
builder.append("start eating fruits.\n")
for (fruit in list) {
	builder.append(fruit).append("\n")
}
builder.append("Ate all fruits.")
val result = builder.toString()
println(result)

上述代码连续调用了多次builder对象的方法,可以考虑使用with简化

val list = listOf("Apple", "Banana", "Pear", "Orange")
val result = with(StringBuilder()) {
	append("start eating fruits.\n")
	for (fruit in list) {
		append(fruit).append("\n")
	}
	append("Ate all fruits.")
	toString()
}
println(result)

run函数
整体和with函数类似,只不过run函数不能直接调用,一定要调用某个对象的run函数,另外run函数只接收一个Lambda参数

val list = listOf("Apple", "Banana", "Pear", "Orange")
val result = StringBuilder().run {
	append("start eating fruits.\n")
	for (fruit in list) {
		append(fruit).append("\n")
	}
	append("Ate all fruits.")
	toString()
}
println(result)

apply函数
apply函数和run函数类似,但是apply函数只返回调用对象本身

val list = listOf("Apple", "Banana", "Pear", "Orange")
// result是StringBuilder对象
val result = StringBuilder().apply {
	append("start eating fruits.\n")
	for (fruit in list) {
		append(fruit).append("\n")
	}
	append("Ate all fruits.")
}
println(result.toString())

Android启动Intent的写法可以使用这个标准函数

// 传参越多,这种写法优势越明显
val intent = Intent(context, SecondActivity::class.java).apply {
	putExtra("param1", data1)
	putExtra("param2", data2)
}
context.startActivity(intent)

13. 静态方法

kotlin已经弱化静态方法的概念,像Utils工具类,Kotlin非常推荐使用单例类来实现

object Util {
	fun doAction() {
		// do somethind
	}
}

不过单例类,会将整个类中所有方法变成静态方法。如果我们希望将类中某一些方法变成静态方法,该怎么办?使用companion object关键字

class Util {
	// 需要创建Util对象,才能调用
	fun doAction1() {
		// do something
	}
	
	companion object {
		// 可以直接使用Util.doAction2()
		fun doAction2() {
			// do something
		}
	}
}
  1. 实际上doAction2()方法也不是静态方法,
  2. companion object关键字会在Util类内部创建一个伴生类,doAction2()是定义在伴生类里面的实例方法。
  3. Kotlin会保证Util类只会存在一个伴生类对象

真正静态方法,在kotlin中通过如下方式实现的:

  1. @JvmStatic注解
  2. 顶层函数

所有的顶层函数,在kotlin中都可以直接调用。
在java中调用kotlin的顶层函数使用: 文件名.方法名() 即可


14. 延迟初始化

lateinit 通知编译器我会晚些时候对这个变量进行初始化,这样就不用一开始就将他赋为null了

class MainActivity : AppCompatActivity(), View.OnClickListener {
	private var adapter: MsgAdapter? = null
	
	override fun onCreate(savedInstanceState: Bundle?) {
		adapter = MsgAdatper(msgList)
	}

	override fun onClick(v : View?) {
		adapter?.notifyItemInserted(msgList.size - 1)
	}
}

由于adapter是全局变量,但是初始化工作在onCreate()方法中。因此不得不先将adapter赋值为null,同时把类型声明成可空类型MsgAdapter?

虽然我们在onCreate()中对adapter进行了初始化,同时也能保证onClick()在onCreate()之后调用,但是我们还是必须在onClick()中对adapter进行判空操作。这个就很恶。

使用 lateinit 可以解决这些无意义的判断

class MainActivity : AppCompatActivity(), View.OnClickListener {
	private lateinit var adapter: MsgAdapter
	
	override fun onCreate(savedInstanceState: Bundle?) {
		if (!::adapter.isInitialized) {
			adapter = MsgAdatper(msgList)
		}
	}

	override fun onClick(v : View?) {
		adapter.notifyItemInserted(msgList.size - 1)
	}
}

15. 密封类 - 解决无意义else分支

interface Result
class Success(val msg: String) : Result
class Failure(val error: Exception): Result

定义一个Result接口,表示某个操作的执行结果。接口中不编写任何内容。然后定义两个类实现Result接口:Success类表示成功时结果,Failure类表示失败的结果。

fun getResultMsg(result: Result) = when(result) {
	is Success -> result.msg
	is Failure -> result.error.massage
	// 虽然else永远走不到,但不得不写,只是为了满足kotlin编译器语法检查
	else -> throw IllegalArgumentException()
}

密封类sealed class 能够解决这个问题:

sealed class Result
class Success(val msg: String) : Result()
class Failure(val error: Exception) : Result()

可以看到代码并没有太大变化,只是将interface关键字改成了sealed class

fun getResultMsg(result: Result) = when(result) {
	is Success -> result.msg
	is Failure -> result.error.massage
}

这里去掉else分支也能编译过。因为当在when语句中传入一个密封类变量作为条件时,Kotlin编译器会检查该密封类有哪些子类,并强制要求对每一个子类的条件全部处理掉。

RecyclerView中可能会存在多种类型的RecyclerView.ViewHolder,可以使用sealed class去实现。


16. 扩展函数

扩展函数: 在不修改某个类源码的情况下,仍然可以打开这个类,向该类添加新的函数。

先来思考一个功能:统计一个字符串中字母的数量。按照一般的变成思维,大多人会写出如下的util方法

object StringUtil {
	fun lettersCount(str: String): Int {
		var count = 0
		for (char in str) {
			if (char.isLetter()) {
				count++
			}
		}
		return count
	}
}

var str = "ABCD1234!@#"
val count = StringUtil.lettersCount(str)

扩展函数是以更加面向对象的思维来解决这个问题: 将lettersCount()函数添加到String类当中。

扩展函数的语法:

fun ClassName.methodName(param1: Int, param2: Int): Int {
	return 0
}

相比于普通函数,扩展函数只需要在函数名前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中了。

扩展函数最好定义成顶层函数,让扩展函数具有全局访问域。文件名没有严格要求,但由于向String类中添加扩展函数,文件名建议写成String.kt

fun String.lettersCount(): Int {
	var count = 0
	for (char in this) {
		if (char.isLetter()) {
			count++
		}
	}
	return count
}

// 调用扩展函数
val count = "ABCD1234!@#".lettersCount()

Kotlin对String类添加丰富的扩展函数,好多功能时java没有的。比如:reverse()函数用于字符串反转,capitalize()函数用于对首字母进行大写。


17. 运算符重载

Kotlin允许我们将任意两个对象相加。而且实现起来非常简单。

运算符重载的关键字是operator关键字,只要在指定函数前面加上operator关键字,就可以实现运算符重载功能。比如:+运算符,对应的就是plus()函数;-运算符,对应的minus()函数。

语法结构如下:

class Obj {
	operator fun plus(obj: Obj): Obj {
		// 处理相加逻辑
	}
}

举个栗子:让Money对象相加

class Money(val value: Int) {
	operator fun plus(money: Money) : Money {
		val sum = value + money.value
		return Money(sum)
	}
}

val money1 = Money(5)
val money2 = Money(10)
// Kotlin最终调用: money1.plus(money2)
val money3 = money1 + money2
println(money3.value)

很多运算符重载,可以让我们代码看上去更加舒服:

if("hello".contains("he")) {

}
// 借助重载,我们可以这样写
if ("he" in "hello") {

}

18. 高阶函数

定义:如果一个函数接收函数类型的参数,或者返回值是函数类型,那么该函数即为高阶函数

函数类型语法:

(String, Int) -> Unit

高阶函数定义示例:

fun example(func: (String, Int) -> Unit) {
	func("hello", 123)
}

高阶函数的作用:高阶函数允许让函数类型的参数决定函数的执行逻辑。比如:

fun num1Andnum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
	val result = operation(num1, num2)
	return result
}

Lambda表达式是最常见也是最普遍的高阶函数调用方式。Lambda表达式可以完整的表达出一个函数类型。

val result1 = num1AndNum2(100, 80) { n1, n2 ->
	n1 + n2
}
val result2 = num1AndNum2(100, 80) { n1, n2 ->
	n1 - n2
}

Sample:编写一个高阶函数,实现类似apply函数功能

fun StringBuilder.apply(block: StringBuilder.() -> Unit) : StringBuilder {
	block()
	return this
}

这里给Stringbuilder类定义了一个apply扩展函数,这个扩展函数接收一个函数类型参数,并且返回值类型也是StringBuilder。

这里的函数类型参数,我们在前面加上了StringBuilder.的语法结构,这是高阶函数的完整语法结构,在函数类型前面加上ClassName. 就表示这个函数类型是定义在哪个类当中。

高阶函数背后的原理:高阶函数最后会转换成java的匿名内部类。所以,每次调用一次Lambda表达式,都会创建一个新的匿名内部类,这也会造成额外的内存和性能开销。

为解决高阶函数的性能问题,Kotlin提供了内联函数的功能。即:

inline fun num1AndNum2(num1: Int num2: Int, operation: (Int, Int) -> Int): Int {
	val result = operation(num1, num2)
	return result
}

内联函数的工作原理:Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方。这样就不存在运行时开销。

内联和非内联的区别

内联和非内联最大的区别就是:

  • 内联函数所引用的Lambda表达式中可以使用return来进行函数返回;
  • 非内联只能进行局部返回

非内联:

fun printString(str: String, block: (String) -> Unit) {
	println("printString begin")
	block(str)
	println("printString end")
}

fun main() {
	println("main start")
	val str = ""
	printString(str) { s ->
		println("Lambda start")
		// 非内联,局部返回,main函数后面的代码还得运行
		if (s.isEmpty()) return @printString
		println(s)
		println("Lambda end")
	}
	println("main end")
}

内联:

inline fun printString(str: String, block: (String) -> Unit) {
	println("printString begin")
	block(str)
	println("printString end")
}

fun main() {
	println("main start")
	val str = ""
	printString(str) { s ->
		println("Lambda start")
		// 内联,函数返回,main()函数返回
		if (s.isEmpty()) return
		println(s)
		println("Lambda end")
	}
	println("main end")
}

两个例子

Sample1: SharedPreferences简化

java思维写法:

val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("nane", "Tom")
editor.putString("age", 28)
editor.putBoolean("married", false)
editor.apply()

Kotlin思维写法:

// 定义一个open高阶函数
fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
	val editor = edit()
	editor.block()
	editor.apply()
}

// 使用
getSharedPreferences("data", Context.MODE_PRIVATE).open {
	putString("name", "Tom")
	putInt("age", 20)
	putBoolean("married", false)
}

使用了高阶函数简化之后,不管在易用性还是可读性上,SharedPreferences用法都简化了很多。

Sample2: ContentValues简化

java思维

val values = ContentValues()
values.put("name", "Game of Thrones")
values.put("author", "George Martin")
values.put("pages", 720)
values.put("price", 20.85)
db.insert("Book", null, values)

Kotlin思维:参考mapOf()函数

fun contentValuesOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
	for (pair in pairs) {
		val key = pair.first
		val value = pair.second
		when (value) {
			is Int -> put(key, value)
			is Long -> put(key, value)
			is Short -> put(key, value)
			is Float -> put(key, value)
			is Double -> put(key, value)
			is String -> put(key, value)
			is Byte -> put(key, value)
			is ByteArray -> put(key, value)
			null -> putNull(key)
		}
	}
}

// 使用
val values = contentValuesOf("name" to "Game of Thrones", "author" to "George Martin", "pages" to 720, "price" to 20.85)
db.insert("Book", null, values)

上面的例子,是参考Google的KTX库提供的函数,目的是加深对Kotlin特性的理解。


19. 泛型

基本用法

一般情况下,我们需要给任何一个变量指定一个具体类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样的代码有更好的扩展性。

比如List是一个数据类型类型,但是List并没有限制存放数据的类型。有List<Int>, List<String>等等。

泛型类:

// 泛型类
class MyClass<T> {
	fun method(param: T): T {
		return param
	}
}

// 使用
val myClass = MyClass<Int>()
val result = myClass.method(123)

如果我们不想定义一个泛型类,只想定义一个泛型方法:

class MyClass {
	fun <T> method(param: T): T {
		return param
	}
}

// 使用
val myClass = MyClass()
// 指定泛型Int,也可省略,Kotlin可以推导出来
val result = myClass.method<Int>(123)

限制泛型类型

class MyClass {
	// 类型只能是数字类型,比如Int, Float, Double
	fun <T : Number> method(param: T): T{
		return param
	}
}

高级特性

泛型实化

java的泛型被很多人称作伪泛型,因为java的泛型功能是通过类型擦除机制实现的。就是说泛型对于类型的约束只在编译期存在,运行的时候会将类型擦除,统一变成Object类型。

这种机制使得我们无法知道运行时的泛型具体类型,即不可能使用a is T, T::class.java这样的语法。因为T的实际类型在运行的时候已经被擦除了。

然而,Kotlin有内联函数的概念,依托内联函数的特性,可以将内联函数的泛型进行实化。示例代码如下:

// 这段代码实现了java中不可能实现的功能
inline fun <reified T> getGenericType() {
	return T::class.java
}

//  测试
fun main() {
	val result1 = getGenericType<String>()
	val result2 = getGenericType<Int>()
	// java.lang.String
	println("result1 is $result1")
	// java.lang.Integer
	println("result2 is $result2")
}

泛型实化功能允许我们在泛型函数中获得泛型的实际类型。借助Kotlin泛型实化,启动Activity我们可以有更好的写法

// 传统写法
val intent = Intent(context, TextActivity::class.java)
context.startActivity(intent)

// 泛型实化的写法
inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
	val intent = Intent(context, T:class.java)
	// 用于intent传参
	intent.block()
	context.startActivity(intent)
}

// 使用
startActivity<TestActivity>(context) {
	// intent传参
	putExtra("param1", "data")
	putExtra("param2", 123)
}
泛型协变

概念:一个泛型类或者泛型接口中的方法,方法的参数列表是接收数据的地方,称为in位置,方法的返回值实输出数据的地方,称为out位置.

interface MyClass<T> {
	// in: (param: T), out: T
	fun method(param: T): T
}

先看下面代码:

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int): Person(name, age)
class Teacher(name: String, age: Int): Person(name, age)

问题一:如果某个方法接收一个Person类型的参数,而我们传入一个Student实例,合法吗?
答:合法

问题二:如果某个方法接受一个List<Person>类型参数,而我们传入一个List<Student>的实例,合法吗?
答:java中不合法,Kotlin中合法

java中不合法:是因为虽然StudentPerson的子类,但是List<Person>并不是List<Student>的子类,否则存在类型转化的风险。看一下有何风险:

// 定义一个泛型类,就提供泛型data的get和set功能
class SimpleData<T> {
	private var data: T? = null
	
	fun set(t: T?) {
		data = t
	}
	fun get(): T? {
		return data
	}
}

fun main() {
	val student = Student("Tom", 19)
	val simpleData = SimpleData<Student>()
	// 这里实际上编译不通过,仅为了说明风险
	simpleData.set(Student)
	handleSimpleData(simpleData)
	// 原来SimpleData<Student>内部封装的是Student数据
	// 到这里,SimpleData<Student>却实际包含了一个Teacher实例
	// 此时必然有转换异常
	val studentData = simpleData.get()
}

// SimpleData的泛型类型为:Person
fun handleSimpleData(data: SimpleData<Person>) {
	val teacher = Teacher("JACK", 35)
	// main函数传入的是Student类型,这里改成了Teacher类型
	data.set(teacher)
}

回顾上面代码,问题的主要原因是我们在handleSimpleData()方法中向SimpleData<Person>设置了一个Teacher实例。如果SimpleData在泛型T上是只读的话,就没有类型转换的风险了,这个时候SimpleData<Student>就可以成为SimpleData<Person>的子类

协变定义:一个泛型类MyClass<T>, 如果A是B的子类型,同时MyClass<A>MyClass<B>的子类型,那么MyClass在T这个泛型上是协变的。

即要保证一个泛型类在其泛型类型的数据上只读,要实现这一点,MyClass<T>类中的所有方法的泛型T只能出现在out位置上,不能出现在in位置上。

class SimpleData<out T>(val data: T?) {
	fun get(): T {
		return data
	}
}

由于泛型T不能出现在in位置上,因此我们也就不能使用set()方法为data赋值了。

fun main() {
	val student = Student("Tom", 19)
	val data = SimpleData<Student>(student)
	// 由于SimpleData类做了协变声明,所以这么做是安全的
	handleMyData(data)
	val studentData = data.get()
}

fun handleMyData(data: SimpleData<Person>) {
	val personData = data.get()
}

Kotlin默认给许多内置的API加上了协变声明,其中包括各种集合的类与接口。Kotlin中List本身是只读的,意味着它天然可以协变。如果想要给List添加数据,需要使用MutableList才行。

泛型逆变

定义:定义一个泛型类MyClass<T>,如果A是B的子类,同时MyClass<B>又是MyClass<A>的子类型,那么称MyClass在T这个泛型上是逆变的,逆变要求泛型T只能出现在in位置上。从定义上看,逆变和协变完全相反。

逆变有什么应用场景呢?看下面例子:

interface Transformer<T> {
	fun transform(t: T): String
}

定义一个Transformer接口,用于将类型T转换称String类型。

fun main() {
	val trans = object: Transformer<Person> {
		override fun transform(t: Person): String {
			return "${t.name} ${t.age}"
		}
	}
	// 这里编译不过,会报错
	handleTransformer(trans)
}

fun handleTransformer(trans: Transformer<Student>) {
	val student = Student("Tom", 19)
	val result = trans.transform(student)
}

从安全角度分析,上面代码是没有任何问题的。

首先,我们写了一个匿名类实现接口Transformer,用于将Person对象转换称字符串。

然后,handleTransformer()方法希望将Student对象转换成字符串。因为Student是Person的子类,所以匿名类中对Person转换成字符串的逻辑也适用于Student类。

但代码却编译不过,因为Transformer<Person>并不是Transformer<Student>的子类型。所以,我们需要告诉编译器,Transformer<Person>Transformer<Student>的子类型,这样就能正常编译通过。

这个时候,逆变就派上用场了。逆变就是专门用于处理这种情况的。代码如下:

// 泛型T只能出现在in位置上
interface Transformer<in T> {
	fun transform(t: T): String
}

逆变在Kotlin内置API中应用也比较多,比较典型的例子就是Comparable接口。源码如下:

interface Comparable<in T> {
	operator fun compareTo(other: T): Int
}

可以看到,Comparable在T这个泛型上就是逆变的。compareTo()是具体的比较逻辑。

问:为什么要让Comparable接口逆变呢?
答:如果使用Comparable<Person>实现了让两个Person对象比较大小的逻辑,那么用这个逻辑去比较两个Student对象的大小也是成立的。因此让Comparable<Person>成为Comparable<Student>的子类也是合情合理的。


20. 委托

类委托

委托是一种设计模式。理念:操作对象自己不去处理某段逻辑,而是把工作委托给另外一个辅助对象去处理。看下面的例子:

class MySet<T>(val helper: HashSet<T>): Set<T> {
	override val size: Int get() = helper.size

	override fun contains(element: T) = helper.contains(element)

	override fun isEmpty() = helper.isEmpty()
	
	override fun iterator() = helper.iterator()
}

可以看到,MySet构造函数接收了一个HashSet参数,这就是一个辅助对象。然后Set接口中所有方法我们都没有实现,而是调用了辅助对象中相应的方法。这就是委托设计模式。

委托模式的好处:

  1. 大部分方法实现调用辅助对象中的方法
  2. 少部分方法由自己重写
  3. 加入一些自己独有的方法

但这种写法的弊端:如果接口中的方法比较多,需要编写大量模板代码。

Kotlin很好的解决了这个问题,即:类委托。使用by关键字:

class MySet<T>(val helper: HashSet<T>) : Set<T> by helper {
	// 添加自己独有方法
	fun hello() = println("hello world")
	
	// 某个方法重新实现
	override fun isEmpty() = false
}

这就是Kotlin的类委托功能,可以帮助我们减少大量的模板代码

属性委托

属性委托就是将一个属性的读写委托给另一个类去完成:

class MyClass {
	// Delegate类需要实现getValue()和setValue()方法
	var p by AnotherClass()
}

// 标准的代码实现模板
class AnotherClass {
	var propValue: Any? = null
	
	operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {
		return propValue
	}

	operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {
		propValue = value
	}
}

当获取MyClass中p属性的值时,会自动调用AnotherClass类的getValue()方法;当给MyClass中p属性赋值的时会自动调用AnotherClass类的setValue()方法。

Kotlin中的懒加载就是借助属性委托实现的:

val uriMatcher by lazy {
	val mather = UriMatcher(UriMatcher.NO_MATCH)
	matcher.addURI(authority, "book", bookDir)
	matcher.addURI(authority, "book/#", bookItem)
	matcher.addURI(authority, "category", categoryDir)
	matcher.addURI(authority, "category/#", categoryItem)
	matcher
}

21. infix函数

就是Kotlin提供的一种语法糖,让代码可读性更好

// to函数是一个infix函数,其实就是 "Apple".to(1)
val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3)

to函数定义大概如下:

infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

infix函数允许我们将函数调用时的小数点、括号等计算机相关语法去掉,从而使用一种更接近英语的语法来编写程序,让代码看起来更加具有可读性。


22. 协程

基本概念

协程并不在Kotlin的标准API中,以依赖库的形式提供的,需要添加依赖:

dependencies {
	implementation 'org.jetbrains. kotlinx:kotlinx-coroutines-core:1.1.1'
	implementation 'org.jetbrains. kotlinx:kotlinx-coroutines-android:1.1.1'
}
创建协程:GlobalScope.launch
fun main() {
	// 开一个顶层协程
	GlobalScope.launch {
		println("codes fun in coroutine scope")
	}
}

mark:

  1. GlobalScope.launch函数会创建一个顶层协程
  2. 这种协程会随应用程序的结束而被强制中断
  3. 即,上面代码是看不到打印的,main()函数运行结束就会强制中断该协程。
创建协程:runBlocking
fun main() {
	runBlocking {
		println("codes run in coroutine scope")
		delay(1500)
		println("codes run in coroutine scope finished")
	}
}

mark:

  1. delay()函数用于挂起当前协程一段时间,不会影响其它协程
  2. runBlocking函数也会创建一个协程作用域
  3. 和GlobalScope.launch不同的是:runBlocking会保证其作用域内的代码都会被执行到,在执行完之前一直阻塞当前线程。
  4. 所以使用要注意,容易产生性能问题
创建子协程:launch
fun main() {
	runBlocking {
		// 创建子协程
		launch {
			println("launch1")
			delay(1000)
			println("launch1 finished")
		}
		// 创建子协程
		launch {
			println("launch2")
			delay(1000)
			println("launch2 finished")
		}
	}
}

mark:

  1. lauch函数只能在协程作用域中才能调用
  2. 它会在当前协程作用域下创建子协程
  3. 如果外部协程运行结束,其作用域下子协程也会一并结束。
让函数拥有协程作用域 coroutineScope

随着launch函数中逻辑越来越复杂,需要将代码提取到一个单独的函数中。那如何让函数拥有协程作用域呢?

首先需要将函数声明为挂起函数

suspend fun pringDot() {
	println(".")
	// 挂起函数中可以调用delay()函数
	delay(1000)
}

挂起函数还没有协程作用域,还不能调用launch函数。

需要借助coroutineScope函数,coroutineScope也是一个挂起函数,它的特点会继承外部的协程作用域并创建一个子作用域。

suspend fun printDot() = coroutineScope {
	launch {
		println(".")
		deley(1000)
	}
}

和runBlocking有点类似,coroutineScope可以保证其作用域内的所有代码和子协程都可以被执行到,在执行完之前,会一直阻塞当前协程。不同的是:runBlocking会阻塞当前线程

fun main() {
	runBlocking {
		coroutineScope {
			launch {
				println(".")
				delay(1000)
			}
		}
		println("coroutineScope finished")
	}
	println("runBlocking finished")
}
取消协程

不管是GlobalScope.launch函数还是launch函数,都会返回一个Job对象,调用Job对象的cancel方法就可以取消协程

val job = GlobalScope.launch {
	// 处理具体逻辑
}
job.cancel()
项目中对协程比较常用的写法
val job = Job()
// 这像个类,其实是方法,kotlin有意为之
val scope = CoroutineScope(job)
scope.launch {
	// 处理决堤逻辑
}
// 这里cancel,可以将同一作用域内的所有协程全部取消
// 方便管理
job.cancel()
获取协程代码执行结果 async()函数

async函数可以获取协程的执行结果。async函数必须在协程作用域内才能调用,它会创建一个新的子协程并返回一个Deferred对象,调用Deffered对象的await()方法可以获取结果。

fun main() {
	runBlocking {
		val result = async {
			5 + 5
		}.await()
		// 获取协程返回值:10
		println(result)
	}
}

另外,await()方法会将当前协程阻塞住,直到可以获得async函数的执行结果。

withContext()

withContext()是一个挂起函数,可以理解成async()函数的简化写法。

fun main() {
	runBlocking {
		val result = withContext(Dispatchers.Default) {
			5 + 5
		}
		println(result)
	}
}

withContext()函数基本相当于:val result = async{ 5 + 5 }.await()

不一样的地方就是:withContext()需要指定一个线程参数。这个线程参数用于给协程指定一个具体运行的线程。

线程参数有3种值可选:

  • Dispatchers.Default:低并发线程策略,CPU密集型
  • Dispatchers.IO:高并发线程策略,网络
  • Dispatchers.Main:表示不会开启子线程

协程简化回调写法

java方式匿名内部类回调写法:

HttpUtil.sendHttpRequest(url, object : HttpCallbackListener {
	override fun onFinish(response: String) {

	}
	
	override fun onError(e: Exception) {
		
	}
})

这种写法特别繁琐,每请求一次就得写一遍匿名内部类。借助suspendCoroutine函数就能将这种传统回调写法大幅度简化。

suspendCoroutine函数必须在协程作用域或者挂起函数中才能调用。它接收一个Lambda表达式作为参数,主要作用:

  1. 将当前协程立即挂起,
  2. 然后在一个普通线程中执行Lambda表达式当中的代码。
  3. Lambda表达式的参数列表上会传入一个Continuation参数,调用resume()或者resumeWithException()可以让协程恢复执行.

示例代码:

suspend fun request(url: String): String {

	return suspendCoroutine { continuation -> 
		HttpUtil.sendHttpRequest(url, object: HttpCallbackListener {
			override fun onFinish(response: String) {
				// 继续执行被挂起的协程
				continuation.resume(response)
			}
			
			override fun onError(e: Exception) {
				continuation.resumeWithException(e)
			}
		})

	}
}

接下来代码,就很方便了

suspend fun getBaiduResponse() {
	try {
		val result = request("https://www.baidu.com")
	} catch (e: Exception) {

	}
}

由于getBaiduResponse()被声明成挂起函数,这样它也只能在协程作用域或其它挂起函数中调用。所以suspendCoroutine()肯定要结合协程一起使用。通过合理的项目架构设计,可以轻松地将各种协程代码应用到一个普通项目当中。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值