kotlin语法学习笔记(1)

335 阅读17分钟

val 定义为不可变变量:

永远优先使用val定义变量,使程序更加健壮。

var 定义一个可变变量

函数语法:

image.png

if

kotlin中的if和java的区别是它可以有返回值,返回值是if每一个条件的最后一句代码。
image.png
可以简化为:
image.png

when

类似java的switch:
image.png

循环

while与java完全相同
for-in类似python
区间:
image.png 下标0-10
image.png 下标0-9
downTo降序迭代:
image.png

面向对象

类:
image.png

1.主构造函数:

每个类默认都会有一个不带参数的主构造函数,主构造函数特点是没有函数体,直接定义在类名后面即可。如果想编写一些逻辑,逻辑部分可以写在init结构体中。 image.png
父类后面为什么会有一个括号"Person()": 子类的构造函数必须要调用父类的构造函数。上面的代码表示调用父类Person的无参数构造函数。也就是说子类的主构造函数调用父类中哪个构造函数,在继承的时间通过这个括号来指定。

调用两个参数父类构造函数的情况: image.png
name和age两个字段不能声明为val,因为主构造函数中声明成val和var的参数将自动成为该类的字段,就会导致和父类中同名的name和age字段造成冲突。

2.次构造函数:

image.png image.png 次构造函数通过**constructor**来定义,任何一个类只能有**一个**主构造函数,但可以有**多个**次构造函数。

次构造函数有函数体,它也可以实例化一个类。

当一个类既有主构造函数又有次构造函数,次构造函数必须调用主构造函数。通过this调用主构造函数和其它次构造函数。

类中只有次构造函数的情况:这种情况十分少见:
image.png
上面代码没有显示定义主构造函数,只定义了次构造函数。因为没有主构造函数,继承父类Person时也不用加上括号了,在次构造函数里使用super关键字直接调用父类的构造函数就行了。super使用类似java。

接口

java中继续使用关键字extends,实现接口使用implements,而kotlin中统一使用冒号:

数据类与单例类

一个类前面声明了data,这就是一个数据类,kotlin会默认自动实现equals()、hashCode()、toString()。
image.png
某个类在全局最多只能拥有一个实例,它就是单例类。kotlin中只需要将class关键字改成object关键字即可。
image.png
调用类似java的静态方法:
image.png

lambda编程

Lambda定义:Lambda就是一小段可以作为参数传递的代码。最后一行代码会自动作为Lambda表达式的返回值。其它就是一个函数,->前面是函数的参数 ->后面是函数体。
语法结构: image.png

    1. 无参数的情况 :
    val/var 变量名 = { 操作的代码 }

    2. 有参数的情况
    val/var 变量名 : (参数的类型,参数类型,...) -> 返回值类型 = {参数1,参数2,... -> 操作参数的代码 }

    可等价于
    // 此种写法:即表达式的返回值类型会根据操作的代码自推导出来。
    val/var 变量名 = { 参数1 : 类型,参数2 : 类型, ... -> 操作参数的代码 }

    3. lambda表达式作为函数中的参数的时候,这里举一个例子:
    fun test(a : Int, 参数名 : (参数1 : 类型,参数2 : 类型, ... ) -> 表达式返回类型){
        ...
    }

maxBy函数就是一个函数式API,接收一个lambda为参数: image.png
下面进行一步一步简化讲解。
不需要专门定义一下变量,直接传入lambda表达式:
image.png
当Lambda参数是函数的最后一个参数时,可以将Lambda表达式移到函数括号的外面: image.png
如果Lambda参数是函数的唯一一个参数的话,还可以将函数的括号省略: image.png
Lambda表达式中的参数列表其实在大多数情况下不必声明参数类型:
image.png
当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替:
image.png
类似的函数式API:map() filter() any() all()

Kotlin调用java方法时使用函数式API

Android SDK还是使用Java语言编写的,造成kotlin中还要使用一些java方法。
kotlin中调用一个java方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式API。
单抽象方法接口就是接口中只有一个待实现的方法。如果有多个待实现方法,则无法使用函数式API。
例子:

public interface Runnable {
    void run();
}

Runnable接口是一个Java单抽象方法接口,Thread类的构造方法中接收了一个Runnable参数。
java版本:

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

