Kotlin要点
这是我参与「第四届青训营 」笔记创作活动的第1天
变量类型
变量:val为不可变变量,var为可变变量
函数
函数:fun 函数名(变量名: 变量类型 (可以加=赋初值,在构建函数时赋值的变量可以不必传参), 变量名: 变量类型...)(: 返回值)可不加
{
执行逻辑....
}
fun methodName(param1: Int = 0,param2: Int): Int{
return param2
}
fun methodName(param1: Int = 0,param2: Int) = param2 //可简化为这样子
//调用函数时
val number = methodName(456,123)
val number = methodName(param2 = 123) //可以不必传入param1,param1将为默认值
val number = methodName(param2 = 123,param1 = 456) //如果有前面的变量名,即使顺序颠倒也无所谓
if
if语句:if语句和java差不多,但是本身具有返回值,返回值是if语句每一个条件中最后一行代码的返回值
fun largerNumber(num1: Int,num2: Int) = if(num1 > num2) num1 else num2
when
when语句:when语句允许传入任意类型的参数并进行匹配执行相应的逻辑
fun getScore(name: String) = when(name){
name.startsWith("Tom") -> 86 //如果这个字符串以Tom开头,则返回值均为86
"Jim" -> 77
else -> 0
}
//还可以进行类型匹配
fun checkNumber(num: Number){ //Number是一个与数字相关的抽象类,Int等都是它的子类
when (num){
is Int -> println("number is Int")
....
}
}
for
for循环:
for (i in 0..10) //表示为0到10的闭区间
for (i in 0 until 10) //表示0到9的闭区间
for (i in 0..10 step 2) //表示0到10的闭区间并每次增2
for (i in 10 downTo 1) //表示从10到1的闭区间降序
继承
继承:默认的类无法被继承,必须加open关键字
open class Person{
...
}
// 继承
class Student : Person(){
...
}
构造函数
open class Person(val name: String,val age: Int){
...
}
class Stuent(val sno: String, val grade: Int,name: String age: Int) : Person(name,age){ //在创建对象时必须传入要求的参数,包括父类要求传的参数,但是父类的参数不需要加val或者var
init{ //在主构造函数执行时,将会init 结构体中的代码
....
}
constructor(name: String,age: Int) : this("",0,name,age){ //所有的次构造函数都必须调用主构造函数
}
constructor() : this("",0){} //这个构造函数通过调用上一个次构造函数进行构造
}
//范例
val student = Student("123",5,"Jack",19)
//如果一个类只有次构造函数,没有主构造函数,次构造函数只能调用父类的构造函数
class Stuent: Person{
constructor(name: String,age: Int) : super(name,age){
}
}
接口
interface Study{
fun readBooks(){...}
}
//调用
class Student(val sno: String, val grade: Int) : Study{
override fun readBooks(){ //如果接口对应的函数没有执行逻辑就必须要重写,没有重写就默认调用接口的
...
}
}
修饰符
修饰符:public 所有类可见(默认)
private 当前类可见
protected 当前类和子类可见
internal 同一模块的类中可见
数据类:如果两个对象中的数据相同,如cellphone1与cellphone2里的数据一样,则两个对象==时返回为 true,如果没有定义数据类得到的结果为false
data class Cellophone(val brand: String,val price: Double) //创建数据类
单例类:全局中最多拥有一个实例,避免创建重复的对象
object Singleton{
fun ...
}
//调用
Singleton.函数名()
集合
val list = listOf("Apple","Banana","Orange","Pear") //初始化不可变集合
val list = mutableListOf("Apple","Banana","Orange","Pear") //初始化可变集合
for (fruit in list){...} //遍历集合并执行相应逻辑
val map = mutableMapOf(1 to "Apple",2 to "Banana",3 to "Orange",4 to "Pear") //初始化可变Map
for ((number,fruit) in map){...} //遍历map
Lambda表达式
语法结构
{参数名1: 参数类型,参数名2: 参数类型 -> 函数体}
val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length })
//化简规则
//当Lambda参数是函数的最后一个参数时,可以将Lambda表达式移到函数括号外面
val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length }
//如果Lambda参数是函数的唯一一个参数的话,可以将函数的括号省略
val maxLengthFruit = list.maxBy { fruit: String -> fruit.length }
//由于类型推导机制,Lambda表达式中的参数列表大多数情况下不需要写参数类型
val maxLengthFruit = list.maxBy { fruit -> fruit.length }
//Lambda表达式的参数列表如果只有一个参数,不必声明参数名,用it关键字代替
val maxLengthFruit = list.maxBy { it.length }
集合函数API
map
val list = listOf("Apple","Banana","Orange","Pear")
val UpperCaseList = list.map { it.toUpperCase } //map API可以将集合中的每个元素都映射成另一个值类似数学中x与y对应关系,例子中将集合中的字符全部改成大写
filter
val newList = list.filter { it.length <= 5 } //filter API可以以某种规则过滤集合里面的参数并形成一个新集合,例子中将list长度≤5的字符放到新集合中
//调用Java方法也可以实现API,但是接口中只有一个待实现的方法时可以使用,例如
Thread{
....
}.start()
button.setOnClickListener{
....
}
any
val anyResult = list.any { it.length <= 5 } //any API可以判断集合中是否存在满足的参数,返回真,假
all
val allResult = list.all { it.length <= 5 } //all API可以判断集合中是否全部元素符合条件,返回真假
with
val result = with(StringBuilder()) { //with把StringBuilder对象传到Lambda表达式内使用
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
toString()
}
run
val result = StringBuilder().run { //和with差不多,就改一点点格式
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
toString()
}
apply
val result = StringBuilder().apply { //apply只能返回StringBuilder对象,没办法返回值
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
}
空指针
空指针检查:Kotlin默认所有的参数和变量都不可为空,在类型后加?表示该类型可空
val a: Int?
val study: Study?
//判空操作符 ?. 当参数为空时不执行
fun doStudy(study: Study?){
study?.readBooks()
study?.doHomework()
}
//判空分支符 ?:
val c = a ?: b //当a不为空时执行a否则为b
//非空断言符 !! 使用!!,即使编译器报空指针错误也可以通过,但建议最好不用
//let函数
study?.let{ //let函数可以将study对象当作参数传入到Lambda表达式中并且使用
it.readBooks()
it.doHomework()
}
字符串内嵌表达式
语法规则
"hello, ${obj.name}. nice to meet you!"
//如果仅有一个变量可以将大括号舍去
"hello, $name. nice to meet you!"
//例子
"brand = $brand , price = $price"
定义静态方法
object,缺点:会将整个类都变成类似静态方法的调用方式
object Util {
fun doAction() {
println("do action")
}
}
companion object,可以只定义某个方法为静态方法,但以上两种都不算真的静态方法
class Util {
fun doAction1() {
println("do action1")
}
companion object {
fun doAction2() {
println("do action2")
}
}
}
注解,不常用
class Util {
fun doAction1() {
println("do action1")
}
companion object {
@JvmStatic
fun doAction2() {
println("do action2")
}
}
}
顶层方法,在里面写的所有方法都是静态方法,创建一个类型为File的Kotlin的文件
fun doSomething() {
println("do something")
}
//在Kotlin中调用
doSomething()
//在Java中调用
文件名Kt.doSomething()
延迟初始化
在定义和使用全局变量时,尽管你保证onCreate方法会在onClick方法之前调用但是还是得加判空判断符
class MainActivity : AppCompatActivity(), View.OnClickListener {
private var adapter: MsgAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
...
adapter = MsgAdapter(msgList)
...
}
override fun onClick(v: View?) {
...
adapter?.notifyItemInserted(msgList.size - 1)
...
}
}
延迟初始化使用的是lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为null了。
class MainActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var adapter: MsgAdapter
override fun onCreate(savedInstanceState: Bundle?) {
...
adapter = MsgAdapter(msgList)
...
}
override fun onClick(v: View?) {
...
adapter.notifyItemInserted(msgList.size - 1)
...
}
}
另外,还可以通过代码来判断一个全局变量是否已经完成了初始化,这样在某些时候能够有效地避免重复对某一个变量进行初始化操作
if (!::adapter.isInitialized) {
adapter = MsgAdapter(msgList)
}
密封类
密封类的关键字是sealed class,新建一个kotlin文件,密封类中包含着所有的分支类
sealed class MsgViewHolder(view: View) : RecyclerView.ViewHolder(view)
class LeftViewHolder(view: View) : MsgViewHolder(view) {
val leftMsg: TextView = view.findViewById(R.id.leftMsg)
}
class RightViewHolder(view: View) : MsgViewHolder(view) {
val rightMsg: TextView = view.findViewById(R.id.rightMsg)
}
class MsgAdapter(val msgList: List<Msg>) : RecyclerView.Adapter<MsgViewHolder>() {
...
override fun onBindViewHolder(holder: MsgViewHolder, position: Int) {
val msg = msgList[position]
when (holder) {
is LeftViewHolder -> holder.leftMsg.text = msg.content
is RightViewHolder -> holder.rightMsg.text = msg.content
}
}
...
}
扩展函数
相比于定义一个普通的函数,定义扩展函数只需要在函数名的前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中了。
语法结构
fun ClassName.methodName(param1: Int, param2: Int): Int {
return 0
}
假如想在String类中添加一个扩展函数用来统计字符串内的字母数量,因此需要先创建一个String.kt文件,并编写如下代码
fun String.lettersCount(): Int {
var count = 0
//现在我们将lettersCount()方法定义成了String类的扩展函数,那么函数中就自动拥有了String实例的上下文。因此 lettersCount()函数就不再需要接收一个字符串参数了,而是直接遍历this即可,因为现在this就代表着字符串本身。
for (char in this) {
if (char.isLetter()) {
count++
}
}
return count
}
Kotlin中的String甚至还有reverse()函数用于反转字符串,capitalize()函数用于对首字母进行大写,等等,这都是Kotlin语言自带的一些扩展函数。
运算符重载
创建一个Money类,重载 “+” 号
class Money(val value: Int) {
operator fun plus(money: Money): Money {
val sum = value + money.value
return Money(sum)
}
operator fun plus(newValue: Int): Money {
val sum = value + newValue
return Money(sum)
}
}
调用
val money1 = Money(5)
val money2 = Money(10)
val money3 = money1 + money2 //15
val money4 = money3 + 20 //35
| 表达式 | 实际调用函数 |
|---|---|
| a + b | a.plus(b) |
| a - b | a.minus(b) |
| a * b | a.times(b) |
| a / b | a.div(b) |
| a % b | a.rem(b) |
| a++ | a.inc() |
| a– | a.dec() |
| +a | a.unaryPlus() |
| -a | a.unaryMinus() |
| !a | a.not() |
| a == b | a.equals(b) |
| a > b a < b a >= b a <= b | a.compareTo(b) |
| a…b | a.rangTo(b) |
| a[b] | a.get(b) |
| a[b] = c | a.set(b,c) |
| a in b | a.contains(b) |
高阶函数
定义:如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。
例子
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
->左边的部分就是用来声明该函数接收什么参数的,多个参数之间使用逗号隔开,如果不接收任何参数,写一对空括号就可以了。而->右边的部分用于声明该函数的返回值是什么类型,如果没有返回值就使用Unit,它大致相当于Java中的void。
Lambda表达式写法
fun main() {
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2) { n1, n2 -> n1 + n2 }
val result2 = num1AndNum2(num1, num2) { n1, n2 -> n1 - n2 }
println("result1 is $result1")
println("result2 is $result2")
}
高阶函数加扩展函数
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().build {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
}
println(result.toString())
}
高阶函数转译的字节码
public static int num1AndNum2(int num1, int num2, Function operation) {
int result = (int) operation.invoke(num1, num2);
return result;
}
public static void main() {
int num1 = 100;
int num2 = 80;
int result = num1AndNum2(num1, num2, new Function() {
@Override
public Integer invoke(Integer n1, Integer n2) {
return n1 + n2;
}
});
}
注意:之前的Lambda表达式在这里变成了Function接口的匿名类实现,然后在invoke()函数中实现了n1 + n2的逻辑,并将结果返回。我们每调用一次Lambda表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。
高阶函数的的进阶运用
SharedPreferences原来的基本用法
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name", "Tom")
editor.putInt("age", 28)
editor.putBoolean("married", false)
editor.apply()
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)
进阶运用
//扩展函数
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", 28)
putBoolean("married", false)
}
//谷歌本身就写好了edit函数可以直接使用,用法与上面类似
//vararg对应的就是Java中的可变参数列表,我们允许向这个方法传入0个、1个、2个甚至任意多个Pair类型的参数,这些参数都会被赋值到使用vararg声明的这一个变量上面
fun cvOf(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 Boolean -> put(key, value)
is String -> put(key, value)
is Byte -> put(key, value)
is ByteArray -> put(key, value)
null -> putNull(key)
}
}
}
//使用
val values = cvOf("name" to "Game of Thrones", "author" to "George Martin","pages" to 720, "price" to 20.85)
db.insert("Book", null, values)
//谷歌也写好了类似的扩展函数contentValuesOf用法把cvof替换即可
内联函数
inline
为了解决每次创建一个匿名类实例所造成的性能损失
调用方法:在函数的fun前加inline
内联函数的工作原理:Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。约等于在一个地方调用一次就将函数代码插入到调用的地方去执行
第一步
第二步
最终结果
使用内联函数一般满足下列情况
- 参数中带有lanbda函数的高阶函数
- 函数体实现简单,代码量不多
- 可能会被多次调用
如果代码量过多且多次调用,复制粘贴的代码会极大的增大代码的体积
noinline
使用该关键字可以选择不内联的函数
inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {
}
内联函数的返回
如果不使用内联函数,只能进行局部返回
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")
if (s.isEmpty()) return@printString //返回时必须要这样子写,相当于在printString函数中返回到main函数
println(s)
println("lambda end")
}
println("main end")
}
//程序运行结果
//main start
//printString begin
//lambda start
//printString end
//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")
if (s.isEmpty()) return //在内联函数中可以直接写return,不过由于内联函数会将函数代码粘贴复制到调用函数,所以这里返回相当于在main函数中直接返回,程序直接结束
println(s)
println("lambda end")
}
println("main end")
}
//程序运行结果
//main start
//printString begin
//lambda start
crossinline
inline fun runRunnable(block: () -> Unit) { //在block前加crossinline,编译就能通过了
val runnable = Runnable {
block()
}
runnable.run()
}
//这段代码在没加inline时可以正常运行,但是加了之后就不能了
上述代码实际上是在匿名类中调用了传入的函数类型参数。而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在
匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回。
crossinline关键字就像一个契约,它用于保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了,问
题也就巧妙地解决了,如果想return的话就用return@runRunnable的写法进行局部返回
泛型
定义一个泛型
//此时的MyClass就是一个泛型类,MyClass中的方法允许使用T类型的参数和返回值。
class MyClass<T> {
fun method(param: T): T {
return param
}
}
//在调用MyClass的时候,可以指定它的类型,那么用这个泛型的方法将会使用这个类型
val myClass = MyClass<Int>()
val result = myClass.method(123)
//由于kotlin的类型推导机制,在使用该方法的时候,传入什么参数,就会默认该泛型为该参数的类型,所以可以省略泛型的指定
val myClass = MyClass()
val result = myClass.method(123)
泛型的实化
怎么写才能将泛型实化呢?首先,该函数必须是内联函数才行,也就是要用inline关键字来修饰该函数。其次,在声明泛型的地方必须加上reified关键字来表示该泛型要进行实化
inline fun <reified T> getGenericType() = T::class.java
//获取该泛型的实际类型
fun main() {
val result1 = getGenericType<String>()
val result2 = getGenericType<Int>()
println("result1 is $result1")//结果:result1 is String
println("result2 is $result2")//结果:result1 is Int
}
这种写法在java中是不可能通过的,在java中会实行类型擦除的机制,即为泛型对于类型的约束只在编译时期存在,假设我们创建了一个List集合,虽然在编译时期只能向集合中添加字符串类型的元素,但是在运行时期JVM并不能知道它本来只打算包含哪种类型的元素,只能识别出来它是个List
泛型实化的应用
inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
val intent = Intent(context, T::class.java)
intent.block()
context.startActivity(intent)
}
//启动别的activity时
startActivity<TestActivity>(context) {
putExtra("param1", "data")
putExtra("param2", 123)
}
泛型的协变
一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为in位置,而它的返回值是输出数据的地方,因此可以称它为out位置,如图所示
泛型协变定义
假如定义了一个MyClass的泛型类,其中A 是B的子类型,同时MyClass又是MyClass的子类型,那么我们就可以称MyClass在T这个泛型上是协变的。
如何才能让MyClass成为MyClass的子类型呢?当且仅当这个泛型类型在数据上是只读的,并且两个类型具有继承关系,否则编译器无法通过并且会出现类型转换的隐患,如果一个泛型类在其泛型类型的数据上是只读的话,那么它是没有类型转换安全隐患的。而要实现这一点,则需要让MyClass类中的所有方法都不能接收T类型的参数。换句话说,T只能出现在out位置上,而不能出现在in位置上
例子
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 data = SimpleData<Student>()
data.set(student)
handleSimpleData(data) // 实际上这行代码会报错,这里假设它能编译通过
val studentData = data.get()
}
fun handleSimpleData(data: SimpleData<Person>) {
val teacher = Teacher("Jack", 35)
data.set(teacher)
}
泛型协变
class SimpleData<out T>(val data: T?) {
fun get(): T? {
return data
}
}
fun main() {
val student = Student("Tom", 19)
val data = SimpleData<Student>(student)
handleMyData(data)
val studentData = data.get()
}
fun handleMyData(data: SimpleData<Person>) {
val personData = data.get()
}
这里对SimpleData类进行了改造,在泛型T的声明前面加上了一个out关键字。这就意味着现在T只能出现在out位置上,而不能出现在in位置上,同时也意味着SimpleData在泛型T上是协变的。更改后的SimpleData类还有main方法就可以通过编译并且正常运行
由于泛型T不能出现在in位置上,因此我们也就不能使用set()方法为data参数赋值了,所以这里改成了使用构造函数的方式来赋值。你可能会说,构造函数中的泛型T不也是在in位置上的吗?没错,但是由于这里我们使用了val关键字,所以构造函数中的泛型T仍然是只读的,因此这样写是合法且安全的。另外,即使我们使用了var关键字,但只要给它加上private修饰符,保证这个泛型T对于外部而言是不可修改的,那么就都是合法的写法。
泛型协变的源码应用
如果某个方法接收一个List类型的参数,而传入的却是一个List的实例, 在Java中是不允许这么做的。但是在kotlin中是合法的,因为Kotlin已经默认给许多内置的API加上了协变声明,其中就包括了各种集合的类与接口。Kotlin中的List本身就是只读的,如果你想要给List添加数据,需要使用MutableList才行。既然List是只读的,也就意味着它天然就是可以协变的
public interface List<out E> : Collection<E> {
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
public operator fun get(index: Int): E
}
List在泛型E的前面加上了out关键字,说明List在泛型E上是协变的。不过这里还有一点需要说明,原则上在声明了协变之后,泛型E就只能出现在out位置上,可是你会发现,在contains()方法中,泛型E仍然出现在了in位置上。
这么写本身是不合法的,因为在in位置上出现了泛型E就意味着会有类型转换的安全隐患。但是contains()方法的目的非常明确,它只是为了判断当前集合中是否包含参数中传入的这个元素,而并不会修改当前集合中的内容,因此这种操作实质上又是安全的。那么为了让编译器能够理解我们的这种操作是安全的,这里在泛型E的前面又加上了一个@UnsafeVariance注解,这样编译器就会允许泛型E出现在in位置上了。但是如果你滥用这个功能,导致运行时出现了类型转换异常,Kotlin对此是不负责的。
泛型的逆变
###### 泛型逆变的定义
假如定义了一个MyClass的泛型类,其中A是B的子类型,同时MyClass又是MyClass的子类型,那么我们就可以称MyClass在T这个泛型上是逆变的。协变与逆变的区别如图
有人可能会觉得这怎么可能,A是B的子类怎么能够把B转换成A类型呢,下面通过一个例子来说明一下
//in说明T只能出现在in的位置上而不是out
interface Transformer<in T> {
fun transform(t: 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)
}
泛型逆变的源码应用
Comparable是一个用于比较两个对象大小的接口
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
可以看到,Comparable在T这个泛型上就是逆变的,compareTo()方法则用于实现具体的比较逻辑。那么这里为什么要让Comparable接口是逆变的呢?想象如下场景,如果我们使用Comparable实现了让两个Person对象比较大小的逻辑,那么用这段逻辑去比较两个Student对象的大小也一定是成立的,因此让Comparable成为Comparable的子类合情合理,这也是逆变非常典型的应用。
委托
委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理
类委托
class MySet<T>(val helperSet: HashSet<T>) : Set<T> {
override val size: Int
get() = helperSet.size
override fun contains(element: T) = helperSet.contains(element)
override fun containsAll(elements: Collection<T>) = helperSet.containsAll(elements)
override fun isEmpty() = helperSet.isEmpty()
override fun iterator() = helperSet.iterator()
}
//kotlin的类委托,效果如上,拥有MySet对象的时候可以直接调用helperSet的方法,还可以自己重写helperSet的方法
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
fun helloWorld() = println("Hello World")
override fun isEmpty() = false
}
委托属性
类委托的核心思想是将一个类的具体实现委托给另一个类去完成,而委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。
//这里使用by关键字连接了左边的p属性和右边的Delegate实例,这种写法就代表着将p属性的具体实现委托给了Delegate类去完成。当调用p属性的时候会自动调用Delegate类的getValue()方法,当给p属性赋值的时候会自动调用Delegate类的setValue()方法。
class MyClass {
var p by Delegate()
}
//在Delegate类中我们必须实现getValue()和setValue()这两个方法,并且都要使用operator关键字进行声明。
class Delegate {
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
}
}
getValue()方法要接收两个参数:第一个参数用于声明该Delegate类的委托功能可以在什么类中使用,这里写成MyClass表示仅可在MyClass类中使用;第二个参数KProperty< >是 Kotlin中的一个属性操作类,可用于获取各种属性相关的值,在当前场景下用不着,但是必须在方法参数上进行声明。另外,< >这种泛型的写法表示你不知道或者不关心泛型的具体类型,只是为了通过语法编译而已,有点类似于Java中<?>的写法。至于返回值可以声明成任何类型,根据具体的实现逻辑去写就行了。
setValue()方法也是相似的,只不过它要接收3个参数。前两个参数和getValue()方法是相同的,最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和getValue()方法返回值的类型保持一致。
lazy关键字
private val uriMatcher by lazy {
val matcher = 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
}
使用了lazy关键字,这段代码一开始并不会执行,当这个变量被调用的时候,才会执行里面的代码,并把lambda表达式里面的最后一行作为返回值,背后的实现也是用了委托的形式
infix函数
我们已经多次使用过A to B这样的语法结构构建键值对,其实to并不是kotlin的关键字,而是kotlin提供的一种高级语法糖特性,它只是把编程语言函数调用的语法规则调整了一下而已,例如:A to B这样的写法,实际上等价于A.to(B)的写法。
if ("Hello Kotlin".startsWith("Hello")) {
// 处理具体的逻辑
}
可以改写成
infix fun String.beginsWith(prefix: String) = startsWith(prefix)
if ("Hello Kotlin" beginsWith "Hello") {
// 处理具体的逻辑
}
infix函数的语法规则并不复杂,上述代码其实就是调用的" Hello Kotlin "这个字符串的beginsWith()函数,并传入了一个"Hello"字符串作为参数。但是infix函数允许我们将函数调用时的小数点、括号等计算机相关的语法去掉,从而使用一种更接近英语的语法来编写程序,让代码看起来更加具有可读性。
另外,infix函数由于其语法糖格式的特殊性,有两个比较严格的限制:首先,infix函数是不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类当中;其次,infix函数必须接收且只能接收一个参数,至于参数类型是没有限制的。
协程
什么是协程呢?它其实和线程是有点类似的,可以简单地将它理解成一种轻量级的线程。要知道,我们之前所学习的线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线程之间的切换。而使用协程却可以仅在编程语言的层面就能实现不同协程之间的切换,从而大大提升了并发编程的运行效率。
fun foo() {
a()
b()
c()
}
fun bar() {
x()
y()
z()
}
//在没有开启线程的情况下,先后调用foo()和bar()这两个方法,那么理论上结果一定是a()、b()、c()执行完了以后,x()、y()、z()才能够得到执行。而如果使用了协程,在协程A中去调用foo()方法,协程B中去调用bar()方法,虽然它们仍然会运行在同一个线程当中,但是在执行foo()方法时随时都有可能被挂起转而去执行bar()方法,执行bar()方法时也随时都有可能被挂起转而继续执行foo()方法,最终的输出结果也就变得不确定了。
导入依赖
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2"
开启协程
fun main() {
GlobalScope.launch {
println("codes run in coroutine scope")
delay(1500)//该函数可以将该协程挂起,并且并不会影响到其他协程,不会像sleep函数一样会阻塞该线程的所有进程
println("codes run in coroutine scope finished")
}
Thread.sleep(1000)//如果不挂起线程,这个协程内的内容将无法输出,因为协程还没执行就已经随着主函数结束而结束
}
fun main() {
runBlocking {//该函数可以让主函数等待该协程执行完毕之后才会结束,所以可以不需要休眠线程
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
}
runBlocking函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题。
runBlocking
fun main() {
runBlocking {
launch {
println("launch1")
delay(1000)
println("launch1 finished")
}
launch {
println("launch2")
www.blogss.cn
delay(1000)
println("launch2 finished")
}
}
}
//结果
//launch1
//launch2
//launch1 finished
//launch2 finished
注意这里的launch函数和我们刚才所使用的GlobalScope.launch函数不同。首先它必须在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。
suspend 关键字
随着launch函数中的逻辑越来越复杂,需要将部分代码提取到一个单独的函数中。这个时候就产生了一个问题:我们在launch函数中编写的代码是拥有协程作用域的,但是提取到一个单独的函数中就没有协程作用域了,那么我们该如何调用像delay()这样的挂起函数呢?
suspend fun printDot() {
println(".")
delay(1000)
}//使用它可以将任意函数声明成挂起函数,而挂起函数之间都是可以互相调用的
但是,suspend关键字只能将一个函数声明成挂起函数,是无法给它提供协程作用域的。比如你现在尝试在printDot()函数中调用launch函数,一定是无法调用成功的,因为launch函数要求必须在协程作用域当中才能调用
coroutineScope 函数
上面的问题可以借助coroutineScope函数来解决,coroutineScope函数也是一个挂起函数,因此可以在任何其他挂起函数中调用。它的特点是会继承外部的协程的作用域并创建一个子协程,借助这个特性,我们就可以给任意挂起函数提供协程作用域了。
suspend fun printDot() = coroutineScope {
launch {
println(".")
delay(1000)
}
}
另外,coroutineScope函数和runBlocking函数还有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之后,coroutineScope函数之后的代码才能得到运行。
虽然看上去coroutineScope函数和runBlocking函数的作用是有点类似的,但是coroutineScope函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此是不会造成任何性能上的问题的。而runBlocking函数由于会挂起外部线程,如果你恰好又在主线程中当中调用它的话,那么就有可能会导致界面卡死的情况,所以不太推荐在实际项目中使用。
实际项目中的域构建器
前面说了,GlobalScope.launch由于每次创建的都是顶层协程,一般也不太建议使用,除非你非常明确就是要创建顶层协程。
为什么说不太建议使用顶层协程呢?主要还是因为它管理起来成本太高了。举个例子,比如我们在某个Activity中使用协程发起了一条网络请求,由于网络请求是耗时的,用户在服务器还没来得及响应的情况下就关闭了当前Activity,此时按理说应该取消这条网络请求,或者至少不应该进行回调,因为Activity已经不存在了,回调了也没有意义。那么协程要怎样取消呢?不管是GlobalScope.launch函数还是launch函数,它们都会返回一个Job对象,只需要调用Job对象的cancel()方法就可以取消协程了
val job = GlobalScope.launch {
// 处理具体的逻辑
}
job.cancel()
但是如果我们每次创建的都是顶层协程,那么当Activity关闭时,就需要逐个调用所有已创建协程的cancel()方法,试想一下,这样的代码是不是根本无法维护?
实际项目中比较常用的写法:
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
// 处理具体的逻辑
}
job.cancel()
先创建了一个Job对象,然后把它传入CoroutineScope()函数当中,注意这里的CoroutineScope()是个函数,虽然它的命名更像是一个类。CoroutineScope()函数会返回一个CoroutineScope对象,这种语法结构的设计更像是我们创建了一个CoroutineScope的实例。
有了CoroutineScope对象之后,就可以随时调用它的launch函数来创建一个协程了。现在所有调用CoroutineScope的launch函数所创建的协程,都会被关联在Job对象的作用域下面。这样只需要调用一次cancel()方法,就可以将同一作用域内的所有协程全部取消,从而大大降低了协程管理的成本。
async
launch函数只能用于执行一段逻辑,却不能获取执行的结果,因为它的返回值永远是一个Job对象。那么有没有什么办法能够创建一个协程并获取它的执行结果呢?当然有,使用async函数就可以实现。
async函数必须在协程作用域当中才能调用,它会创建一个新的子协程并返回一个Deferred对象,如果我们想要获取async函数代码块的执行结果,只需要调用Deferred对象的await()方法即可
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val result1 = async {
delay(1000)
5 + 5
}.await()
val result2 = async {
delay(1000)
4 + 6
}.await()
val end = System.currentTimeMillis()
println(result1)
println(result2)
println("cost ${end - start} ms.")
}
}
//结果
//10
//10
//cost 2032 ms.
事实上,在调用了async函数之后,代码块中的代码就会立刻开始执行。当调用await()方法时,如果代码块中的代码还没执行完,那么await()方法会将当前协程阻塞住,直到可以获得async函数的执行结果。
但是上面的写法效率实在是太低下了,协程的运行可以同时执行,根本不需要运行完一个之后再运行下一个
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val deferred1 = async {
delay(1000)
5 + 5
}
val deferred2 = async {
delay(1000)
4 + 6
}
println(result1.await() + result2.await())
println("cost ${end - start} ms.")
}
}
//结果
//20
//cost 1029 ms.
withContext
调用withContext()函数之后,会立即执行代码块中的代码,同时将外部协程挂起。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为withContext()函数的返回值返回,因此基本上相当于val result = async{ 5 + 5}.await()的写法。唯一不同的是,withContext()函数强制要求我们指定一个线程参数
fun main() {
runBlocking {
val result = withContext(Dispatchers.Default) {
5 + 5
}
println(result)
}
}
线程参数主要有以下3种值可选:Dispatchers.Default、Dispatchers.IO和Dispatchers.Main。Dispatchers.Default表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用Dispatchers.Default。Dispatchers.IO表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量,此时就可以使用Dispatchers.IO。Dispatchers.Main则表示不会开启子线程,而是在Android主线程中执行代码
使用协程简化回调
suspendCoroutine
suspendCoroutine函数必须在协程作用域或挂起函数中才能调用,它接收一个Lambda表达式参数,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行Lambda表达式中的代码。Lambda表达式的参数列表上会传入一个Continuation参数,调用它的resume()方法或resumeWithException()可以让协程恢复执行。
suspend fun request(address: String): String {
return suspendCoroutine { continuation ->
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(response: String) {
continuation.resume(response)
}
override fun onError(e: Exception) {
continuation.resumeWithException(e)
}
})
}
}
suspend fun getBaiduResponse() {
try {
val response = request("https://www.baidu.com/")
// 对服务器响应的数据进行处理
} catch (e: Exception) {
// 对异常情况进行处理
}
}
可以看到,request()函数是一个挂起函数,并且接收一个address参数。在request()函数的内部,我们调用了刚刚介绍的suspendCoroutine函数,这样当前协程就会被立刻挂起,而Lambda表达式中的代码则会在普通线程中执行。接着我们在Lambda表达式中调用HttpUtil.sendHttpRequest()方法发起网络请求,并通过传统回调的方式监听请求结果。如果请求成功就调用Continuation的resume()方法恢复被挂起的协程,并传入服务器响应的数据,该值会成为suspendCoroutine函数的返回值。如果请求失败,就调用Continuation的resumeWithException()恢复被挂起的协程,并传入具体的异常原因。
retrofit回调简化
suspend fun <T> Call<T>.await(): T {
return suspendCoroutine { continuation ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
if (body != null) continuation.resume(body)
else continuation.resumeWithException(
RuntimeException("response body is null"))
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
//请求写法
suspend fun getAppData() {
try {
val appList = ServiceCreator.create<AppService>().getAppData().await()
// 对服务器响应的数据进行处理
} catch (e: Exception) {
// 对异常情况进行处理
}
}
首先await()函数仍然是一个挂起函数,然后我们给它声明了一个泛型T,并将await()函数定义成了Call的扩展函数,这样所有返回值是Call类型的Retrofifit网络请求接口就都可以直接调用await()函数了。