kotlin值得系列2,关于类你所应该知道的一切

本文是kotlin系列第二篇

  • 类的成员和可见性修饰符
  • 构造函数
  • 继承,抽象和接口
  • 拓展方法
  • 空类型安全
  • 智能类型转换
  • 类属性的延迟初始化
  • 代理Delegate
  • 单例object
  • 内部类
  • 数据类data class
  • 枚举类enum class
  • 密封类sealed class
  • 内联类inline class

一、类

在Java中,类基于Object,而在Kotlin中类基于 Any,所有类都默认继承Any

Any类中,只有3个方法,分别是equals,hashCode和toString。

package kotlin
 
public open class Any {
   
    public open operator fun equals(other: Any?): Boolean
 
    public open fun hashCode(): Int
 
    public open fun toString(): String
复制代码
  • 声明类的关键字是 class

  • 声明类的格式

class Test{
    // 属性...
    ...
    // 构造函数
    ...
    // 函数
    ...
    // 内部类
   ...
   ...
}
复制代码
  • 需要注意的是,如果类没有结构体,那么大括号是可以被忽略的
class Test
复制代码

说一千道一万,不如来个小例子

class Animal {
    fun eat() {
        println("每天都是吃吃吃")
    }
    fun move() {
        println("每天都是浪浪浪")
    }
}
fun main() {
    var ani = Animal()
    ani.eat()
    ani.move()
}
复制代码

输出

每天都是吃吃吃
每天都是浪浪浪
复制代码

.
.

关于kt类需要注意的几个点

Kotlin的接口是可以包含属性声明,Kotlin默认的声明是fianl 和public的,意味默认不允许被继承

Kotlin的类和方法默认都是final的(准确来说是 public final),这意味着,默认是不允许被继承的,一个类想要允许被继承,那么需要显示地声明为 open 。(Java的类默认是不会加上的final的,也就是随意继承))
一个方法如果如果想要被重写,也需要声明为open。如果类不是open的,类的成员也不允许open。

open class Animal {
    fun eat() {
        println("每天都是吃吃吃")
    }
    open fun move() {
        println("每天都是浪浪浪")
    }
}
// kt的继承用的是 : ,不是extends
class Bird : Animal() {
    // eat 方法不是open,不允许被复写

    // move方法就允许被复写
    override fun move() {
        super.move()
        println("一直都在天上,顽皮小鸟喝不醉")
    }
}
fun main() {
    var b = Bird()
    b.eat()
    b.move()
}
复制代码

输出

每天都是吃吃吃
每天都是浪浪浪
一直都在天上,顽皮小鸟喝不醉
复制代码

当然了,除了open,我们也能使用 abstract 让类变成抽象类,这样也能被继承。抽象的我们另外再说。

二、类的成员和访问修饰符

类的成员

  • 构造函数和初始化块(Constructors and initializer blocks)
  • 函数(Functions)
  • 属性(Properties)
  • 嵌套类和内部类(Nested and Inner Classes)
  • 数据对象(Object Declarations)

访问修饰符/可见性修饰符

类的修饰符包括 classModifier 和_accessModifier_:

classModifier: 类属性修饰符,标示类本身特性。

  • abstract // 抽象类
  • final // 类不可继承,默认属性
  • enum // 枚举类
  • open // 类可继承,类默认是final的
  • annotation // 注解类

accessModifier: 访问权限修饰符

  • public kt中的默认修饰符,全局可见
  • protected 受保护修饰符,类及子类可见
  • private 私有修饰符,类内修饰只有本类可见,类外修饰文件内可见
  • internal 模块内可见 (这个是kt独有的,java中没有)

1、与java不同,kotlin中啥也不写,默认就是public的,而java中不写默认是default包内可见
2、kotlin中多一个限制可见性的internal关键字,去掉了default关键字
3、protected只能用来修饰成员。

对于protectedjava是包内可见,而kotlin是类内可见,这点不同,当然子类肯定都是可见的,kotlin中protected不能用来修饰类

// 文件名:example.kt
package foo

private fun foo() {} // 在 example.kt 内可见

public var bar: Int = 5 // 该属性随处可见

internal val baz = 6    // 相同模块内可见
复制代码

三、kotlin类的构造函数

kotlin类构造函数的最大特色,就是,就是分为 主构造函数次构造函数

  • 主构造函数 – 初始化类的简洁方法(仅有一个)
  • 次构造函数 – 放置其他初始化逻辑(允许多个)

在kotlin中,constructor关键用户声明构造方法,在一定条件下,这个constructor允许被省略。

kotlin中,构造函数是通过 constructor 关键字来标明的,对于主构造函数来说,它的位置在类的标题中声明,而对于次级构造函数来说它的位置在类中。

主构造函数

  • 主构造函数类标头的一部分
  • 括号括起来的代码块主构造函数
  • 主构造函数允许含有默认值,其实这也想当初初始化
  • 主构造函数的语法受约束,不能包含任何代码。如果有初始化操作,应该放在 init 这个特有的代码块里面

也可以认为,主构造函数的初始化,要么是设置默认值,要么就就必须在init代码块里面实现

  • 声明一个构造函数一般需要用到 constructor 关键字,但是当constructor关键字没有注解可见性修饰符作用于它时,constructor关键字可以省略。
  • 当我们定义一个类并没有声明一个主构造函数的时候,kotlin会默认为我们生成一个无参的主构造函数,这一点和java一样。

说完了,来代码吧。

// constructor(name:String,age:Int) 就是主构造函数
// 主构造函数是类标头的一部分
class Person constructor(name:String,age:Int,hobby:String = "睡觉觉"){
    val pName:String
    val pAge:Int
    val pHobby:String
    // this.name = name 不能这么写,初始化的操作必须放在 init 里面

    // init 代码块用户初始化构造函数
    init {
        this.pName = name
        this.pAge = age
        this.pHobby = hobby
        println("名字: $pName")
        println("年龄: $pAge")
        println("爱好: $pHobby")
    }
}

fun main() {
    var tony = Person("王哈哈",18)
}

输出:
名字: 王哈哈
年龄: 18
爱好: 睡觉觉
复制代码

.
.
其实就上面这个 Person 类而言,constructor 是可以被省略的。