以上代码创建了一个Runnable接口的匿名类实例,并将它传给了Thread类的构造方法,最后调用Thread类的start()方法执行这个线程。
kotlin版本:

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

Kotlin完全舍弃了new关键字,因此创建匿名类实例的时候就不能再使用new了,而是改用了object关键字。这种写法虽然算不上复杂,但是相比于Java的匿名类写法,并没有什么简化之处。
因为Thread类的构造方法是符合Java函数式API的使用条件,可进行简化。
kotlin进行简化:

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

Runnable类中只有一个待实现方法,即使这里没有显式地重写run()方法,Kotlin也能自动明白Runnable后 面的Lambda表达式就是要在run()方法中实现的内容。
如果一个Java方法的参数列表中有且仅有一个Java单抽象方法接口参数,我们还可以将接口名进行省略:

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

和之前Kotlin中函数式API的用法类似,当Lambda表达式是方法的最后一个参数时,可以将Lambda表达式移到方法括号的外面。同时,如果Lambda表达式还是方法的唯一一个参数,还可以将方法的括号省略,最终简化结果如下:

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

android开发的例子,java代码去注册这个按钮的点击事件:

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

kotlin的函数式API写法:

button.setOnClickListener {
}

看看下面的listView.setOnItemClickListener{}:

class MainActivity : AppCompatActivity() {
    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initFruits() // 初始化水果数据
        val adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
        listView.adapter = adapter
        listView.setOnItemClickListener { parent, view, position, id ->
            val fruit = fruitList[position]
            Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
        }
    }

    ...
}

lamdba参数列表_下划线语法糖

按住Ctrl键(Mac系统是command键)点击setOnItemClickListener()方法查看它的源码: image.png
它的唯一待实现方法onItemClick()中接收4个参数,这些就是我们要在Lambda表达式的参数列表中声明的参数了。
虽然这里我们必须在Lambda表达式中声明4个参数,但实际上却只用到了position这一个参数而已。针对这种情况,Kotlin允许我们将没有用到的参数使用下划线来替代,因此下面这种写法也是合法且更加推荐的:

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

它们之间的位置是不能改变的,position参数仍然得在第三个参数的位置。

空指针

变量前?号:
表示变量可以为空。比如:Int表示不可为空的整型,而Int?就表示可为空的整型;String表示不可为空的字符串,而String?就表示可为空的字符串。
?.操作符:
image.png
上面代码相当于:
image.png
?:操作符:
如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。
!!操作符 非空断言工具
image.png

这是一种有风险的写法,意在告诉Kotlin,我非常确信这里的对象不会为空,所以不用你来帮我做空指针检查了,如果出现问题,你可以直接抛出空指针异常,后果由我自己承担,强行通过编译。
let函数
image.png
这里调用了obj对象的let函数,然后Lambda表达式中的代码就会立即执行,并且这个obj对象本身还会作为参数传递到Lambda表达式中。不过,为了防止变量重名,这里我将参数名改成了obj2,但实际上它们是同一个对象,这就是let函数的作用。

语法糖Geter和Setter

image.png

以上的java类,kotlin中可以用更简单的方法调用:
image.png
看起来是直接对pages字段进行了赋值和读取,背后调用了setPages()方法和getPages()方法。这是kotlin的一个语法糖。

with用法

语法:

val result = with(obj) {
    // 这里是obj的上下文
    "value" // with函数的返回值
}

例子:

