读书笔记《第一行代码》第三版 kotlin 部分

1 变量和函数

1.1 变量

kotlin中定义变量只允许在变量前声明两种关键字:var val

val是value的缩写,声明一个不可变变量,初始化赋值后不能再重新赋值,相当于java中的final或者js中的const。

var是variable的缩写,声明一个个遍变量。

疑问?

仅仅通过 val var 声明的变量,编译器如何知道变量的类型? kotlin-类型推导机制

fun main() {
    val a = 10
    println("a = " + a)
}

上述代码中,定义了一个不可变变量a,初始化值为10。kotlin会自动推导出变量a的类型为整型。

当我们对一个变量延时赋值的话,kotlin的类型推导就不好使了。这个时候就需要显示的声明变量类型

fun main() {
    var a: Int
    a = 10
    println("a = " + a)
}

基本参数类型表

java基本类型kotlin基本类型解释
intInt整型
longLong长整型
shortShort短整型
floatFloat单精度浮点
doubleDouble双精度浮点
booleanBoolean布尔型
charChar字符
byteByte字节

1.2 函数

main函数是入口函数,与java中的main相似。

函数定义的语法规则

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

func是function的缩写。fun后面紧跟的是函数名。其次是参数定义,参数数量不限,声明方式是参数名:参数类型,定义完参数后需要定义函数返回值的类型,这一步是可选的,如果函数不需要返回值则可以不写。最后是函数体。

变体

import kotlin.math.max
fun largeNumber(a: Int, b: Int): Int {
    return max(a,b)
}

当函数体只有一行的时候可以这样写

fun largeNumber(a:Int, b:Int): Int = max(a,b)
fun largeNumber(a:Int, b:Int)=max(a,b)

2 逻辑控制

2.1 if条件语句

一般用法

fun largeNumber(a: Int, b:Int) :Int {
    if(a>b){
        return a
    }else{
        return b
    }
}

kotlin中的if结构体可以返回值

fun largeNumber(a: Int, b: Int) :Int {
    val value = if(a>b){ a }else { b }
    return value
}

if语句使每个条件的最后一行代码作为返回值。

再次简写

fun largeNumber(a: Int, b: Int) :Int {
    return if(a>b){a}else{b}
}

此时发现,函数体只有一行代码,因此我们可以更进一步的简写。

fun largeNumber(a: Int,b: Int) = if(a>b){a}else{b}

另外,当if结构体中的代码足够简洁,可以进一步压缩。

fun largeNumber(a: Int,b: Int) = if(a>b) a else b

2.2 when条件语句

简单来说when语句类似与switch-case,但是比switch-case更加的强大。

fun getScore(name: String) = when(name){
    "a"->80
    "b"->90
    "c"->100
    else->0
}

结构为

when(匹配变量){
    匹配值->{匹配后的逻辑}
    ...
    else->{都不匹配的逻辑}
}

当逻辑代码只有一行的时候,大括号和if结构一样可以省略,且每个逻辑结构的最后一行就是返回值。

当我们来检测参数类型的时候

fun checkNumber(num: Number){
    when(num){
        is Int->println("整型")
        is Double->println("双精度浮点")
        else -> println("其他类型")
    }
}

when结构还有一种不带参数的用法,但是这种用法不常用。

fun getScore(name: String) = when{
    name == "a" -> 80
    name == "b" -> 70
    name.startSwith("z") -> 50
    else -> 0
}

2.3 循环语句

while循环和java中的while循环用法一致,但是for循环做了一些修改。

java中常用的for-i循环在kotlin中被移除了而for-each循环在kotlin中获得了增强变成了for-in循环。

kotlin中表示一个双闭区间val range = 0..10表示创建了一个[0,10]的闭区间。

kotlin中表示一个左闭右开的区间val range = 0 until 10表示创建了一个[0,10)区间。

kotlin中表示一个双闭降序区间val range = 10 downTo 1表示创建了一个[10,1]的降区间。

遍历一个区间

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

遍历区间还可以使用步长,默认是1

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

3 面向对象

3.1 定义对象

class Person {
    var name = ""
    var age = 0
    fun eat(){
        println(name + " eating He is age = "+age)
    }
}