// 比如这个类就可以省略 constructor
// 因为这个 constructor 没有注解 和 可见性修饰符
//class Animal constructor (move:String){
class Animal(move:String){
    val aMove:String;
    init {
        this.aMove = move;
    }
}
// 这个 constructor 就不能省略,有可见性修饰符
class Animal2 private  constructor(move:String){
    val aMove:String;
    init {
        this.aMove = move;
    }
}
复制代码

次构造函数

在Kotlin中,一个类还可以包含一个或多个次构造函数。它们是使用 constructor 关键字创建的。

次构造函数在Kotlin中并不常见。 当您需要扩展提供以不同方式初始化类的多个构造函数的类时,次要构造函数的最常见用法就出现了。

主要看以下几点:

  • 次构造函数一定需要使用 constructor 定义
  • 在Kotlin中,您还可以使用 this() 来从同一类另一个构造函数(如Java中)调用构造函数。
  • 通过在一个类中使用主和次级构造函数,次级构造函数需要授权给(委托给)``主构造函数,也就是次级构造函数会直接或者间接调用主构造函数。使用this()关键字对同一个类中的另一个构造函数进行授权(委托)。
  • 子类构造函数可以通过super关键字调用父类的构造方法

次构造函数的大概有两种使用方式

  • 直接使用次构造函数
  • 主 次 构造函数一起使用

直接使用次构造函数

class Car{
    constructor(name: String) {
        println("车的名字是 $name")
    }
}

fun main() {
    var car = Car("大黄蜂")
}

输出
车的名字是 大黄蜂
复制代码

如上,只有次构造函数。

主 次 构造函数一起使用

主要看看代码里面的 this

  • 在Kotlin中,您还可以使用 this() 来从同一类的另一个构造函数(如Java中)调用构造函数。

  • 通过在一个类中使用主和次级构造函数,次级构造函数需要授权给(委托给)主构造函数,也就是次级构造函数会直接或者间接调用主构造函数。使用this()关键字对同一个类中的另一个构造函数进行授权(委托)。

class Animal(val name: String) {
    init {
        val outPutName = "init 动物叫做: $name"
        println(outPutName)
    }
    // 使用 this() 来从同一类的另一个构造函数(如Java中)调用构造函数。

    // 直接 委托给主构造方法
    constructor(name: String, age: Int): this(name) {
        println("直接委托: 名字:$name  年龄:$age")
    }

    // 通过上面的构造方法 间接 委托给主构造方法
    constructor(name: String, age: Int, move: String): this(name, age) {
        println("简介委托: 名字:$name  年龄:$age 动起来:$move" )
    }
}

fun main() {
    var bird1 = Animal("主黄蜂")
    println("==========")
    var bird2 = Animal("次黄蜂1号",18)
    println("==========")
    var bird3 = Animal("次黄蜂2号",18,"一飞冲天")
}
复制代码

.
.

  • 子类构造函数可以通过super关键字调用父类的构造方法。(不这么干还会报错)
open class Car{
    constructor(name: String) {
        println("Car类 车的名字是 $name")
    }
}
// 子类利用super调用父类的构造方法
class NiceCar : Car{
    constructor(name: String,spec: String):super (name) {
        println("NiceCar 车的名字是 $name 。 技能是:$spec")
    }
}

