温习Android基础知识——《第一行代码(第三版)》读书笔记 Chapter 2 Kotlin语法

第二章:探究新语言,快速入门Kotlin编程

Google在2017年的I/O大会上宣布Kotlin为Android的一级开发语言,之后又在2019年的I/O大会上宣布其成为Android第一开发语言。

目录

Kotlin历史

2011年JetBrains发布第一个版本,2012年开源,2016年发布1.0正式版。

Why Kotlin?

编程语言大致可以分为两类:编译型语言解释型语言。编译型语言的特点是编译器会将我们编写的源代码一次性地编译成计算机可识别的二进制文件,然后计算机直接执行,像C和C++都属于编译型语言。解释型语言则完全不一样,它有一个解释器,在程序运行时,解释器会一行行地读取我们编写的源代码,然后实时地将这些源代码解释成计算机可识别的二进制数据后再执行,因此解释型语言通常效率会差些,像Python和JavaScript都属于解释型语言。
而Java 是属于编译型语言还是解释型语言呢?
虽然Java代码确实是要先编译再运行的,但是Java代码编译之后生成的并不是计算机可识别的进制文件,而是一种特殊的class文件,这种class文件只有Java 虚拟机( Android中叫ART,一种移动优化版的虚拟机)才能识别,而
这个Java虚拟机担当的其实就是解释器的角色,它会在程序运行时将编译后的class文件解释成计算机可识别的二进制数据后再执行,因此,准确来讲,Java属于解释型语言。所以,其实Java虚拟机并不直接和你编写的Java代码打交道,而是和编译之后生成的class文件打交道。

Kotlin工作原理

Kotlin用自己的编译器,将Kotlin代码编译成同样规格的class文件,Java虚拟机也是可以识别并解释成计算机可识别的二进制数据后再执行。

Kotlin特性

Kotlin语法更加简洁(如无分号)、高级(类型推导机制),在语言安全性方面做得更好(几乎杜绝了空指针异常),并且与Java 100%兼容,既可以直接调用Java编写的代码,也可以无缝使用Java第三方的开源库。

Kotlin变量和函数

变量

  1. Kotlin中声明变量只有两种关键字:val 和
    var。前者用来声明不可变的变量,对应Java中的final变量。后者用来声明一个可变的变量,对应Java中的非final变量。
  2. 如果类型推导机制工作不正常,也可以显示地声明变量类型:
val a : Int = 10
  1. Kotlin中没有基本数据类型,全部使用对象数据类型。
  2. 为什么要使用val,全部使用var岂不美哉?答 : 这是为了解决Java中final关键字没有被合理使用的问题。一个好的编程习惯是:除非一个变量明确允许被修改,否则都应该加上final关键字。所以,应该永远优先使用val来声明一个变量,而当val没有办法满足需求的时候再使用var。

函数(方法)

基本语法规则:

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

这其中,fun是用来定义函数的关键字,后跟函数名称。函数名后面紧跟一对括号,里面可以声明该函数接收哪些参数,参数声明的格式是“参数名:参数类型”。参数括号后部分可省略,用来声明该函数会返回什么类型的数据。

fun largerNumber(num1 : Int, num2 : Int) : Int {
 	return max(num1,num2)
 }
  • 语法糖:当函数中只有一行代码时,不必编写函数体,可以直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可。
    如:
fun largerNumber(num1 : Int, num2 : Int) : Int = max(num1,num2)

根据类型推导机制,上述代码还可以进一步简化为:

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

逻辑控制

if 语句

Kotlin中的if语句相比于Java有一个额外的功能——可以用每一个条件中最后一行代码返回值。(有点像Python,甚至感觉Kotlin就是Python和Java的私生子。。)如:

fun largerNumber(num1 : Int, num2 : Int) : Int {
	 val value = if(num1 > num2){
			 		num1
			 	}else{
			 		num2
			 		}
	return value
 }

它还可以被语法糖简化为:

fun largerNumber(num1 : Int, num2 : Int) = 
	if(num1 > num2) num1 else num2							 		

when 语句(类似于switch语句)

when语句和if语句一样,也可以有返回值。
下列代码