实例化

fun main () {
    val p = Person()
    p.name = "张三"
    p.age = 19
    p.eat()
}

3.2 继承

想要继承,那么被继承的类需要可以被继承。kotlin中创建的类在默认情况想是不可继承的。想要类可以被继承,必须用open关键字声明这个类。

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

特殊点,被继承的类后面为什么有括号?这里涉及到主构造函数和次构造函数的内容。

kotlin中将构造函数分为两种,主构造函数和次构造函数。

3.2.1 主构造函数

主构造函数是常用的构造函数,定义的类都会默认有一个不带参数的主构造函数,当然也可以显示的指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面。

class Student(val sno: String,val grade: Int) : Person() {
    ...
}

fun main() {
    val s = Student("a123456",2)
}

如果想要在主构造函数中添加一些逻辑,那么可以在init结构中实现。

class Student(val sno: String,val grade: Int) : Person() {
    init {
        println("sno is "+sno)
        println("grade is "+grade)
    }
    ...
}

Kotlin最终是编译成.class文件在jvm虚拟机中执行的。Java中要求子类中的构造函数必须可以调用父类的构造函数,因此Kotlin也需要遵守这个规定。

Student继承Person,Sutdent构造函数中必须能调用Person的构造函数,但是Student主构造函数没有函数体,没地方写调用Person主构造函数的代码,如果这段代码放在Init结构体中,也不是不可以,可是绝大多数情况下是没有必要编写init结构体的。因此Kotlin设计成在声明子类的时候,就直接调用父类构造函数,也就是加了个括号的原因。

class Student(val sno:String,val grade:Int) :Person() /* 父类主构造函数调用 */{
    ...
}

因此,即使父类构造无参,也需要把空括号带上。如果父类是有参构造,那么如下。

open class Person(name:String,age:Int){}
class Student(val sno:String,val grade:Int,name:String,age:Int) :Person(name,age){
    ...
}
/*Student本来没有name,age参数,但是父类需要name,age参数,所以子类必须加上name,age参数来提供给父类使用*/

这里需要注意,name和age参数是没有 使用 var或者val修饰的,仅作为参数传入。因为如果用var或val修饰,那么被修饰的参数将自动成为类的属性。这会导致子类和父类属性冲突,因此不修饰name,age参数,将他两个的作用域只限定到主构造函数内部。

3.2.2 次构造函数

次构造函数是constructor,可以有多个,但是主构造函数只有一个。

class Student(val son:String,val grage:Int,name:String,age:Int) : Person(name,age){
    constructor(name:String,age:Int) : this("",0,name,age){
        /*
        接受name,age参数然后通过this关键字调用主构造函数,将sno默认为空字符串,grage默认为0
        */
    }
    constructor(): this("",0){
        /*
        不接受任何参数,通过this关键字调用了第一个次构造。次构造函数可以相互调用。
        */
    }
}


// 因此有三种实例化方式
val s1 = Student() // 次构造函数2
val s2 = Student("a123",1) // 次构造函数1
val s3 = Student("a123",1,"张三",18) // 主构造函数

特殊情况:主构造函数是可以没有函数体的,但是次构造函数有函数体,所以会出现某个类,没有显示的声明主构造函数,仅声明了次构造函数。

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

当子类没有显示声明主构造函数的时候,也就不需要在继承父类的时候加括号了。只有在这个情况下是可以不加括号的。不加括号的话,需要在次构造函数中调用父类的构造方法,在次构造函数中可以使用super关键字直接调用父类的构造方法。

3.3 接口

kotlin中接口的概念和java中的一样,用于丰富类来实现多态编程的。因为Java是单继承,只能继承一个父类,但是可以实现任意多的接口,kotlin也是这样。

interface Study {
    fun readBooks()
    fun doHomeworks()
    ...
}

实现方式

class Student(name:String,age:Int):Person(name.age),Study{
    override fun readBooks(){
        println(name + "readBooks")
    }
    override fun doHomeworks(){
        println(name + "doHomeworks")
    }
}

例子