fun main() {
    var niceCar = NiceCar("大黄蜂","每天都在天上飞"
}
复制代码

构造函数和继承

继承这一块,本来是后面讲,但是这里讲到构造函数,就干脆放一起了。

  • 当父类存在主构造函数,子类也必须有一个构造函数(可以是 主构造函数 或者 次构造函数),不然报错

image.png

open class Animal(name: String){

}
// 主构造
class Bird(name: String) : Animal(name) {

}
// 次构造
class Bird : Animal {
    constructor(name: String) : super(name)
}
复制代码

.
.

  • 当父类存在多个构造函数,子类的主构造函数一般实现参数最多父类中参数最多的构造函数。子类中参数少的可以用this关键字引用。
open class Animal{
    constructor(name:String)
    constructor(name:String,gender:String)
    constructor(name:String,gender:String,age: Int)
}
class Bird(name: String, gender: String, age: Int) : Animal(name, gender, age) {
    constructor(name: String) : this(name,"",0)
}
复制代码

四、继承,抽象和接口

继承

继承需要用到 open:,一个类如果想被继承,就需要声明为open。继承需要用到 :(java中是extends),类的方法属性,如果想要被复写,也需要声明为open

来一份小代码,演示下 open,: 注意看方法和属性是否为open

open class Animal{
    var hobby:String = "爱好"
    open var name:String ="动物";

    fun move(){
        println("运动")
    }
    open fun eat(){
        println("吃吃吃")
    }
}
// 继承一下
class Bird1 : Animal() {
}
// 继承一下
class Bird2 : Animal() {
    // 这是能复写是因为open了
    // hobby没有open无法override
    override var name: String
        get() = super.name
        set(value) {"鸟呀"}

    // 这是能复写是因为open了
    // move无法复写,无法override
    override fun eat() {
        super.eat()
        println("吃点虫子吧")
    }
}
fun main() {
    var b1 = Bird1();
    var b2 = Bird2();

    println(b1.name)
    println(b1.hobby)
    b1.eat()
    b1.move()

    println("===============")
    println(b2.name)
    println(b2.hobby)
    b2.eat()
    b2.move()
}

输出:

动物
爱好
吃吃吃
运动
===============
动物
爱好
吃吃吃
吃点虫子吧
运动

复制代码

.
.

  • 父类的open函数子类可以重写这点我知道了,但是父类中没有用open修饰的函数,子类也不能起相同名字的函数名。

image.png

抽象类

  • Kotlin中抽象可分为抽象类、抽象函数、抽象属性,抽象类的成员包括抽象函数和抽象属性。抽象类的成员只有定义而没有实现,也不能有实现,否则就是非抽象类了

  • 声明抽象使用abstract关键字进行修饰,抽象类的子类必须全部重写带有abstract修饰的抽象属性函数。抽象类中允许存在非抽象的函数和属性。

  • 抽象类默认就是open修饰的,可以直接继承。不像普通类要继承需要声明为open

  • 抽象类的属性可以的保存状态的。(接口的属性不能保存状态,必须抽象)

abstract class Car{
    // 非抽象的 属相和函数,可以有初始化值和实现
    // whoMake有值,说明抽象属性是可以保存状态的
    var whoMake = "人类制造"
    // 抽象类中的普通函数,默认final,没有指定为open,子类不能复写
    fun oKCar(){
        println("检验合格")
    }

    // 抽象成员不能有初始化值
    abstract var name:String
    // 抽象方法不能有实现(不能有{})
    abstract fun spec(spe:String)
}

class NiceCar : Car() {
    // 必须复写的,不然报错
    override var name:String =  "上天汽车"

    // 必须复写的,不然报错
    override fun spec(spe: String) {
        println(spe)
    }
}

fun main() {
    var niceCar = NiceCar()
    println(niceCar.whoMake)
    niceCar.oKCar()

    println(niceCar.name)
    niceCar.spec("每天都在天上飞")
}

输出:

人类制造
检验合格
上天汽车
每天都在天上飞
复制代码

接口类

kt使用关键字interface定义接口,一个类可实现一个或多个接口

  • 接口的实现使用的关键字是:冒号(:)。(这一点是和Java不同的。Java中使用接口使用的是implements关键字)

  • 接口只能继承接口,不能继承类

  • kt接口的成员支持private和public两种访问修饰符,默认public(java的接口的成员的修饰符只能是public)

  • Kotlin 的接口可以有抽象方法也可以有非抽象方法。接口中的函数,如果带了结构体/方法体(带了{}),那么就不再是抽象方法。

  • 接口中的函数,一般是没有方法体的(默认是public abstract),那么必须复写;如果函数有方法体(public open),可以不复写。

  • 接口无法保存状态。

  • 接口可以有属性但必须声明为抽象,或着提供访问器实现(提供一个get方法,但提供了就不再是抽象函数)。

  • 接口的属性不能幕后字段(backing field),其实就是不能直接赋值。

接口中的函数

interface SimpleInterface{
    // 定义一个无参数无返回值的方法
    fun fun1()
    //定义一个有参数的方法
    fun fun2(num: Int)
    // 定义一个有参数有返回值的方法
    fun fun3(num: Int) : Int

    // 下面的两个方法是有结构体, 故可以不重写
    //定义一个无参数有返回值的方法
    fun fun4() : String{
        return "fun4"
    }
    //定义一个无结构体函数,大括号是可以省略的
    fun fun5(){
        // 如果函数中不存在表达式,大括号可以省略。
        // 如fun1一样
    }
}

class Demo2 : SimpleInterface{
    // fun1,fun2,fun3必须复写(因为接口中他们没有返回值),不然报错
    override fun fun1() {
        println("我是fun1()方法")
    }

    override fun fun2(num: Int) {
        println("我是fun2()方法,我的参数是$num")
    }

    override fun fun3(num: Int): Int {
        println("我是fun3()方法,我的参数是$num,并且返回一个Int类型的值")
        return num + 100
    }

    override fun fun4(): String {
        println("我是fun4()方法,并且返回一个String类型的值")

        /*
            接口中的fun4()方法默认返回”fun4“字符串.
            可以用super.fun4()返回默认值
            也可以不用super关键字,自己返回一个字符串
        */
        return super.fun4()
    }

    /*
         接口中的fun5()带有结构体,故而可以不用重写,
         fun4()同样
    */
    //    override fun fun5() {
    //        super.fun5()
    //    }
}

fun main() {
    var demo = Demo2()

    demo.fun1()
    demo.fun2(5)
    println(demo.fun3(3))
    println(demo.fun4())
    //可以不重写该方法直接调用
    demo.fun5()
}

输出:
我是fun1()方法
我是fun2()方法,我的参数是5
我是fun3()方法,我的参数是3,并且返回一个Int类型的值
103
我是fun4()方法,并且返回一个String类型的值
fun4
复制代码

接口中的属性

接口可以有属性但必须声明为抽象,或着提供访问器实现(提供一个get方法,但提供了就不再是抽象函数)

interface SimpleInterface{
    // 抽象属性 接口中的属性默认就是抽象的
    val num: Int //等价于 public abstract val num: Int

    // 非抽象属性 提供了访问器
    // 等价于 public open val hobby: String
    val hobby: String
        get() = "一些爱好"

    fun fooNum() {
        print(num)
    }
}

class Demo2 : SimpleInterface{
    // 因为num是抽象的,所以这里我们必须复写
    override val num: Int
        get() = 100
}

fun main() {
    var demo2 = Demo2()
    println(demo2.num)
    // 因为hobby提供了访问器get,所以我们能直接访问到
    println(demo2.hobby)
    // 调用接口中的方法
    demo2.fooNum()
}
复制代码

.
.

接口的属性不能幕后字段(backing field),其实就是不能直接赋值。

image.png

接口继承

一个接口可以从其他接口派生,从而既提供基类型成员的实现,也声明新的函数与属性。很自然地,实现这样接口的类只需定义所缺少的实现

interface Person : Named {
    var firstName: String
    var lastName: String

    override val name: String get() = "$firstName $lastName"
}
// Employee实现自 Person,而Person实现自Named
class Employee : Person {
    // Employee不需要复写name属性了

    // 按道理 Employee 应该复写爷爷类的name属性
    // 但是因为他爹Person已经复写过了,所以Employee不需要复写name属性了
    // 当然了,只要Employee愿意,也可以自己复写name
    override var firstName: String = ""
    override var lastName: String = ""
}

fun main() {
    var emp = Employee()
    emp.firstName = "大海"
    emp.lastName = "李"
    println(emp.name)
}


输出:
大海 李

复制代码

解决覆盖冲突

如果甲接口和乙接口都有相同的方法,那么丙派生自甲乙,这个时候,应该怎么办呢?

interface A {
    fun foo() { println("A-foo") }
    fun goo()
    fun move()
}

interface B {
    fun foo() { println("B-foo") }
    fun goo() { println("B-goo") }
    fun move()
}
// 我们注意到,A和B都实现foo方法,两者的foo都不是抽象方法了
// A的goo方法是抽象方法,A的goo是 非抽象方法
// A和B的move方法都是抽象方法

class C : A {
    // 因为A中goo和move都是抽象的
    override fun goo() { println("C-goo") }
    override fun move() {println("C-move") }
}

class D : A, B {
    // 多继承,A和B都实现了foo,所以这里需要复写
    override fun foo() {
        super<A>.foo()
        super<B>.foo()
        println("D-foo")
    }

    //多继承,因为B的goo方法自己有实现,所以我们这里只需要复写superB的
    override fun goo() {
        super<B>.goo()
        println("D-goo")
    }

    //多继承,因为A和B的move都是抽象的,所以这里我们不需要superA或者B
    override fun move() {
        println("D-move")
    }
}

fun main() {
    var c = C();
    c.foo()
    c.goo()
    c.move()
    println("=========")
    var d = D();
    d.foo()
    d.goo()
    d.move()
}

输出:

A-foo
C-goo
C-move
=========
A-foo
B-foo
D-foo
B-goo
D-goo
D-move

复制代码

五、拓展方法

拓展,这个可是个好东西。

kotlin支持在不修改类代码的情况下,动态为类添加属性(扩展属性)和方法(扩展方法)。

举个例子,一个三方库你觉得很好用,但是用着用着拍大腿说他为什么没有xxx方法,但是你有修改不了它的代码,这个时候,就该 拓展 登场了。

扩展方法执行静态解析(编译时),成员方法执行动态解析(运行时)。

原理

kotlin扩展属性、方法时看起来是为该类动态添加了成员,实际上并没有真正修改这个被扩展的类,kotlin实质是定义了一个函数,当被扩展的类的对象调用扩展方法时,kotlin会执行静态解析,将调用扩展函数静态解析为函数调用。

格式

其实很简单,就是放方法名前面加上 类型.

来一个感受感受

class Person{
    fun eat(){
        println("吃吃吃")
    }
}
fun Person.playMan(){
    println("玩世不恭")
}

fun main() {
    var per = Person()
    per.eat()
    per.playMan()
}

输出:
吃吃吃
玩世不恭
复制代码

拓展方法

当拓展遇到本尊 真假美猴王

如果被扩展的类的扩展方法与该类的成员方法名字和参数一样,该类对象调用该方法时,调用的会是成员方法。

成员方法优先于拓展方法

class Person{
    fun eat(){
        println("吃吃吃")
    }
}
fun Person.playMan(){
    println("玩世不恭")
}
fun Person.eat(){
    println("大吃一惊")
}

fun main() {
    var per = Person()
    per.eat() // 拓展遇到本尊,就会怂了
    per.playMan()
}
复制代码

系统自带的类也能拓展

举个例子,为系统类String添加拓展方法

//为String定义一个拓展方法 
fun String.lastIndex() = length - 1
复制代码

str.lastIndex()方法执行的过程为:
​ 1、检查str类型(发现为String类型);

​ 2、检查String是否定义了lastIndex()成员方法,如果定义了,编译直接通过;

​ 3、如果String没定义lastIndex()方法,kotlin开始查找程序是否有为String类扩展了lastIndex()方法(即是否有fun String.lastIndex()),如果有定义该扩展方法,会执行该扩展方法;

​ 4、既没定义lastIndex()成员方法也没定义扩展方法,编译自然不通过。

当子父类都拓展了同一个方法

由于静态调用扩展方法是在编译时执行,因此,如果父类和子类都扩展了同名的一个扩展方法,引用类型均为父类的情况下,会调用父类的扩展方法。

/**
 * 父类
 */
open class ExtensionTest

/**
 * 子类
 */
class ExtensionSubTest : ExtensionTest()

/**
 * 父类扩展一个test方法
 */
fun ExtensionTest.test() = println("父类扩展方法")

/**
 * 子类扩展一个test方法
 */
fun ExtensionSubTest.test() = println("子类扩展方法")

fun main() {
    val father : ExtensionTest = ExtensionTest()
    father.test()//调用父类扩展方法

    // 注意类型为 ExtensionTest
    val child1 : ExtensionTest = ExtensionSubTest()
    child1.test()//引用类型为父类类型,编译时静态调用的还是父类的扩展方法

    // 注意类型为 ExtensionSubTest
    val child2 : ExtensionSubTest = ExtensionSubTest()
    child2.test()//此时才是调用子类的扩展方法
}

输出:

父类扩展方法
父类扩展方法
子类扩展方法
复制代码

可空类型扩展方法

在扩展函数内, 可以通过 this 来判断接收者是否为 NULL,这样,即使接收者为 NULL,也可以调用扩展函数。

fun Any?.toString(): String {
    if (this == null) return "null"
    // 空检测之后,“this”会自动转换为非空类型,所以下面的 toString()
    // 解析为 Any 类的成员函数
    return toString()
}
fun main(arg:Array<String>){
    var t = null
    println(t.toString())
}

输出:
null

复制代码

以类成员方式定义扩展方法

在某个类里面为其他类定义扩展方法、属性,该扩展的方法,只能在该类中通过被扩展的类的对象调用扩展方法。

以类成员方式定义的扩展,属于被扩展的类,因此在扩展方法直接调用被扩展的类的成员(this可以省略),同时因为它位于所在类中,因此又可以直接调用所在类的成员。

/**
 * 定义一个类包含test方法
 */
class ExtensionTest {
    fun test() = println("ExtensionTest的test方法")
}

/**
 * 定义一个类包含test方法,包含ExtensionTest的一个扩展方法
 */
class ExtensionTest2 {
    val a = "a"
    fun test() = println("ExtensionTest2的test方法")
    fun ExtensionTest.func() {
        println(a)//调用扩展类的成员
        test()//调用被扩展类的成员,相当于this.test()
        this@ExtensionTest2.test()//同名的需要用this@类名的方式来调用
    }

    fun info(extensionTest: ExtensionTest) {
        extensionTest.func()
    }
}

fun main() {
    val extensionTest = ExtensionTest()
    val extensionTest2 = ExtensionTest2()
    extensionTest2.info(extensionTest)
}


输出:

a
ExtensionTest的test方法
ExtensionTest2的test方法

复制代码

带接收者的匿名扩展函数

  • 扩展方法(fun 类名.方法名())去掉方法名就是所谓的带接收者的匿名扩展函数,接收者就是类本身,形如:fun Int.() : Int。
/**
 * 定义一个空类
 */
class ExtensionTest

/**
 * 为空类定义一个带接收者的匿名扩展函数
 */
var noNameExtensionFun = fun ExtensionTest.(param: String): String {
    println(param)
    return "我是来自带接收者的匿名扩展函数的返回值"
}

fun main() {
    val extensionTest = ExtensionTest()
    println(extensionTest.noNameExtensionFun("向带接收者的匿名函数传入的参数"))//使用匿名扩展函数
}

输出:

向带接收者的匿名函数传入的参数
我是来自带接收者的匿名扩展函数的返回值
复制代码

与普通函数一样,匿名扩展方法也有函数类型,上述例子中,函数类型为:ExtensionTest.(String) -> String

拓展属性

  • kotlin允许动态为类扩展属性,扩展属性是通过添加get、set方法实现,没有幕后字段(不能直接赋值)。

扩展属性也没有真的为该类添加了属性,只能说是为该类通过get、set方法计算出属性。

拓展属性的限制
1、扩展属性不能有初始值;
2、扩展属性不能用filed关键字访问幕后字段;
3、val必须提供get方法,var必须提供get和set方法。

来份代码

/**
 * 定义一个类,包含属性param1、属性param2
 */
class ExtensionTest(var param1: String, var param2: String)

/**
 * 为该类扩展属性extensionParam
 */
var ExtensionTest.extensionParam: String
    set(value) {
        param1 = "param1$value"
        param1 = "param2$value"
    }
    get() = "$param1-$param2"
复制代码

匿名拓展和lambda

如果能根据上下文推断出接收者类型,则可以使用lambda表达式

/**
 * 定义一个空类
 */
class ExtensionTest

/**
 * 定义一个函数,形参为ExtensionTest.(String) -> String类型,相当于同时为ExtensionTest类扩展了一个匿名扩展函数
 */
fun test(fn: ExtensionTest.(String) -> String) {
    val extensionTest = ExtensionTest()
    println("${extensionTest.fn("匿名扩展函数传入形参")}")
}

fun main() {
    test {
        println(it)
//        println("123")
        "返回值(匿名扩展函数)"
    }
}

输出:

匿名扩展函数传入形参
返回值(匿名扩展函数)
复制代码

六、空类型安全

先来个例子吧

// name为不可为空的变量, 不能赋值为null ,若有判断 if(name==null) 无意义,因为肯定不为null
var name : String = ""

// hobby 为可空变量  因为 String? 
var hobby : String? = ""
复制代码

Kotlin 空类型分为:可空类型 和 非空类型

  • 可空类型:使用?操作符声明可空类型,值可以为null,避免抛出 NPE(NullPointerException);
  • 非空类型:常规变量不能容纳 null

let操作符

如果要只对非空值执行某个操作,安全调用操作符 ? 可以与 let一起使用

例子:例如筛选出一个集合非空的元素

fun main() {
    // 不用 let操作符 的常规写法
    fun filterArr1(arr: Array<String?>):Unit{
        for (item in arr){
            if (item==null){
                continue
            }else{
                println("非空元素为->"+item)
            }
        }
    }

    // ?.let 写法
    // ?.let 出来的数据,都是非空的
    fun filterArr2(arr:Array<String?>):Unit{
        for (item in arr){
            item?.let {
                println("非空元素为->"+item)
            }
        }
    }

    var testArray = arrayOf("什么","是","快乐",null,"星球",null)
    filterArr1(testArray)
    println("==========")
    filterArr2(testArray)
}


输出:

非空元素为->什么
非空元素为->是
非空元素为->快乐
非空元素为->星球
==========
非空元素为->什么
非空元素为->是
非空元素为->快乐
非空元素为->星球

复制代码

Evils操作符(?:   !!  as)

Evils操作符分为,?:   !!  as as?

  • ?: 如果?:左侧表达式非空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式。
  • !! 在一个可空变量使用的时候后面加上!! ,则当该变量为null的时候抛出空指针异常
  • as和as? 分2种情况
    • 当使用as的时候若类型转换失败则抛出类型转换(ClassCastException)异常
    • 当使用as?的时候若类型转换失败则返回null,不会抛出异常

所以!抛出异常的 !! as 尽量少用,用as? ?. ?: let来代替


来点例子吧

  • 来个?:

如果?:左侧表达式非空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式。

fun main() {
    var str: String? = null
    val len1 = str?.length ?: -1
    println(len1) // 输出 -1

    str = "hello"
    val len2 = str?.length ?: -1
    println(len2) // 输出 5
}
复制代码
  • 来个 !!

在一个可空变量使用的时候后面加上!! ,则当该变量为null的时候抛出空指针异常 NPE

fun main() {
    var s: String? = null
    println(s!!.length) //运行时候回报错
}
复制代码

空指针

image.png

  • 来个 asas?

    • 当使用as的时候若类型转换失败则抛出类型转换(ClassCastException)异常
    • 当使用as?的时候若类型转换失败则返回null,不会抛出异常

as 的例子

fun main() {
    var name:String?= 12 as String

    //运行时候回报错  ClassCastException
    // java.lang.Integer cannot be cast to java.lang.String
    println(name)
}
复制代码

image.png

as? 的例子

fun main() {
    var name:String?= 12 as? String
    println(name)  // 输出  null
}

输出:
null
复制代码

七、智能类型转换

kotlin的只能类型转换,可以分为两个情况

  • 自动推断类型并转换
  • 空类型的安全转换

自动推断类型并转换

在 Kotlin 中,只要对类型进行了判断,就可以直接通过父类的对象去调用子类的函数了

open class Person{}

class Student: Person() {
    fun study(){
        println("学一学")
    }
}
fun main() {
    var person:Person = Student()
    if(person is Student){
        person.study()
    }
}

输出:
学一学
复制代码

如果是java中想做这种操作,就显得麻烦很多。

需要这样:

public class Person{
}

public class Student extends Person{
    public  void study(){
        System.out.println("我在学习一门新的语言 Kotlin !");
    }
}

public static void main(String[] args){
    Person person = new Student();
    if(person instanceof Student){
        ((Student) person).study();
    }
}
复制代码

如果不进行智能类型转换呢?

上述的例子中,如果我们不进行 类型判断,直接把父类强转为子类调用study,是会报错的。

fun main() {
    val person = Person()
    (person as Student).study()
}
复制代码

image.png

当然这也是有解决办法,那就是可空类型转换。

空类型的安全转换

空类型的安全转换,其实就是我们文章前面说到的 Evils 操作符里面的 as?as

val person: Person = Person()
val student:Student? =person as? Student
println(student) // null
复制代码

八、类属性的延迟初始化

kotlin中声明属性的时候,是要求必须初始化的,否则就会编辑器就会报错。

class Cat() {
    val name: String // 报错
    var baby: Int // 报错

    val hobby: String = "睡觉" // 不报错
}
复制代码

image.png

但是在开发中我们很多时候并不是立即初始化属性。

比如定义控件名称的时候,我们不会在定义时就fingViewById来初始化控件,而是在页面生命周期函数里面来初始化控件。kotlin为开发者们提供了延迟初始化的方案。

kotlin中延迟初始化,一共分别两种。

  • lateinit
  • by lazy

lateinit 在var使用

.

使用 lateinit 需要注意几个点

  • lateinit只能用于var(val不行),所以经常是lateinit var连用

  • lateinit只能用来修饰类属性,不能用来修饰局部变量

  • lateinit不能修饰基本数据类型,如果int(kotlin基本类型:Double、Int、Float、Long、Short、Byte)

  • lateinit用于只能生命周期流程中进行获取或者初始化的变量,比如 Android 的 onCreate()

  • lateinit var的作用就是让编译期在检查时不要因为属性变量未被初始化而报错。不是说真的可以为空。Kotlin相信当开发者显式使用lateinit var关键字的时候,他一定也会在后面某个合理的时机将该属性对象初始化的。

class Cat() {
    // lateinit 延迟初始化
    lateinit var catName: String // 报错
}

fun main() {
    var cat = Cat()
    //println(cat.catName) // 没初始化,直接这么用是有问题的
    // 报错: lateinit property catName has not been initialized
    
    // 初始化了,调用就没问题了
    cat.catName = "蓝猫"
    println(cat.catName)
}

输出:
蓝猫
复制代码

lateinit在Android中使用

private lateinit var s: String
private val ss:String by lazy { "132" }

fun main() {
    print("懒加载 ss = $ss")
    try {
        print("没有初始化 是s =  $s  \n")   // 必须要初始化之后才能使用
    }catch (e:Exception){
        e.printStackTrace()
    }
    s = "123456"
    print("初始化之后的  s =  $s")
}
复制代码

再来一个

class MainActivity : AppCompatActivity() {

    private lateinit var bt: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        bt = findViewById(R.id.bt)
        bt.setOnClickListener {
            Toast.makeText(baseContext, "click", Toast.LENGTH_SHORT).show()
        }
    }
}
复制代码