fun getScore(name : String) = 
	 if(name == "Tom"){ 
	 //注意,Kotlin中判断字符串或对象是否相等可以直接使用==关键字
		86
	}else if(name == "Jim"{
		77
	}else if(name == "Jack"{
		95
	}else if(name == "Lily"{
		100
	}else{
		0
	}

可以被简化为:

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

when语句允许传入一个任意类型的参数(Java中的switch只能接收整形或短于整形的变量和字符串变量),然后可以在when的结构体中定义一系列条件,格式为:

匹配值 -> { 执行逻辑 }

除了精确匹配之外,when语句还可以进行类型匹配。如下:

fun checkNumber(number: Number) = when(number){
    is Int -> 1 //is关键字相当于Java中的instanceof关键字
    is Double -> 2
    else -> 0
}

when语句还支持不带参数的用法:

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

循环语句

while循环与Java中相同,就此略过。
Kotlin中无for i 循环和for each循环之分,只有一个for in循环,可用来遍历区间,数组,集合。

val range = 0..10 //闭区间
val rangeUntil = 0 until 10 //左闭右开区间
val rangeDownTo = 10 downTo 1 //闭区间

for (i in 0 until 10 step 2) { //step 步长
   println("而今迈步从头越") //输出五次
}

for (i in 10 downTo 0){
	println("从头越$i") //输出11次 $后跟变量
}

总结:可能不如for i循环灵活,但简单好用,足以应付大多数使用场景。如果遇到for in循环实在无法实现的情况,可以用whlie循环实现。

面向对象编程

Kotlin中新建类的格式如下:

open class Person { //open的含义:代表该类可被继承,或该方法可被覆盖
	var name = ""//记得初始化
	var age = 0
	
    open fun eat(){
        println("$name 吃吃吃 $age years old")
    }
}

Kotlin中实例化类的方式和Java类似,只是去掉了new关键字。

val p = Person()

继承和构造函数

想要实现继承,需要做两件事:
①Kotlin中的非抽象类默认都是不可被继承的(Effective Java 中明确提到,如果一个类不是专门为继承而设计的,那么就应该主动将它加上final声明,禁止其被继承),所以需要让父类可被继承(标记为open)
②Kotlin中继承的关键字不是extends而是 : 。如:

class Student : Person(){
	var sno = ""
	var grade = 0
}	

Kotlin中有两种构造函数,主构造函数和次构造函数。
主构造函数将是最常用的构造函数,每个人都会有一个不带参数(当然也可以显式指明参数)的主构造函数。其特点是没有函数体,直接定义在类名后面。比如下面这种写法:

class Student(val sno : String, val grade : Int) : Person() {
	init{ //Kotlin提供的,用于在主构造函数编写逻辑的结构体
		println("sno is " + sno)
		println("grade is "+grade)
	}
}

由于子类中的构造函数必须调用父类中的构造函数,所以Kotlin借由父类后的括号来指定子类的主构造函数调用的是父类的哪个构造函数。所以,请理解下列代码。

class Student(val sno : String, val grade : Int, name : String, age : Int) :
 Person(name,age) {
    constructor(name: String,age: Int) : this("",0,name,age)

    init {
        println("sno is $sno")
        println("garde is $grade")
    }
    override fun eat(){
        println("$name 吃吃吃 $age years old $sno $grade ")
    }
}

任意一个类只能有一个主构造函数,但可以有多个次构造函数。Kotlin还规定,当一个类既有主构造函数,又有次构造函数时,所有的次构造函数都必须调用主构造函数。代码如下:

class Student(val sno : String, val grade : Int, name : String, age : Int) :
 Person(name,age) {
    constructor(name: String,age: Int) : this("",0,name,age){
    }//次构造函数由constructor关键字定义
    //这里我们定义了两个次构造函数,又通过this关键字调用了主构造函数
	constructor() : this("",0){//该构造器调用第一个次构造函数为name和age参数赋初值
	}//像这样间接调用主构造函数也是合法的

    init {
        println("sno is $sno")
        println("garde is $grade")
    }
    override fun eat(){
        println("$name 吃吃吃 $age years old $sno $grade ")
    }
}

而如果没有主构造函数,只有次构造函数时:

class Student : Person{//此时没有主构造函数,自然就不需要加括号了
	constructor(name :String, age:Int) : super(name, age){
	}//由于Student类没有主构造函数,次构造函数就只能直接调用父类的构造函数
}

接口

interface Study{
	fun readBook()
	fun doHomework(){//接口如果提供了函数体,其所提供的实现为其默认实现
		println(" do homework default implementation")
	}
}		

Kotlin中接口与Java中基本相同,只是Java中的extends和implements关键字都被Kotlin简化成了 : 。当同时继承、实现接口,或实现多个接口时,用逗号隔开即可。

class Student(val sno : String, val grade : Int, 
	name : String, age : Int) : Person(name,age) , Study {
	
    constructor(name: String,age: Int) : this("",0,name,age)

    init {
        println("sno is $sno")
        println("garde is $grade")
    }
    //覆盖(重写)父类函数和实现接口方法时都要加上override关键字
    override fun eat(){
        println("$name 吃吃吃 $age years old $sno $grade ")
    }
    
    override fun readBooks() {
        println(name + " is reading.")
    }

    override fun doHomework() {
        println(name + " is doing homework.")
    }
}

修饰符

修饰符 Java Kotlin
public 所有类可见 所有类可见
private 当前类可见 当前类可见
protected 当前类及其子类,以及同一包路径下的类可见 当前类及其子类可见
default 同一包路径下的类可见
internal 同一模块中的类可见

数据类与单例类

数据类通常需要重写equals hashcode toString这几个方法。Java中需要我们手动完成,而在Kotlin中,只要再class前加上data关键字即可。

data class CellPhone(val brand : String, var price : Double)
//当一个类没有任何代码时,还可以将尾部的大括号省略

单例模式是最常用、最基础的设计模式之一,它可以用来避免创建重复的对象。在Java中实现单例类需要将构造器设为私有,再提供一个公有的获得唯一实例的方法,还要考虑并发安全性,比较麻烦。而在Kotlin中,只要将新建类型指定为object即可。

object Singleton {
    fun singletonTest(){
        println("我是单例类")
    }
}

调用单例类中的函数也很简单,比较类似于Java中静态方法的调用方式。虽然这种写法看上去是静态方法的调用,但其实Kotlin在背后自动帮我们创建了一个Singleton类的实例,并且保证全局只会存在一个Singleton。

Singleton.singletonTest()

Lambda编程

集合的创建和遍历

Kotlin为我们提供了一个listof函数来简化初始化集合的写法(该函数生成的集合是只读集合,如果需要可变集合需要调用mutableListOf函数)(Set类似,不再赘述)

val list = listOf("Apple","Banana","Orange","Grape","Pear")
for(fruit in list) println(fruit) 
//前面提到过,for in循环除区间外还可遍历集合

val mutableFruitList = mutableListOf("Apple","Banana","Orange","Grape","Pear")
mutableFruitList.add("Watermelon")
mutableFruitList.removeIf { t: String -> t == "Orange"  }
mutableFruitList[0] = "apple"
for (fruit in mutableFruitList){
   println(fruit)
}

val carSet = setOf("小轿车","大卡车")
val mutableCarSet = mutableSetOf("小轿车","大卡车")
for (car in carSet){
   println(car)
}

在Kotlin中,对Map的添加和读取操作与Java有很大不同。

    val map = HashMap<String,Int>()
    //传统写法,Java写法
    map.put("Apple",1)
    map.put("Banana",2)
    map.put("Grape",3)
    val valueOfGrape = map.get("Grape")
    println(valueOfGrape)
    //Kotlin写法,Python写法
    map["Apple"] = 0
    val valueOfApple = map["Apple"]
    println(valueOfApple)

当然,Map也有相应的初始化函数。

    val fruitMap = mapOf("Apple" to 1,"Banana" to 2,"Orange" to 3,"Grape" to 4,"Pear" to 5)
    for ((fruit, number) in fruitMap){
        println("fruit is $fruit and number is $number")
        //这里用到了后面讲到的字符串内嵌表达式
    }

集合的函数式API

Lambda就是一小段可以作为参数传递的代码,其语法结构为:

{参数名1 : 参数类型, 参数名2 :参数类型  -> 函数体}

如:

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

其简化过程如下:

//step1 lambda表达式可以直接传入参数
val maxLengthFruit1 = list.maxBy({fruit : String -> fruit.length})
 //step2 当Lambda参数是函数最后一个参数时,可以将Lambda表达式移到括号的外面
val maxLengthFruit2 = list.maxBy(){fruit : String -> fruit.length}
//step3 如果lambda表达式是函数的唯一参数的话,还可以将括号省略
val maxLengthFruit3 = list.maxBy{fruit : String -> fruit.length}
//step4 由于类型推导机制,lambda表达式中的参数列表在大多数情况下都不必声明参数类型
val maxLengthFruit4 = list.maxBy{fruit -> fruit.length}
//step5 当lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是用it关键字来代替
val maxLengthFruit5 = list.maxBy{it.length}

map函数,filter函数,any函数,all函数都是集合中很常用的函数式API。

    //map函数可将集合中的每个元素都映射为另一个值
    val newList = list.map{it.length}
    for(fruit in newList) println(fruit)
    //filter函数可过滤集合中的数据
    val newlist = list.filter { it.length > 5}
                      .map{it.toUpperCase()}
    //这里是先过滤,后映射,why?                  
    for(fruit in newlist) println(fruit)
    
	//any函数用来判断集合中是否有元素符合指定条件
    val anyResult = list.any { it.length<=5 }
    //all函数用来判断集合中所有元素是否都符合指定条件
    val allResult = list.all { it.length<=5 }
    println("anyResult = " + anyResult + " allResult = " +allResult)

Java函数式API

以上是在Kotlin中的函数式API的用法,事实上,在Kotlin中调用Java方法也可以使用函数式API,但有一定条件限制。如果我们在Kotlin代码中调用了一个Java方法,并且该方法接受一个Java函数式接口参数,就可以使用函数式API。比如以下代码是符合这一要求的:

Thread(object : Runnable{
//注意,Kotlin中没有new关键字,所以创建匿名类实例的时候不该用new,而是object
    override fun run(){
        println("Thread1 is running")
   }
}).start()

//可以被函数式API简化为
Thread(Runnable{
	println("Thread2 is running")
}).start()

//如果Java方法的参数列表中只有一个Java单抽象方法接口参数,则可以省略接口名
Thread({
    println("Thread3 is running")
}).start()

//lambda表达式拿出括号后(这里的括号也可以省略)
Thread(){
    println("Thread4 is running")
}.start()

对于为按钮注册点击事件的代码:

button.setOnClickListener(new View.OnClickListener(){
	@Override
	public void onClick(View v){
	}
});	

//上述代码在使用Java中的lambda表达式后可被简化为
button.setOnClickListener(new View.OnClickListener(){
	@Override
	public void onClick(View v){
	}
});	

就可以被简化为:

button.setOnClickListener{
}

注意,这里学习Java函数式API的使用都限定于从Kotlin中调用Java方法,并且函数式接口也必须是用Java语言定义的。

空指针检查

Kotlin利用编译时判空检查的机制几乎解决了空指针异常。

fun main() {
    val a = 0
    val b = 1
    val c = a ?: b
    doStudy(null) //此时这里会提示错误,告知你不能为空

    val str = null
    println(getTextLength(str))


//    content = null
    if (content!=null) printUpperCase()
}

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

也就是说,Kotlin将空指针异常的检查提前到了编译时期。
如果需要为空的时候怎么办呢?Kotlin提供了另外一套可为空的类型系统,只不过在使用可为空的类型系统时,我们需要在编译时期就将所有潜在的空指针异常都处理掉,否则代码将无法编译通过。
当参数类型后面跟一个问号的时候就代表该参数可能为空。

//全局变量判空问题
var study : Study? = null
fun doStudySE() {
    if (study!=null){
 //虽然在这判断了study为非空,但事实上,study作为全局变量随时可能被其他线程修改
//        study.readBooks() Error
//        study.doHomework() Error
    }
}
fun doStudy(study: Study?) {
	if(study!=null){
	    study.readBooks()
	    study.doHomework()
	}    
}

该函数是非并发安全的,有可能在判断完成之后,其他线程就将这里的study设置为 null了,所以我们就需要?.操作符(当对象为空时正常使用相应的方法,当对象为空时什么都不做)。

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

还有一个?:操作符与此相似,这个操作符左右两边都接收一个表达式,如果左侧表达式的结果不为空就返回左侧表达式的结果,否则返回右侧表达式的结果。

fun getTextLength(text : String?) = text?.length ?: 0

如果我们想强行通过编译,可以使用非空断言工具!!

fun printUpperCase(){//非空断言工具
    val upperCase = content!!.toUpperCase()
    println(upperCase)
}

let函数提供了函数式API,并将原始调用对象作为参数传给lambda表达式中。它可以处理if语句无法处理的全局变量的判空问题。

private fun doStudyPro(study: Study?) {
    study?.let { stu ->//这里的study和stu其实是一个对象
        stu.readBooks()
        stu.doHomework()
    }
}

private fun doStudyProPlus(study: Study?) {
    study?.let {
    	//当lambda表达式的参数列表只有一个参数时,可以不声明参数名,直接使用it关键字代替
        it.readBooks()
        it.doHomework()
    }
}

Kotlin中的小魔术

字符串内嵌表达式

用${obj.name}传递表达式,当表达式中只有一个变量时,可省略{}

fun stringEmbeddedExpression(cellPhone: CellPhone){
    println("Cellphone(brand="+ cellPhone.brand +", price="+ cellPhone.price +")")
    println("Cellphone(brand=${cellPhone.brand}, price=${cellPhone.price})")
    println(cellPhone)
}

函数的参数默认值

次构造函数很少使用,原因就在于Kotlin中可为函数设定参数默认值。

fun theDefaultParameterValueOfTheFunction(num : Int = 100, string: String="hello"){
    //通过直接在函数的参数列表里设值,来为参数提供默认值
    println("num=$num string=$string")
}

fun main() {
    theDefaultParameterValueOfTheFunction()
    theDefaultParameterValueOfTheFunction(0)
    //由于传参有顺序,如果想不按顺序传参需要使用键值对传参
    theDefaultParameterValueOfTheFunction(string = "Hello World")
    theDefaultParameterValueOfTheFunction(0,"world")
}

Chapter 3 Kotlin课堂:标准函数和静态方法

标准函数with、run和apply

Kotlin的标准函数指的是Standard.kt文件中定义的函数,任何Kotlin代码都可以自由地调用所有的标准函数。之前用来辅助判空的let函数就是标准函数之一。

with run apply
直接调用,无需对象 需要调用某个对象的本函数 需要调用某个对象的本函数
两个参数 一个参数 一个参数
任意类型对象和Lambda表达式 lambda表达式 lambda表达式
lambda表达式中的最后一行代码可以作为返回值返回 lambda表达式中的最后一行代码可以作为返回值返回 无法指定返回值,只能自动返回调用本函数的对象本身
fun withFun() {
    val list = listOf("Apple","Banana","Pear","Grape")
    //未使用标准函数
    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)

    //with函数
    val result2 = with(StringBuilder()){
        append("Start eating fruits")
        for (fruit in list){
            append(fruit).append("\n")
        }
        append("Ate all fruits.")
        toString()
    }
    println(result2)

    //run函数
    val result3 = StringBuilder().run {
        append("Start eating fruits")
        for (fruit in list){
            append(fruit).append("\n")
        }
        append("Ate all fruits.")
        toString()
    }
    println(result3)
    
    //apply函数
    val result4 = StringBuilder().apply {
        append("Start eating fruits")
        for (fruit in list){
            append(fruit).append("\n")
        }
        append("Ate all fruits.")
    }
    println(result4.toString())

}

静态方法

Kotlin中没有直接定义静态方法的关键字,但是提供了一些语法特效来支持类似于静态方法调用的写法,如:单例类和companion object关键字。
如果确实需要真正的静态方法,有两种实现方式:注解和顶层方法。

注解

在单例类或companion object中的方法加上@JvmStatic注解,那么Kotlin 编译器就会将这些方法编译成真正的静态方法。

顶层方法

顶层方法指的是那些没有定义在任何类中的方法,Kotlin 编译器会将这些方法全部编译成静态方法。这些顶层方法可以在任何位置上被直接调用。

//在JavaTest类的inbokeStaticMethod方法中调用Helper.kt下的dosomeing方法
public class JavaTest{
	public void invokeStaticMethod(){
		HelperKt.doSomething();
	}
}	

Chapter 4 Kotlin课堂:延迟初始化和密封类

第四章提到的其他知识点

as 关键字

Kotlin中用as关键字来进行强制类型转换

kotlin-android-extensions插件

使我们不需findViewById方法就能获取到控件的插件,这个插件只能在Activity和Fragment中使用

标准函数 repeat

传入一个数值n,会将lambda表达式内容执行m遍。

private fun initFruits() {
   repeat(2){
       fruitlist.apply {
           add(Fruit("Apple", R.drawable.apple_pic))
           add(Fruit("Banana",R.drawable.banana_pic))
           add(Fruit("Orange",R.drawable.orange_pic))
           add(Fruit("Watermelon",R.drawable.watermelon_pic))
           add(Fruit("Pear",R.drawable.pear_pic))
           add(Fruit("Grape",R.drawable.grape_pic))
           add(Fruit("Pineapple",R.drawable.pineapple_pic))
           add(Fruit("Strawberry",R.drawable.strawberry_pic))
           add(Fruit("Cherry",R.drawable.cherry_pic))
           add(Fruit("Mango",R.drawable.mango_pic))
       }
   }
}

下划线代替参数

Kotlin允许我们将没有用到的参数使用下划线代替,注意:不能省略,不能改变顺序。

listView.setOnItemClickListener { _, _, position, _ ->
     val fruit = fruitlist[position]
     Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
}

关键字 const

该关键字用来定义常量,只能在单例类,companion object或顶层方法中才可以使用const 关键字。

lateinit 关键字

对全局变量使用该关键字,表明确信它在任何地方调用之前就已经完成了初始化工作,以达到延迟初始化的目的。
如果一个变量使用该关键字后未初始化即使用,将抛出UnititializedPropertyAccessException。

::varname.isInitialized

该语法用来判断varname变量是否已经被初始化

密封类

使用密封类之前,我们明知该变量值只有两个可能也得加上else:

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val msg = msgList[position]
        when(holder){
            is LeftViewHolder -> holder.leftMsg.text = msg.content
            is RightViewHolder -> holder.rightMsg.text = msg.content
            esle throw Exception
        }
    }

为了解决这一问题,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)
    }
}