/*Person*/
open class Person(val name: String, val age: Int) {
}
/*Study*/
interface Study {
    fun readBooks()
    fun doHomeWorks(){
        println("这里是对doHomeworks方法的默认实现")
    }
}
/*Sutdent*/
class Student(name: String, age: Int) : Person(name, age), Study {
    override fun readBooks() {
        println("name=$name readBooks")
    }
}
/*Start*/
fun doStudy(study: Study) {
    study.readBooks()
}

fun main() {
    val s = Student("张三", 16)
    doStudy(s)
}
/*
结果:name=张三 readBooks
*/

这里有个要点,来体现多态。

s变量可以直接调用readBooks()方法,但是这里将其传入到doStudy函数中,而doStudy函数需要个Study类型的参数。因为Student实现了Study接口,所以Student可以转换为Study类型,来供doStudy函数使用,这就是面向接口编程,称之为多态。

3.4 函数的可见性修饰

修饰符javakotlin
public所有类可见默认 所有类可见
private当前类可见当前类可见
protected当前类,子类,同一包路径下可见当前类,子类可见
default同一包路径下可见(默认不写default)
internal统一模块中可见

3.5 数据类与单例类

数据类是用于数据映射的,在常用的MVC,MVP,MVVM等架构中,M表示的就是数据类。

通常数据类需要重写equals(),hashCode(),toString()方法的。

例如java中

public class CellPhone{
    String brand;
    double price;
    public CellPhone(String brand,double price){
        this.brand = brand;
        this.pricee = price;
    }
    @Override
    public boolean equles(Object o){
        if(o instanceof CellPhone){
            CellPhone other = (CellPone) o;
            return other.brand.equles(brand) && other.price == price;
        }else{
            return false;
        }
    }
    
    @Override
    public int hashCode(){
        retrun brand.hashCode() + (int)price;
    }
    
    @Override
    public String toString(){
        return String.format("CellPhone(brand=%s,price=%s)",brand,price);
    }
}

在Kotlin中实现:

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

一行代码实现data类,当类中没有任何代码时,大括号都可以省略掉。

val phone1 = CellPhone("小米", 2990.0)
val phone2 = CellPhone("小米", 2990.0)
println(phone1==phone2)
// 结果 => true

如果类声明的时候没有用data修饰,那么结果肯定是不相等的。

单例类 既单例模式,java中常见的单例写法为。

public class Singleton {
    private static Singleton instanc;
    public synchronized static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
    private Singleton(){}
    public say(){
        ...
    }
}

//使用
Singleton s = Singleton.getInstance();
s.say();

在kotlin中声明单例模式只需要将class替换成object即可

object Singleton{
    fun say(){
        ...
    }
}

使用起来也很直接

Singleton.say()

用法和java中的静态方法调用,但是内部其实创建了个单例。

4 lambda

lambda编程在kotlin中极其强大,可以说是kotlin的灵魂所在。

4.1 集合的创建和遍历

创建一个含有多种水果的集合

val list = ArrayList<String>()
list.add("苹果")
list.add("橘子")
list.add("梨")

简化创建方法

val list = listOf("苹果","橘子","梨")

使用for-in遍历集合

for(i in list){
    println(i)
}

注意:listOf创建的是只读集合,不可进行增删改。

如果我们需要一个可变的集合,需要用mutableListOf("a","b","c")

上述是List类型集合的用法,Set类型的用法与上述一致

val set = setOf("a","b","c")

set集合不可有重复的元素,重复的元素会被替换掉。

Map集合的创建

val map = HashMap<String,Int>()
map.put("a",1)
map.put("b",2)
map["c"] = 3
val number = map["a"]

简写可以只用mapOf及mutableMapOf

val map = mapOf("a" to 1, "b" to 2, "c" to 3)
for((a,b) in map){
    println("name:$a number: $b")
}

4.2 集合的函数式API

对比不使用函数是API和使用函数式API

val list = listOf("a111","b2222","c33333")
var maxLengthItem = ""
for (item in list){
    if(item.length>maxLengthItem){
        maxLengthItem = item
    }
}
println(maxLengthItem)

val list = listOf("a111","b2222","c33333")
val maxLengthItem = list.maxBy{it.length}
println(maxLengthItem)

lambda函数结构