.
.
.

by lazy 在val使用

  • by lazy本身是一种属性委托。属性委托的关键字是by

  • lazy只能修饰val,即不可变变量

  • 应用于单例模式(if-null-then-init-else-return),而且当且仅当变量被第一次调用的时候,委托方法才会执行

  • by lazy可以使用于类属性或者局部变量

来个例子

class Cat() {
    // by lazy 当且仅当变量被第一次调用的时候,委托方法才会执行。
    val name by lazy {
        println("只调用一次")
        "大脸猫"
    }
}

fun main() {
    val lazyCat = Cat()
    // lazyCat.name的时候,委托方法执行
    val name = lazyCat.name
    // lazyCat1.name执行的时候,委托方法上面执行过了,不再执行了
    val name1 = lazyCat.name
    
    println("====")
    println("lazy-name:$name")
    println("lazy-name:$name1")
}

输出:
只调用一次
====
lazy-name:大脸猫
lazy-name:大脸猫

复制代码

在 Android 中使用

class MainActivity : AppCompatActivity() {

   private val bt by lazy {
        findViewById<Button>(R.id.bt)
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        bt.setOnClickListener {
            Toast.makeText(baseContext, "click", Toast.LENGTH_SHORT).show()
        }
    }
}
复制代码

# lazy 的3种延迟模式

  • LazyThreadSafetyMode.SYNCHRONIZED (默认模式)
  • LazyThreadSafetyMode.PUBLICATION (可多次 Initializer)
  • LazyThreadSafetyMode.NONE (没有锁用于同步,多个实例)