当when语句传入一个密封类变量作为条件时,Kotlin编译器就会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应的条件全部处理。

注意,受密封类底层实现机制所限制,密封类及其子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中。

Chapter 5 Kotlin课堂:扩展函数和运算符重载

扩展函数

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

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

为了便于查找,建议向哪个类添加扩展函数,就定义一个同名的Kotlin文件,在其中编写扩展函数。当然,扩展函数可以定义在任何一个现有类中,并不一定非要创建新文件,但最后还是将其定义为顶层方法,这样可以让扩展函数拥有全局可见性。

运算符重载

Kotlin允许我们将所有的运算符甚至其他的关键字进行重载,从而扩展这些运算符和关键字的用法。

class Money (val value:Int){

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

    operator fun plus(num : Int) : Money{
        return Money(this.value+num)
    }
}

我们就可以调用了

fun main() {
    val money1 = Money(5)
    val money2 = Money(10)
    val money3 = money1+money2
    println(money3.value)

    val money4 = money3+20
    println(money4.value)
}

这种obj1+obj2的语法会在编译的时候被转换为obj1.pius(obj2)
在这里插入图片描述
在这里插入图片描述

Chapter 6 Kotlin课堂:高阶函数详解

高阶函数

如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么这个函数就被称为高阶函数。