{param1:param1Type,param2:param2Type->body}
val list = listOf("a111","b2222","c33333")
val lambda = {item:String->item.length}
val maxLengthItem = list.maxBy(lambda)
// 一般可简化为
val maxLengthItem = list.maxBy({item:String->item.length})
// Kotlin 当lambad为函数最后一个参数时候可以将其移到括号后面
val maxLengthItem = list.maxBy(){item:String->item.length}
// 如果移到括号后面后,括号里没有别的参数那么括号也可以省略
val maxLengthItem = list.maxBy{item:String->item.length}
// 由于类型推导机制存在,可以把类型省略掉
val maxLengthItem = list.maxBy{item->item.length}
// lambda函数只有一个参数的时候,可以不显示声明参数,直接用it代替
val maxLengthItem = list.maxBy{it.length}

map函数

可以将集合中的每个元素映射成为另外的元素返回一个新集合。

val list = listOf("a111","b2222","c33333")
val newList = list.map{it.toUpperCase()}

filter函数

可以从集合中过滤

val list = listOf("a111","b2222","c33333")
val newList = list.filter{it.length<=5}
// 可以配合map使用
val newList = list.filter{it.length<=5}
                  .map{it.toUpperCase()}

all和any函数

all函数用于判断集合中是否均满足条件,any判断集合中是否存在元素满足条件,返回的是布尔值。

val list = listOf("a111","b2222","c33333")
val isAll = map.all{it.length>=5}
val isAny = map.any{it.length>=5}

应用

java

new Thread(new Runnable(){
    @Override
    public void run(){
        System.out.println(" thread is running ");
    }
}).start();

kotlin

Thread(object : Runnable {
    override fun run(){
        println("thread is running")
    }
}).start()

kotlin没有new 关键字,这里使用匿名类的使用改用object关键字,相比较java并无简化之处。

Thread(Runnable{
    println("thread is running")
}).start()

因为runnable中只有一个待实现的方法,所以不用显示的声明重写run(),kotlin会明白lambda表达式中的逻辑就是在run中实现的内容。

另外,如果一个java方法的参数列表中只有一个java单抽象接口参数,可以将接口名称省略。

Thread()方法只有一个参数Runnable,而且Runnable又是java接口参数,所以可以继续压缩代码。

Thread({println("Thread is running")}).start()
// lambda作为参数,且只有一个参数可以省略括号
Thread{println("thread is running")}.start()

5 Kotlin处理空指针

编译时控制正检查机制。杜绝空指针异常的出现。

在java中

public void doStudy(Study:s){
    s.readBooks();
}

这个函数虽然很简单,但是s参数如果为空则会导致程序出现空指针错误。

在kotlin中

fun doStydy(s:Study){
    s.readBooks()
}

这里则不会出现空指针异常,这是因为kotlin的函数,默认参数不可为空。也就是说,kotlin将空指针异常提前到编译时期。

如果我们程序需要某些参数可以为空,则需要显示声明参数可为空,只是这种情况下,需要自己在函数体中处理空指针问题。

fun doStudy(s:Study?){
    // 一般用法
    if(s!=null){
        s.readBooks()
    }
    // 简写为
    a?.readBooks()
}

参数类型后面加问号?表示参数可空。当使用参数的时候a?.readBooks()表示当a不为空的时候执行后面的逻辑。

还有一种表达式

val response = if(a!=null){
    a
}else{
    b
}
// 简写为
val response = a?:b

例子:

fun getTextLength(text:String ?) :Int{
    if(text ! =null){
        return text.length
    }else{
        return 0
    }
}
// 简写
fun getTextLength(text:String?)=text?.length?:0
// 解释
/*
1. 首先要使用text.length,就要判断text是否为空
2. text?.lenght 在text不为空的时候 返回text.length的结果
3. text为空的时候,text?.length 然乎null
4. 触发 ?:0 返回0
*/

注意,kotlin的前置空指针判断功能,不是那么智能。有时候在业务内部解决了空指针异常,可是在编译的时候,编译器并不能识别到我们的业务逻辑,会导致编译失败。