在使用 lazy 延迟初始化的时候,Kotlin提供了3中模式,源码如下:

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

private val sss:String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { "最后一个函数 可以放在外面 " }
复制代码

.

当我们模式都不用的情况下,默认使用 LazyThreadSafetyMode.SYNCHRONIZED 线程安全模式。源码如下:

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
复制代码

by lazy实现一个单例

双重校验锁式模式的单例

//Java实现
public class SingletonDemo {
    private volatile static SingletonDemo instance;
    private SingletonDemo(){} 
    public static SingletonDemo getInstance(){
        if(instance==null){
            synchronized (SingletonDemo.class){
                if(instance==null){
                    instance=new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

//kotlin实现
class SingletonDemo private constructor() {
    companion object {
        val instance: SingletonDemo by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        SingletonDemo() }
    }
}
复制代码

这里面涉及到几个东西,一个是lambda,一个是高阶函数(将函数用作参数或返回值的函数),一个是属性委托。

九、代理Delegate

代理模式和委托模式类似,但是两者不能认为是同一种模式。

  • 委托模式 Delegation pattern
  • 代理模式 Proxy Pattern

Kotlin 使用 by 关键字来实现委托(delegat) 模式

委托模式 是软件设计模式中的一项基本技巧,在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理

关于代理和委托,可以参考这个文章

[Kotlin | 委托(Delegation)详解](juejin.cn/post/700932…

十、单例object

kt的单例的多种实现方式

  • 饿汉式单例
  • 饿汉式单例
  • 线程安全的懒汉式
  • 双重校验锁式(Double Check)
  • 静态内部类式

饿汉式单例

Kotlin引入了一个叫做object的类型,用来很容易的实现单例模式。

// 超简易的单例
object SimpleSington {
  fun test() {}
}
//在Kotlin里调用
SimpleSington.test()
复制代码

是的你没看错,一行object SimpleSington{}实现单例

这是这是kt的一个语法糖。

其内部真正的实现是这样的

public final class SimpleSington {
   public static final SimpleSington INSTANCE;

   private SimpleSington() {
      INSTANCE = (SimpleSington)this;
   }

   static {
      new SimpleSington();
   }
}
复制代码

因而Kotlin这个超简版单例实现省去了,使用如下

  • 显式声明静态instance变量
  • 将构造函数private化处理

但是在Java和Kotlin混编时,Java代码中调用则需要注意,使用如下

SimpleSington.INSTANCE.test();
复制代码

饿汉式单例

针对饿汉式的潜在问题,我们可以使用懒汉式来解决,即将实例初始化放在开始使用之前。Kotlin版的懒汉式加载代码如下

class LazySingleton private constructor(){
    companion object {
        val instance: LazySingleton by lazy { LazySingleton() }
    }
}
复制代码
  • 显式声明构造方法为private
  • companion object用来在class内部声明一个对象
  • LazySingleton的实例instance 通过lazy来实现懒汉式加载
  • lazy默认情况下是线程安全的,这就可以避免多个线程同时访问生成多个实例的问题

关于懒汉的选择

关于如何选择饿汉式还是懒汉式,通常应该从两方面考虑

  • 实例初始化的性能和资源占用
  • 编写的效率和简洁

对于实例初始化花费时间较少,并且内存占用较低的话,应该使用object形式的饿汉式加载。否则使用懒汉式。

线程安全的懒汉式

大家都知道在使用懒汉式会出现线程安全的问题,需要使用使用同步锁,在Kotlin中,如果你需要将方法声明为同步,需要添加 @Synchronized注解。

//Java实现
public class SingletonDemo {
    private static SingletonDemo instance;
    private SingletonDemo(){}
    public static synchronized SingletonDemo getInstance(){//使用同步锁
        if(instance==null){
            instance=new SingletonDemo();
        }
        return instance;
    }
}
//Kotlin实现
class SingletonDemo private constructor() {
    companion object {
        private var instance: SingletonDemo? = null
            get() {
                if (field == null) {
                    field = SingletonDemo()
                }
                return field
            }
        @Synchronized
        fun get(): SingletonDemo{
            return instance!!
        }
    }

}
复制代码

单例 双重校验锁式(Double Check)

//Java实现
public class SingletonDemo {
    private volatile static SingletonDemo instance;
    private SingletonDemo(){} 
    public static SingletonDemo getInstance(){
        if(instance==null){
            synchronized (SingletonDemo.class){
                if(instance==null){
                    instance=new SingletonDemo();
                }
            }
        }
        return instance;
    }
}
//kotlin实现
class SingletonDemo private constructor() {
    companion object {
        val instance: SingletonDemo by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        SingletonDemo() }
    }
}
复制代码

.
.
如何在Kotlin版的Double Check,给单例添加一个属性?

class SingletonDemo private constructor(private val property: Int) {//这里可以根据实际需求发生改变
  
    companion object {
        @Volatile private var instance: SingletonDemo? = null
        fun getInstance(property: Int) =
                instance ?: synchronized(this) {
                    instance ?: SingletonDemo(property).also { instance = it }
                }
    }
}
复制代码

静态内部类式

//Java实现
public class SingletonDemo {
    private static class SingletonHolder{
        private static SingletonDemo instance=new SingletonDemo();
    }
    private SingletonDemo(){
        System.out.println("Singleton has loaded");
    }
    public static SingletonDemo getInstance(){
        return SingletonHolder.instance;
    }
}
//kotlin实现
class SingletonDemo private constructor() {
    companion object {
        val instance = SingletonHolder.holder
    }

    private object SingletonHolder {
        val holder= SingletonDemo()
    }

}
复制代码

十一、内部类

内部类使用 inner 关键字来表示。

内部类会带有一个对外部类的对象的引用,所以内部类可以访问外部类成员属性和成员函数。

class Outer {
    private val bar: Int = 1
    var v = "成员属性"
    /**嵌套内部类**/
    inner class Inner {
        fun foo() = bar  // 访问外部类成员
        fun innerTest() {
            var o = this@Outer //获取外部类的成员变量
            println("内部类可以引用外部类的成员,例如:" + o.v)
        }
    }
}

fun main(args: Array<String>) {
    val demo = Outer().Inner().foo()
    println(demo) //   1
    val demo2 = Outer().Inner().innerTest()   
    println(demo2)   // 内部类可以引用外部类的成员,例如:成员属性
}
复制代码

匿名内部类

对象表达式的格式: Object[: 若干个父类型,中间用逗号分隔]

使用对象表达式来创建匿名内部类:

class Test {
    var v = "成员属性"

    fun setInterFace(test: TestInterFace) {
        test.test()
    }
}

/**
 * 定义接口
 */
interface TestInterFace {
    fun test()
}

fun main(args: Array<String>) {
    var test = Test()

    /**
     * 采用对象表达式来创建接口对象,即匿名内部类的实例。
     */
    test.setInterFace(object : TestInterFace {
        override fun test() {
            println("对象表达式创建匿名内部类的实例")
        }
    })
}
复制代码

嵌套类

嵌套类、内部类 两者 不是 同一个东西

我们可以把类嵌套在其他类中

class Outer {                  // 外部类
    private val bar: Int = 1
    class Nested {             // 嵌套类
        fun foo() = 2
    }
}

fun main(args: Array<String>) {
    val demo = Outer.Nested().foo() // 调用格式:外部类.嵌套类.嵌套类方法/属性
    println(demo)    // == 2
}
复制代码

十二、数据类data class

Kotlin 可以创建一个只包含数据的类,关键字为 data:

例子:

data class User(val name: String, val age: Int)
复制代码

数据类需要满足以下条件:

  • 主构造函数至少包含一个参数。
  • 所有的主构造函数的参数必须标识为val 或者 var ;
  • 数据类不可以声明为 abstractopensealed 或者 inner;
  • 数据类不能继承其他类 (但是可以实现接口)。

copy()

复制使用 copy() 函数,我们可以使用该函数复制对象并修改部分属性

使用 copy 类复制 User 数据类,并修改 age 属性:

data class User(val name: String, val age: Int)


fun main(args: Array<String>) {
    val jack = User(name = "Jack", age = 1)
    val olderJack = jack.copy(age = 2)
    println(jack)
    println(olderJack)

}

输出:
User(name=Jack, age=1)
User(name=Jack, age=2)

复制代码

数据类以及解构声明

组件函数允许数据类在解构声明中使用:

val jane = User("Jane", 35)
val (name, age) = jane // 组件函数允许数据类在解构声明中使用
println("$name, $age years of age") // prints "Jane, 35 years of age"
复制代码

标准数据类

标准库提供了 Pair 和 Triple 。在大多数情形中,命名数据类更好的设计选择,因为这样代码可读性更强而且提供了有意义的名字和属性。

十三、枚举类enum class

  • 枚举类最基本的用法是实现一个类型安全的枚举。

  • 枚举常量用逗号分隔,每个枚举常量都是一个对象。

enum class Color{
    RED,BLACK,BLUE,GREEN,WHITE
}
复制代码

枚举初始化

每一个枚举都是枚举类的实例,它们可以被初始化:

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}
复制代码

.
默认名称为枚举字符名,值从0开始。若需要指定值,则可以使用其构造函数:

enum class Shape(value:Int){
    ovel(100),
    rectangle(200)
}
复制代码

.
枚举还支持以声明自己的匿名类及相应的方法、以及覆盖基类的方法。

enum class ProtocolState {
    WAITING {
        override fun signal() = TALKING
    },

    TALKING {
        override fun signal() = WAITING
    };

    abstract fun signal(): ProtocolState
}
复制代码

如果枚举类定义任何成员,要使用分号将成员定义中的枚举常量定义分隔开
.

使用枚举常量

Kotlin 中的枚举类具有合成方法,允许遍历定义的枚举常量,并通过其名称获取枚举常数。

先来看看 valueOfvalues 这两个方法

EnumClass.valueOf(value: String): EnumClass  // 转换指定 name 为枚举值,若未匹配成功,会抛出IllegalArgumentException
EnumClass.values(): Array<EnumClass>        // 以数组的形式,返回枚举值
复制代码

获取枚举相关信息:

val name: String //获取枚举名称
val ordinal: Int //获取枚举值在所有枚举数组中定义的顺序
复制代码

看个例子,你就明明白白

enum class Color{
    RED,BLACK,BLUE,GREEN,WHITE
}
fun main() {
    var color:Color=Color.BLUE

    println(Color.values().joinToString ())
    println(Color.valueOf("RED"))
    println(color.name)
    println(color.ordinal)
}

输出:
RED, BLACK, BLUE, GREEN, WHITE
RED
BLUE
2
复制代码

十四、密封类sealed class

特点

  • 密封类用来表示受限的类继承结构当一个值为有限几种的类型, 而不能有任何其他类型时。

  • 声明一个密封类,使用 sealed 修饰类,密封类可以有子类,但是所有的子类都必须要内嵌在密封类中

    • 在某种意义上,他们是枚举类的扩展。枚举类型的值集合 也是受限的,但每个枚举常量只存在一个实例,而密封类 的一个子类可以有可包含状态的多个实例
  • sealed 不能修饰 interface ,abstract (会报 warning,但是不会出现编译错误)

某种程度上,密封类是一个强化的枚举类,枚举更在意数据,Sealed 更在意类型

来点例子

例子1

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

fun eval(expr: Expr): Double = when (expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
}
复制代码

使用密封类的关键好处在于使用 when 表达式 的时候,如果能够 验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了。

fun eval(expr: Expr): Double = when(expr) {
    is Expr.Const -> expr.number
    is Expr.Sum -> eval(expr.e1) + eval(expr.e2)
    Expr.NotANumber -> Double.NaN
    // 不再需要 `else` 子句,因为我们已经覆盖了所有的情况
}
复制代码

.
.
例子2

// 外部无法实例化密封的类
// 外部只能实例化他的子类
sealed class Color {
    // 只能在内部继承密封类
    class Red(val value: Int) : Color()
    class Green(val value: Int) : Color()
    class Blue(val name: String) : Color()
}

fun isInstance(color: Color) {
    when (color) {
        // 必须写全所有条件,否则报错
        is Color.Red -> println("这是红色")
        is Color.Green -> println("这是绿色")
        is Color.Blue -> println("这是蓝色")
    }
}


fun main() {
    isInstance(Color.Red(123))
}

输出:
这是红色
复制代码

十五、内联类inline class

  • Kotlin 引⼊了⼀种被称为内联类的特殊类,它通过在类的前⾯定义⼀个 inline 修饰符来声明。

  • 内联类必须含有唯⼀的⼀个属性在主构造函数中初始化。在运行时,将使用这个唯⼀属性来表示内联类的实例。这就是内联类的主要特性,类的数据被 “内联”到该类使用的地方。

  • 内联类的唯一作用是成为某种类型的包装(类似于Java的装箱类型 Integer、Double)

内联类的注意点

  • 最多一个参数 (类型不受限制)(内联类必须含有唯一的一个属性在主构造函数中)
  • 不能有幕后字段(不能直接赋值)
  • 被包装类不能是泛型
  • 不能有 init 块
  • 不能继承其他类,也不能被继承
  • 从接口继承,具有属性和方法

例子

inline class BoxInt(val value: Int): Comparable<Int> {
    override fun compareTo(other: Int)
            = value.compareTo(other)

    operator fun inc(): BoxInt {
        return BoxInt(value + 1)
    }
}
复制代码

Typealias 看起来与内联类相似,但是类型别名只是为现有类型提供了可选名称,而内联类则创建了新类型。


参考:
www.cnblogs.com/nicolas2019…
www.jianshu.com/p/ff6f5101e…
www.jianshu.com/p/c9bd56a64…
droidyue.com/blog/2017/0…
www.jianshu.com/p/5797b3d0e…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享