函数类型

类似于整型,布尔型,函数类型即一种类型。
其语法规则为:

 (String, Int) -> Unit

->左侧声明了该函数接收哪些参数,右侧声明该函数的返回值类型,如果没有返回值则使用Unit,它大致相当于Java中的void。

fun num1Andnum2(num1 : Int, num2 : Int, operation : (Int, Int) -> Int) : Int{
//调用一个函数类型的参数,其语法类似于调用一个普通的函数
    return operation(num1,num2)
}

用途

高阶函数允许让函数类型的参数来决定执行逻辑。即使是同一个高阶函数,只要传入不同的函数类型参数,那么它的执行逻辑和最终返回的结果就可能是完全不同的。

fun plus(num1 : Int, num2 : Int) : Int{
    return num1+num2
}

fun minus(num1 : Int, num2 : Int) : Int{
    return num1-num2
}

fun main() {
    val num1 = 100
    val num2 = 80
    //val result1 = num1Andnum2(num1,num2,::plus)
    //val result2 = num1Andnum2(num1,num2,::minus)
    //如果使用lambda表达式
    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")
}
//函数类型前加上ClassName,表示该函数类型是定义在哪个类当中
fun StringBuilder.build(block :StringBuilder.() -> Unit) : StringBuilder{
    block()
    return this
}