var content:String? = "HELLO"
fun main(){
    if(content != null){
        printUpperCase()
    }
}
fun printUpperCase(){
    val upperCase = content.toUpperCase()
    println(upperCase)
}
/**
虽然main中进行了非空判断,但是 printUpperCase函数并不知道外部已经进行过非空检查,所以编译无法通过,
如果想要编译通过,就需要在printUpperCase函数中进行非空断言
*/
fun printUpperCase(){
    val upperCase = ontent!!.toUpperCase()
    printlen(upperCase)
}

params!!表示非常确信params不为空,如果运行时候params出现了空,则抛出空指针异常。

kotlin中的一个特殊函数

let不是关键字也不是操作符,却是一个函数。

obj.let{
    obj2->业务
}

obj.let会将obj作为参数传递到后方的{lambda}中,obj2就是obj,他俩是同一个对象。

将doStudy函数改成let方式调用。

fun doStudy(s:Study?){
    study?.let{ 
        stu->
        stu.readBooks()
    }
}
// 由于lambda函数仅有一个参数可以省略声明参数,并用it代替
fun doStudy(s:Study?){
    study?.let{
        it.readBook()
        it.doHomeworks()
    }
}

注意:let函数是可以处理全局变量的判空问题的,if却做不到

var s:Study? = null
fun doStudy(){
    if(s!=null){
        s.readBooks() // 错误
    }
    // let写法
    s?.let{
        it.readBooks() // 真确
    }
}

7 字符串内嵌

字符串内嵌 : ${obj.name}

字符串内嵌 : $name

val brand = "Samsung"
val price = 1299.99
println("Cellphone(brand=$brand, price=$price)")

8 函数默认值

fun printParams(num: Int, str: String = "hello") {
println("num is $num , str is $str")
}
fun printParams(num: Int = 100, str: String) {
println("num is $num , str is $str")
}

fun main(){
    printParams("hello") // 错误,需要用键值传参
    parintParams(str="hello")
}

9 标准函数和静态方法

9.1 标准函数 with、run、apply

便准函数既kotlin的Standard.kt中定义的函数,在kotlin代码的任意文职都可自由的调用标准函数。

9.1.1 with函数

with有两个参数,参数一可以是任意类型的对象,参数二是个lambda表达式,with函数会在lambda表达式中提供第一个参数对象的上下文,并使用lambda表达式中的最后一行代码作为返回值返回。

val result = with(obj){
    // 上下文
    "value" //返回值
}

作用是,可以在连续调用同一个对象的多个方法时,简写代码。

val list = listOf("a","b","c","d")
val bulider = StringBuilder()
bulider.append("start say .\n")
for(item in list){
    builder.append(item).append("\n")
}
builder.append("say over")
val result = builder.toString()
println(result)

这里我们调用了很多builder.append,使用with来简写代码。

val list = listOf("a","b","c","d")
val result = with(StringBuilder()){
    append("start say . \n")
    for(item in list){
        append(item).append("\n")
    }
    append("say over")
    toString()//最后一行作为返回值,将StringBuilder转为字符串
}
println(result)

9.1.2 run函数

用法与上述with函数基本一致,区别在于,run函数不会直接调用,直接收一个lambda函数作为参数,并在lambda表达式中提供上下文。

val result = obj.run{
    // obj上下文
    "value" // 最后一行代码返回值
}

使用run来简写say代码

val list = listOf("a","b","c","d")
val result = StringBuilder().run{
    append("start say . \n")
    for(item in list){
        append(item).append("\n")
    }
    append("say over")
    toString()//最后一行作为返回值,将StringBuilder转为字符串
}
println(result)

9.1.3 apply函数

apply函数调用方式与run函数一致,但是apply函数无法指定返回值,因为操作的是对象本身,with和run返回的是新对象。

obj.apply{
    // 上下文
}
println(obj)
val list = listOf("a","b","c","d")
val result = StringBuilder().allpy{
    append("start say . \n")
    for(item in list){
        append(item).append("\n")
    }
    append("say over")
}
println(result.toString())

9.2 定义静态方法

静态方法在java中也叫做类方法,不需要实例化类对象,直接通过类名就可以调用。

java中定义静态方法