val list = listOf("Apple", "Banana", "Orange", "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)

上面代码多次调用stringBuilder对象的方法,可以用with进行简化:

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

with函数接收两个参数:第一个参数可以是一个任意类型的对象,第二个参数是一个Lambda表达式。with函数会在Lambda表达式中提供第一个参数对象的上下文,并使用Lambda表达式中的最后一行代码作为返回值返回。with函数的第一个参数传入了一个StringBuilder对象,接下来整个Lambda表达式的上下文就会是这个StringBuilder对象,就可以直接调用,并在最后一行代码进行返回。

run用法

val result = obj.run {
    // 这里是obj的上下文
    "value" // run函数的返回值
}

与with相比,总体来说变化非常小,只是将调用with函数并传入StringBuilder对象改成了调用
StringBuilder对象的run方法。

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

apply函数

apply函数和run函数也是极其类似的,都要在某个对象上调用,并且只接收一个Lambda参数,也会在Lambda表达式中提供调用对象的上下文,但是apply函数无法指定返回值,而是会自动返回调用对象本身。

val result = obj.apply {
    // 这里是obj的上下文
}
// result == obj

示例:

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

静态方法

1.companion object

单例类里面的所有方法都是使用类似于静态方法的调用方法(Util.doAction())。

object Util {
    fun doAction() {
        println("do action")
    }
}

要使一个类中某一个方法使用静态方法调用方式,方法前面加companion object:

class Util {
    fun doAction1() {
        println("do action1")
    }

    companion object {
        fun doAction2() {
            println("do action2")
        }
    }
}

doAction1使用Util实例调用,doAction2使用Util.doAction2()调用。
doAction2()方法其实也并不是静态方法,companion object这个关键字实际上会在Util类的内部创建一个伴生类。使用java代码是无法像使用静态方法那样调用它的。解决方法就是下面的@JvmStatic注解和顶层方法。

2.@JvmStatic注解

class Util {
    fun doAction1() {
        println("do action1")
    }

    companion object {
        @JvmStatic
        fun doAction2() {
            println("do action2")
        }
    }
}

@JvmStatic注解只能加在单例类或companion object中的方法上,如果你尝试加在一个普通方法上,会直接提示语法错误。doAction2()方法已经成为了真正的静态方法,那么现在不管是在Kotlin中还是在Java中,都可以使用Util.doAction2()的写法来调用了。

3.顶层方法

在android studio中,右键一个包名,新建一个kotlin类,类型选为File,在这个文件里面写的方法就全部为顶层方法,自动编译为静态方法。使用kotlin语言直接使用方法名就可以调用,java语言要调用的话使用新建的kotlin类文件名+Kt.方法名来调用,比如:新建文件名为heler.kt 方法为dosomething() 调用方法为helerKt.dosomething()

lateinit关键字

延迟初始化使用的是lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为null了。

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

更改后:

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)
        ...
    }
}
class MainActivity : AppCompatActivity(), View.OnClickListener {
    private lateinit var adapter: MsgAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        if (!::adapter.isInitialized) {
            adapter = MsgAdapter(msgList)
        }
        ...
    }
}

::adapter.isInitialized可用于判断adapter变量是否已经初始化。虽然语法看上去有点奇怪,但这是固定的写法。然后我们再对结果进行取反,如果还没有初始化,那么就立即对adapter变量进行初始化,否则什么都不用做。

密封类sealed

interface 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 -> result.error.message  
    else -> throw IllegalArgumentException()  
    }      

虽然else完全没有必要,但是因为when语法要求必须要有这个else。这个多出来的else会有潜在风险。 改为密封类:

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 is ${result.error.message}"
}

当在when语句中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应的条件全部处理。这样就可以保证,即使没有编写else条件,也不可能会出现漏写条件分支的情况。而如果我们现在新增一个Unknown类,并也让它继承自Result,此时getResultMsg()方法就一定会报错,必须增加一个Unknown的条件分支才能让代码编译通过。

扩展函数

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

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

向String类添加一个扩展函数:

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

下面可以这样调用了:

val count = "ABC123xyz!@#".lettersCount()

将lettersCount()方法定义成了String类的扩展函数,那么函数中就自动拥有了String实例的上下文。因此lettersCount()函数就不再需要接收一个字符串参数了,而是直接遍历this即可,因为现在this就代表着字符串本身。
Kotlin中的String甚至还有reverse()函数用于反转字符串,capitalize()函数用于对首字母进行大写,等等,这都是Kotlin语言自带的一些扩展函数。

运算符重载

语法:

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

关键字operator和函数名plus都是固定不变的,而接收的参数和函数返回值可以根据你的逻辑自行设定。那么上述代码就表示一个Obj对象可以与另一个Obj对象相加,最终返回一个新的Obj对象。
例子:

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
val money4 = money3 + 20
println(money4.value)

image.png

高阶函数

高阶函数的定义:

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

(String, Int) -> Unit

->左边的部分就是用来声明该函数接收什么参数的,多个参数之间使用逗号隔开,如果不接收任何参数,写一对空括号就可以了。而->右边的部分用于声明该函数的返回值是什么类型,如果没有返回值就使用Unit,它大致相当于Java中的void。

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)
    println("result1 is $result1")
    println("result2 is $result2")
}