fun main() {
    val list = listOf("Apple","Banana","Orange","Grape","Pear")
    val result = StringBuilder().build {
        append("Start eating fruits.\n")
        for (fruit in list){
            append(fruit + "\n")
        }
        append("Ate all fruits.")
    }
    println(result.toString())
}

内联函数

Kotlin通过其编译器,将这些高阶函数的语法转换成Java支持的语法结构。而其背后的实现原理即为创建匿名类。每调用一次lambda表达式,都会创建一个新的匿名类实例,这就会造成额外的内存和性能开销。
为了解决这一问题,Kotlin提供了内联函数(关键字为inline)的功能,它可以将使用lambda表达式带来的运行开销完全消除。

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

内联函数的工作原理

Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方。

noinline和crossinline

noinline

当一个高阶函数接收了两个或者更多函数类型的参数,这时我们给函数加上了Inline关键字,你们Kotlin编译器会自动将所有引用的Lambda表达式全部进行内联。
这时如果我们只想内联其中一个lambda表达式,就可以使用noinline关键字了。被noinline修饰的函数类型就不会被内联了。

inline fun inlineTest(block1:() -> Unit, noinline block2 : () -> Unit){
}
为什么使用内联可以消除运行时开销却不使用?

因为内联的函数类型错参数在编译的时候被进行了代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由的传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是其最大的局限性。
除此之外,内联函数和非内联函数还有一个重要的区别:内联函数所引用的lambda表达式中是可以使用return关键字进行函数返回的,而非内联函数只能进行局部返回。