public class Util {
    public static String simple(){
        retrun "simple"
    }
}
// 调用
Util.simple()

在kotlin中,设计上弱化了静态方法,而是使用单例来实现相应功能。

object Util {
    fun simple() :String {
        return "simple"
    }
}
//
Util.simple()

这里实现了一个类似java的调用,但是用的是单例,虽然 fun simple并不是静态方法,但是单例可以让类中的方法都具备静态方法的调用方式。

如果我们想在一个普通类中实现某个或某些静态方法,需要用到companion object

class Util {
    fun doSomething(){
        println("doSomething")
    }
    // 创建内部伴生类,相当于将单例嵌入到Util类内,调用Util.simple(),调用半生对象的方法
    companion object {
        fun simple():String{
            return "simple"
        }
    }
}

虽然没有真正意义上的静态方法,但是这种实现方式已经足够在形式上模仿静态方法使用了,如果非要实现一个正真意义上的静态方法,需要注解和顶层方法。

class Util {
    fun doSomething(){
        println("doSomething")
    }
    companion object {
        @JvmStatic 
        fun simple():String{
            return "simple"
        }
    }
}

被注解的方法将是正真意义上的静态方法,可以在java中以静态方法的方式调用。此注释只可以添加在单例类和伴生类的方法上。

顶层方法,就是不在类内的方法,例如man方法。只需要创建kotlin文件,不需要创建kotlin类。然后在其中直接定义方法。

顶层方法被创建后,可以在任意文件,任意位置,不用在乎包名,之际调用。

如果在java中想调用kotlin中创建的顶层方法,那么就需要使用FileName.functionName()方式来调用了,java会将存放顶层方法的文件直接识别成一个类。

10 延时初始化

kotlin的空指针检查机制虽然可以保证程序运行中,杜绝出现控制正错误。可以当么类中存在很多全局变量时候,我们为了保证kotlin判空语法检查,需要进行很多判空操作。

class MainActivity : AppCompatActivity(),View.OnClickListener {
    private var adapter:MsgAdapter? = null
    override fun onCreate(savedInstanceStat:Bundle?){
        ...
        adapter = MsgAdapter(msgList)
        ...
    }
    override fun onClick(v:View?){
        ...
        adapter?.notifyItemInserted(msgList.size-1)
        ...
    }
}

首先定义了一个全局便令adapter,因为我们需要在onCreate中初始化此变量,所以需要将其定义为可为空。

在onClick中使用adapter变量时,我们可以确定此变量肯定不为空。但是还需尽心adapter?.否则编译不通过。

如果这种变量多了的时候,就需要对每个变量进行判空处理。

laterinit可以延迟变量初始化。

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

延迟初始化后,就不需要对全局变量设置null了,这样adapter就是不可空全局变量了,下面的函数在使用的时候便不用在进行手动判空。

这样也留出一处风险,如果在adapter变量还没有初始化的时候就使用了adapter变量,就会触发ninitializedPropertyAccessException 变量未初始化的异常。

当然,我们也可以手动判断延迟初始化的变量是否已经初始化。

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

有时候我们为了避免某个变量重复初始化,也可以使用延迟初始化功能。

11 密封类

密封类特点:可继承,当在when语句中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应
的条件全部处理 ,否则编译不通过。

声明关键字sealed class ClassName

注意:密封类及其所有子类必须写在一个顶层文件中。

sealed class Result
class Success(val msg:String):Result()
class Failure(val error:Exception):Result()
fun getResultMsg(result:Result)=when(result){
    is Success -> result.msg
    is Failure -> "error message ${result.error.message}"
    // 不需要else
}

如果在顶层文件中添加了一个UnKnown类,那么就需要在getResult函数中处理UnKnown类,否则编译不通过。

在android的Adapter中使用

顶层定义

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

adapter中使用

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 = msg.content
            is RightViewHolder -> holder.rightMsg = msg.content
        }
    }
}

12 扩展函数

在不修改一个类源码的情况下,任然可以为某个类添加自定义方法。

如果我们想将统计字符串中所有字母的数量。

我们会定义个Util