::plus和::minus这种写法,这是一种函数引用方式的写法,表示将plus()和minus()函数作为参数传递给num1AndNum2()函数。

调用高阶函数也可以使用Lambda表达式、匿名函数、成员引用等。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 num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    return operation(num1, num2)
}

模仿apply函数的方法:

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

在函数类型的前面加上ClassName. 就表示这个函数类型是定义在哪个类当中的。使用StringBuilder. 当我们调用build函数时传入的Lambda表达式将会自动拥有StringBuilder的上下文,同时这也是apply函数的实现方式。

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

目前只能作用在StringBuilder类上面,而apply函数是可以作用在所有类上面的。如果想实现apply函数的这个功能,需要借助于Kotlin的泛型才行。
再来一个例子:
原代码:

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

kotlin高阶函数改造后代码:

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

调用open函数时,传给open函数的函数类型参数,将自动带有SharedPreferences.Editor类的上下文,所以可以直接使用putString(),PutInt()等函数。

内联函数:

Kotlin的代码最终还是要编译成Java字节码的,但Java中并没有高阶函数的概念,最终产高阶函数中使用的Lambda表达式在底层被转换成了匿名类的实现方式,我们每调用一次Lambda表达式,都会创建一个新的匿名类实例,会造成额外的内存和性能开销,kotlin内联函数可以解决这个性能问题。
内联函数就是将需要调用函数的地方用,用函数代码来直接替换。

noinline

原书看不懂

普通函数与内联函数一个区别:

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
        println(s)
        println("lambda end")
    }
    println("main end")
}
printString begin
lambda start
printString end
main end

因为ambda表达式中是不允许直接使用return关键字的,所以要使用return@printString的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。

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
        println(s)
        println("lambda end")
    }
    println("main end")
}
main start
printString begin
lambda start

现在printString()函数变成了内联函数,我们就可以在Lambda表达式中使用return关键字了。此时的return代表的是返回外层的调用函数,也就是main()函数,因为内联函数对调用函数时行了代码替换。不管是main()函数还是printString()函数,都在return关键字之后停止执行了。

使用内联函数可能出现的错误--crossinline

如果在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。借助借助crossinline关键字就可以很好地解决这个问题。原理:
因为内联函数的Lambda表达式中允许使用return关键字,和高阶函数的匿名类实现中不允许使用return关键字之间造成了冲突。而crossinline关键字就像一个契约,它用于保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了,问题也就巧妙地解决了。
声明了crossinline之后,我们就无法在调用runRunnable函数时的Lambda表达式中使用return关键字进行函数返回了,但是仍然可以使用return@runRunnable的写法进行局部返回。

泛型

泛型两种定义方法:泛型类、泛型方法
定义一个泛型类:

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

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

不想定义一个泛型类,只是想定义一个泛型方法:

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

val myClass = MyClass()  
val result = myClass.method<Int>(123)  
# Kotlin还拥有非常出色的类型推导机制,也可写为:  
val myClass = MyClass()  
val result = myClass.method(123)

设置泛型上界,限制泛型类型:

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

将method()方法的泛型指定成数字类型,比如Int、Float、Double等。但是如果你指定成字符串类型,就肯定会报错,因为它不是一个数字。
在默认情况下,所有的泛型都是可以指定成可空类型的,这是因为在不手动指定上界的时候,泛型的上界默认是Any?。而如果想要让泛型的类型不可为空,只需要将泛型的上界手动指定成Any就可以了。

泛型进行实化

函数必须是内联函数才行,也就是要用inline关键字来修饰该函数。其次,在声明泛型的地方必须加上reified关键字来表示该泛型要进行实化。

inline fun <reified T> getGenericType() {  
}
inline fun <reified T> getGenericType() = T::class.java
fun main() {
    val result1 = getGenericType<String>()
    val result2 = getGenericType<Int>()
    println("result1 is $result1")
    println("result2 is $result2")
}

泛型对于类型的约束只在编译时期存在,运行期间是不知道具体T类型的。假设我们创建了一个List集合,虽然在编译时期只能向集合中添加字符串类型的元素,但是在运行时期JVM并不能知道它本来只打算包含哪种类型的元素,只能识别出来它是个List。所有基于JVM的语言,它们的泛型功能都是通过类型擦除机制来实现的,其中当然也包括了Kotlin。这种机制使得我们不可能使用a is T或者T::class.java这样的语法,因为T的实际类型在运行的时候已经被擦除了。使用泛型实化可以解决这个问题。