//lambda表达式中不允许直接调用return关键字,只能进行局部返回
fun main() {
    println("main start")
    val str = ""
    printString(str){ s->
        println("lambda start")
        if (str.isEmpty()) return@printString//局部返回
        println(s)
        println("lambda end")
    }
}

crossinline

将高阶函数声明为内联函数是一种良好的编程习惯,事实上大多数高阶函数都可以直接声明成内联函数,但是也有少部分例外的情况。
下面的代码在没有inline修饰是是可以正常运行的,但加上inline关键字后就会提示错误:

inline fun runRunnable(block: () -> Unit){
    val runnable = Runnable{
        block()
    }
    runnable.run()
}

这里我们在Runnable对象的lambda表达式中调用了传入的函数类型参数,而lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。而内联函数所引用的Lambda表达式是允许使用return关键字进行函数返回的,但由于我们是在匿名类汇总调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数进行返回。
也就是说,如果我们在高阶函数中创建了另外的lambda或者匿名类的实现,并且在这些实现汇总调用了函数类型参数,此时再将高阶函数声明为内联函数就会提示错误。
这个时候如果要使用内联函数,就需要crossinline关键字了。

inline fun runRunnable(crossinline block: () -> Unit){
    val runnable = Runnable{
        block()
    }
    runnable.run()
}

crossinline像一个契约,保证在内联函数的lambda表达式中一定不会使用Return关键字,这样没有了冲突,问题自然就被解决了。
总体来说,除了在return关键字的使用上有所区别之外,crossinline保留了内联函数的其他特性。

Chapter 7 Kotlin课堂:高阶函数应用