object StringUtil {
    fun lettersCount(str: String): Int {
        var count = 0
            for (char in str) {
                if (char.isLetter()) {
                    count++
                    }
                }
                return count
        }
}
//
val str = "ABC123xyz!@#"
val count = StringUtil.lettersCount(str)

如果我们扩展String类,使其具备lattersCount方法

fun String.lettersCount():Int{
    var count = 0
    for (char in this) {
        if (char.isLetter()) {
            count++
            }
        }
    return count
}
// 
val count = "ABC123xyz!@#".lettersCount()

这样就很方便,如果这个方法定义成为顶层方法,那么我们在项目的任意一个位置都可以调用。

13 运算符重载

在kotlin中通过重载运算符或者关键字,可以拓展用法。

在kotlin中,重载运算符可以使任意两个对象进行运算。不如两个Money对象相加。

class ObjName {
    operator fun plus(obj: ObjName):ObjName{
        //处理逻辑
    }
}
// 使用
val o1 = ObjName()
val o2 = ObjName()
var o3 = o1 + o2

Money例子

class Money(val value:Int){
    operator plus(obj:Money):Money{
        val sum = value + obj.value
        return Money(sum)
    }
    // 继续重载
    operator plus(newValue:Int):Money{
        val sum = value + newValue
        return Money(sum)
    }
}
val m1 = Money(1)
val m2 = Money(2)
var m3:Money = m1+m2
println(m3.value)
// 结果为3
m3 = m2+1
println(m3.value)
// 结果为3

生成随机长度的字符出

// 一般方法
fun getRandomLengthString(str:String):String{
    val n = (1..20).random()
    val builder = StringBuilder()
    repeat(n){
        builder.append(str)
    }
    return builder.toString()
}

结合扩展函数和运算符重载实现

// 扩展
operator fun String.times(n:Int):String{
    val builder = StringBuilder()
    repeat(n){
        builder.append(this)
    }
    return builder.toString()
}
// 精简代码
operator fun Stirng.times(n:Int)=repeat(n)
val value = "abc"
println(value * 3)
// 结果=》 abcabcabc
// 随机
fun getRandomLengthString(str:String) = str*(1..20).random()

14 高阶函数详解

14.1 定义高阶函数

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

要点:

  1. 以函数作为参数
  2. 返回值是一个函数

参数为函数?

参数类型有整型、布尔型等字段类型,在Kotlin中增加了个类型:函数类型。我们在定义函数的时候,将参数定义为函数类型或者返回值定义为函数类型,那么定义的这个函数就是高阶函数。

规则例如:

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

例如我们定义一个根据两个数计算另一个数的函数,计算方法不确定

// 高阶函数
fun num1AndNum2(num1:Int,num2:Int,operation:(Int,Int)->Int):Int{
    val response = operation(num1,num2)
    return response
}
// 匹配函数
fun plus(num1:Int,num2:Int)=num1+num2
fun minus(num1:Int,num2:Int)=num1-num2
//调用
fun main(){
    val num1 = 100
    val num2 = 10
    val result1 = num1AndNum2(num1,num2,::plus)
    val result2 = num2AndNum2(num1,num2,::miuns)
}

当函数作为参数传入的时候要使用::name的方式。

还是上面的函数,我们将匹配函数使用lambda定义

fun main(){
    val num1 = 100
    val num2 = 10
    val result1 = num1AndNum2(num1,num2,{a,b->a+b})
    val result2 = num1AndNum2(num1,num2,{a,b->a-b})
}

回顾上述的扩展函数我们给StringBuilder扩展一个build方法,这个方法为高阶函数,参数是函数,返回值是StringBuilder对象。

fun StringBuilder.build(block:StringBuilder.()->Unit):StringBuilder{
    block()
    retrun this
}

这里在定义函数参数的时候用到了类名.()-Unit的方式。这种方式是完整的方式。表示这个函数类型的参数是定义在StringBuilder这个类中的,这样做的好处是当我们调用build函数传入lambda的时候,会自动拥有StringBuilder的上下文,集合式API-apply就是这样实现的。