泛型的协变

image.png

意味着现在T只能出现在out位置上,而不能出现在in位置上,同时也意味着SimpleData在泛型T上是协变的:

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

构造函数中的泛型T不也是在in位置上的吗?没错,但是由于这里我们使用了val关键字,所以构造函数中的泛型T仍然是只读的,因此这样写是合法且安全的。另外,即使我们使用了var关键字,但只要给它加上private修饰符,保证这个泛型T对于外部而言是不可修改的,那么就都是合法的写法。

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
}

@UnsafeVariance注解,这样编译器就会允许泛型E出现在in位置上了。

泛型的逆变

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

泛型T的声明前面加上了一个in关键字。这就意味着现在T只能出现在in位置上,而不能出现在out位置上,同时也意味着Transformer在泛型T上是逆变的。

委托类和委托属性

类委托:将一个类的具体实现委托给另一个类去完成。
例子:Set是一个接口,如果要使用它的话,需要使用它具体的实现类,比如HashSet。而借助于委托模式,我们可以轻松实现一个自己的实现类。比如这里定义一个MySet,并让它实现Set接口,MySet的构造函数中接收了一个HashSet参数,这就相当于一个辅助对象。然后在Set接口所有的方法实现中,我们都没有进行自己的实现,而是调用了辅助对象中相应的方法实现。
委托模式的意义:为什么不直接使用HashSet类?因为如果我们只是让大部分的方法实现调用辅助对象中的方法,少部分的方法实现由自己来重写,甚至加入一些自己独有的方法,这就是委托的意义。

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写法,作用完全一样,使用by helperSet

class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
}

我们要对某个方法进行重新实现,只需要单独重写那一个方法就可以了,其他的方法仍然可以享受类委托所带来的便利:

class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
    fun helloWorld() = println("Hello World")
    override fun isEmpty() = false
}

MySet成为了一个全新的数据结构类,它不仅永远不会为空,而且还能打印helloWorld(),至于其他
Set接口中的功能,则和HashSet保持一致。这就是Kotlin的类委托所能实现的功能。

委托属性:将一个属性(字段)的具体实现委托给另一个类去完成。
语法:

class MyClass {
    var p by Delegate()
}

代表着将p属性的具体实现委托给了Delegate类去完成。当调用p属性的时候会自动调用Delegate类的getValue()方法,当给p属性赋值的时候会自动调用Delegate类的setValue()方法。

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

在Delegate类中必须实现getValue()和setValue()这两个方法,并且都要使用operator关键字进行声明。getValue()方法要接收两个参数: 第一个参数用于声明该Delegate类的委托功能可以在什么类中使用,这里写成MyClass表示仅可在MyClass类中使用;
第二个参数KProperty<*>是Kotlin中的一个属性操作类,可用于获取各种属性相关的值,在当前场景下用不着,但是必须在方法参数上进行声明。
另外,<*>这种泛型的写法表示你不知道或者不关心泛型的具体类型,只是为了通过语法编译而已,有点类似于Java中<?>的写法。至于返回值可以声明成任何类型,根据具体的实现逻辑去写就行了,上述代码只是一种示例写法。
setValue()方法也是相似的,只不过它要接收3个参数。前两个参数和getValue()方法是相同的,最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和getValue()方法返回值的类型保持一致。
如果p属性使用val关键字,它是一个无法在初始化之后被重新赋值的属性,所以没有必要实现setValue()方法,只实现getValue()方法。

infix关键字

例子:
一个普通的string的starsWith()用法:

if ("Hello Kotlin".startsWith("Hello")) {  
// 处理具体的逻辑  
}
infix fun String.beginsWith(prefix: String) = startsWith(prefix)

定义一个Sring类增加一个beginsWith函数,使用infix语法糖后可以这样调用:

if ("Hello Kotlin" beginsWith "Hello") {  
// 处理具体的逻辑  
}

infix函数允许我们将函数调用时的小数点、括号等计算机相关的语法去掉,从而使用一种更接近英语的语法来编写程序,让代码看起来更加具有可读性。 使用infix的限制:
一、不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类当中。
二、infix函数必须接收且只能接收一个参数,至于参数类型是没有限制的。