其他本章提到的知识点

use函数可保证在lambda表达式中的代码全部执行完之后自动将外层的流关闭。
vararg关键字声明接收参数为可变参数列表。
Any是Kotlin中所有类的基类,相当于Java中的Object。

简化SharedPerferences的用法

fun SharedPreferences.open(block : SharedPreferences.Editor.() -> Unit){
    val editor = edit()
    editor.block()
    editor.apply()
}

简化ContentValues的用法

fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
    val cv = ContentValues()
    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)
        }
    }
}

Chapter 8 Kotlin课堂:泛型和委托

泛型

默认情况下,所有的泛型都可以制定为空,默认泛型上界为Any?,如果想让泛型类型不可为空,去掉?即可。
<*>类似于java中的<?>
这里提到的泛型用法和Java几乎相同,这里只提一点,其他不再赘述。

委托

委托是一种设计模式,其理念是操作对象自己去处理某段逻辑,而是交给另一个辅助对象去处理。
Kotlin将委托功能分为了两种:类委托和属性委托。

类委托

类委托的核心思想是将一个类的具体实现委托给另一个类去完成。
这里我们实现了一个自己的myset,交由参数helperset完成具体方法

class MySet<T> (val helperSet : HashSet<T>, override val size: Int) : Set<T>  {
    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()
    
    fun helloworld() = println("Hello world") 
}

这个代码可以被by关键字简化为:

class MySet<T> (val helperSet : HashSet<T>, override val size: Int) : Set<T> by helperSet {
    fun helloworld() = println("Hello world")
    
    override fun isEmpty() = false
}

属性委托

属性委托的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。

class MyClass {

    var p by Delegate()
    //将p属性的具体实现交给了Delegate类去完成
    //当调用p属性的时候自动调用Delegate类的getValue

    fun <T> method(param : T) : T{
        return param
    }
}
懒加载

by lazy代码块是Kotlin提供的一种懒加载技术,代码块中的代码一开始并不会执行,只有当首次调用变量的时候才会执行,并将代码块中最后一行代码的返回值赋给变量。

val p by lazy{ ... }

这里的by是关键字,lazy在这里是一个高阶函数。在lazy函数中会创建并返回一个Delegate对象,当我们调用p属性时,其实调用的是Delegate对象的getVaule()方法,然后getVaule方法,然后getVaule方法中又会调用lazy函数传入的lambda表达式,这样表达式中的代码就可以得到执行了,并且调用p属性后得到的值就是lambda表达式中最后一行代码的返回值。

Chapter 9 Kotlin课堂:infix函数

当我们使用A to B的语法构建键值对时,背后调用的就是infix函数。

"hello Kotlin".startsWith("hello")
//可以通过infix函数
infix fun String.beginWith(prefix : String) = startsWith(prefix)
//简化为以下形式
"hello Kotlin" beginWith "hello"

infix函数因其语法糖格式的特殊性,有两个严格的限制:一是不能定义为顶层函数,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类中;二是infix函数接受的参数数量只能为一。

Chapter 10 Kotlin课堂:泛型的高级特性

对泛型进行实化

Java中由于类型擦除机制,无法获得对泛型的具体信息。
在Kotlin中可以借助reified关键字表示该泛型要进行实化,但函数必须是内联函数。

inline fun <reified T> getGenericType(){
}

被实化后,就可以在泛型函数中获得泛型的实际类型,为a is T, T::class.java这样的语法提供了可能。

inline fun <reified T> getGenericType() = T::class.java

fun main() {
    val r1 = getGenericType<String>()
    val r2 = getGenericType<Int>()
    println(r1)
    println(r2)
}

泛型的协变

in out位置:在参数括号内还是外

设有父类A,子类B,子类C。
则接受A类型参数的方法可以接受B,C,但接受B,C类型参数的方法就只能接受B,C
原因在于子类可以转换为父类,而将父类转换为子类的不安全的。

如:

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)
}

这段代码的问题就在于在handleSimpleData方法中向SimpleData< Student>()传入了一个实例。而如果SimpleData< Student>在泛型上是只读的话就没没有这个类型转换隐患了。
即,如果一个泛型类在其泛型类型的数据上是只读的,就能保证不存在类型转换安全隐患。
所以我们只要声明泛型类型只能在out位置,就能保证SimpleData< Student>是SimpleData< Person>的子类了。

而,有List< T>泛型类,当B是A的子类,且List< B>是List< A>的子类时,即接收List< B>类型参数的方法同时可以接收List< A>,我们就说List在T这个泛型上是协变的。
因为只要保证只读,就不会往List< A>里写入A的其他子类C 的对象,就能保证安全。

要求:协变类型只能出现在out位置
如果我们确定写入动作是安全的,可以使用@UnsafeVariance注解写入。

泛型的逆变