// 使用
fun main(){
    val list = listOf("a","b","c")
    // 只有一个参数,且参数是个lambda可以省略函数调用的括号
    val result = StringBuilder().build {
        // build函数的参数是个函数,且函数使用 className. 声明,那么此函数可以直接拿到StringBuilder上下文,直接使用append
        append("start say: \n")
        for(item in list){
            append(item).append("\n")
        }
        append("over")
    }
    println(result)
}

14.2 内联函数

分析高阶函数实现的原理

在Java中并没有高阶函数这个概念。将上述num1AndNum2编译为Java:

public static int num1AndNum2(int num1,int num2,Function operation) {
    int result = (int) operation.invoke(num1,num2);
    return result;
}
public static void main(){
    int a = 100;
    int b = 10;
    int result = num1AndNum2(a,b,new Function(){
        @override
        public Integer invoke(Integer a,Integer b){
            return a+b;
        }
    })
}

这种转换每次传入lambda都会创建个匿名内部类,会造成额外开销,所以kotlin提供了内联函数功能,可以将创建匿名类的开销省去。

内联函数用法非常简单,只需要在定义高阶函数的时候加上inline关键字声明即可

inline fun num1AndNum2(num1:Int,num2:Int,operation:(Int,Int)->Unit)=operation(num1,num2)

这种inline优化了什么?

kotlin会将inline中的代码在编译的时候自动替换到调用的地方:

// 原始kotlin
fun main(){
    val num1 = 100
    val num2 = 10
    val result1 = num1AndNum2(num1,num2,{a,b->a+b})
}
// 替换为新kotlin
fun main(){
    val num1 = 100
    val num2 = 10
    val result1 = num1+num2
}
//再进行编译.class文件

高阶函数定义中说了,参数中有函数类型的参数就是高阶函数,所以可能存在多个函数类型参数的情况。在这种使用如果使用inline声明高阶函数,将会把所有lambda表达式内联。

如果我们仅想部分内联:

inline fun funcName(block1:()->Unit,noinline block2:()->Unit){}

这样block2匿名函数就不会被内联了。

内联函数可以减少消耗,但是也有限制:内联函数在编译的时候会被替换,所以内联函数就不属于真正的函数了,没有真正函数类型的参数属性,而非内联函数类型参数可以自由的传递给其他函数,因此它是真正的参数,内联函数类型的参数只允许传递给另外一个内联函数。

另外还有个重要区别:内联函数引用的lambda表达式中可以使用return关键字来进行函数返回,而非内联函数只能进行局部返回。

fun printString(str:String,block:(String)->Unit){
    println("11111")
    block(str)
    println("2222")
}
fun main(){
    println("0000")
    val str = ""
    printString(str){
        s->
        println("aaaaaa")
        if(s.isEmpty())retrun@printString
        println(s)
        println("bbbbbb")
    }
    println("33333")
}
# 结果
0000
11111
aaaaa
2222
33333
# bbbbbb没输出因为内部return@pringString

这里lambda函数不允许世界使用return关键字,所以使用return@函数名的方式来停止执行lambda函数体剩余的部分。这就是局部返回,仅仅停止了block内部,而block外部还可以继续执行。

如果内联

fun printString(str:String,block:(String)->Unit){
    println("11111")
    block(str)
    println("2222")
}
fun main(){
    println("0000")
    val str = ""
    printString(str){
        s->
        println("aaaaaa")
        if(s.isEmpty())retrun
        println(s)
        println("bbbbbb")
    }
    println("33333")
}
# 结果
0000
11111
aaaaa
# 往后都没了

直接打断整个高阶函数。并且main也被打断。

理论上大多数函数都是可以内联的,但是在有些情况下,内联会产生错误。

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

发现内联后提示错误block()

解释:

内联之后,lambda表达式将不再具备参数属性,Runnable实例化过程中需要一个参数类型的函数,所以block不符合要求,因此错误。

冲突主要在于内联函数可以使用return直接返回,而Runnable的lambda允许使用return返回

也就是说,如果我们在高阶函数的函数体中,创建了另一个lambda或者匿名类的实现(Runnable就是匿名类的实现),此时我们把高阶函数内联就必定会出错。

这种情况下使用 crossinline可解决

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

crossinline表示约定 block 一定不会使用retrun关键字进行返回,这样冲突就不存在了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值