有List< T>泛型类,当B是A的子类,且List< A>是List< B>的子类时,即接收List< B>类型参数方法也可以接收List< A>时,我们说List在T这个泛型上是逆变的。
要求:协变类型只能出现在in位置
因为只要保证只写,就不会从接收的List< A>里读出A的其他子类C 的对象,就能保证安全。

Chapter 11 Kotlin课堂:协程

什么是协程

线程是依靠操作系统的调用实现不同线程之间的切换的,而协程是在编程语言的层面实现的。
协程允许我们在单线程模式下模拟多线程编程的效果,代码执行时的挂起和恢复完全是由编程语言来控制的,和操作系统无关。

协程的用法

协程不在Kotlin标准库中,要使用协程要写添加依赖。

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
    testImplementation 'junit:junit:4.13'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

具体用法不再赘述,总结一下:

  • Global.launch函数可以创建一个协程的作用域,这样传递给launch函数的代码块就是在协程中运行的了。该函数创建的是顶层协程,这种协程当应用程序运行结束时也会跟着一起结束。
  • runBlocking函数同样会创建一个协程的作用域,但它可以保证在协程作用域内所有的代码和子协程没有全部执行完之前一直阻塞当前线程。
  • launch函数可以创建多个协程,但它只有在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程都会一起结束。
  • suspend关键字可以将任意函数声明为挂起函数,挂起函数之间是一个互相调用的,但无法该关键字为其提供协程作用域。
  • coroutineScope函数可以在协程作用域或挂起函数中调用,它也是一个挂起函数,它的特点是会继承外部的协程作用域并创建一个子作用域。虽然与runBlocking函数一样,在协程作用域内所有的代码和子协程没有全部执行完之前一直阻塞,但它阻塞的是当前协程,不影响任何线程。
  • Global.launch函数和launch函数会返回一个Job对象,调用该对象的cancel方法就可以取消协程了
  • async函数必须在协程作用域中调用,它会创建一个新的子协程并返回一个Deferred对象,如果我们想要获取async函数代码块的执行结果,只需调用Deferred对象的await方法即可。而且,当调用await方法时,如果代码块中的代码还没执行完,那么该方法会将当前协程阻塞住,直到获得async函数的执行结果。
  • withContext函数是一个挂起函数,调用该函数后会立即执行代码块中的带妹妹,同时将当前协程阻塞住,当代码块的代码全部执行完之后,会将最后一行的执行结果作为withContext函数的返回值返回。它类似async函数,但它强制要求我们指定线程参数(其实除coroutineScope函数外这个参数都是可选的)
  • 线程参数有三种值:
Dispatchers.Default Dispatchers.IO Dispatchers.Main
默认低并发的线程策略 较高并发的线程策略 不开子线程,在主线程中执行代码
适合计算密集型任务 适合线程大多处于阻塞和等待状态 ----------------
suspend fun printDot() = coroutineScope {
    launch {
        println(".")
        delay(1000)
    }
}

fun main() {
    GlobalScope.launch{
        println("codes run in coroutine scopes1")
        delay(1500)
        println("codes run in coroutine scopes finished1")
    }
    Thread.sleep(1000)

    runBlocking {
        println("codes run in coroutine scopes2")
        delay(1500)
        println("codes run in coroutine scopes finished2")
    }

    runBlocking {
        launch {
            println("launch1")
            delay(1000)
            println("launch1 finished")
        }
        launch {
            println("launch2")
            delay(1000)
            println("launch2 finished")
        }
    }

    val start = System.currentTimeMillis()
    var num = 0
    runBlocking {
        repeat(100000){
            launch {
                println(++num)
            }
        }
    }
    val end = System.currentTimeMillis()
    println(end - start)


    runBlocking {
        coroutineScope {
            launch {
                for (i in 1..10){
                    println(i)
                    delay(1000)
                }
            }
        }
        println("runBlocking finished")
    }

    fun offen(){
        val job = Job()
        val scope = CoroutineScope(job)
        scope.launch {
            //
        }
        job.cancel()
    }
fun main() {
//    runBlocking {
//        val result = async {
//            5+5
//        }.await()
//        print(result)
//    }

    runBlocking {
        val start = System.currentTimeMillis()
//        val result1 = async {
//            delay(1000)
//            5+5
//        }.await()
//        val result2 = async {
//            delay(1000)
//            1+1
//        }.await()

        val result1 = async {
            delay(1000)
            5+5
        }
        val result2 = async {
            delay(1000)
            1+1
        }

        println("result = ${result1.await()+result2.await()}")
        val end = System.currentTimeMillis()
        println("cost ${end - start} ms")
    }
}
fun main() {
    runBlocking {
        val result = withContext(Dispatchers.Default){
            5+5
        }
        print(result)
    }
}

猜你喜欢

转载自blog.csdn.net/qq_45254908/article/details/107350207
今日推荐