此文是在阅读《Kotlin进阶实战》中所写的一些笔记记录,其中一些内容之前学习过故会引出连接。
函数与类
函数
基本格式就是:
fun <函数名>(): <返回值> {
...
}
这些基本的与Java
差不多的就不再赘述了。
返回Unit的函数
在Kotlin中,没有void
,函数总会返回一个值,如果该函数不返回任何类型的对象,那么就会返回Unit
类型。
fun printHello(): Unit {
println("Hello World")
}
其中Unit
可以省略:
fun printHello() {
println("Hello World")
}
返回Nothing的函数
与Unit
相比,Nothing
的区别是在Nothing
后面执行的代码,均不能执行。
fun doForever(): Nothing {
while(true) {
println("do something")
}
}
fun main() {
doForever()
println("done") //IDE上会提示"Unreachable code"
}
单表达式函数
当函数只是返回一个表达式时括号可省略:
fun sum(x: Int, y: Int): Int {
return x + y
}
等同于:
fun sum(x: Int,y: Int) = x + y
返回的类型Int
也被省略的原因是Kotlin自身的推导机制。
局部函数
局部函数,指在一个函数中定义另一个函数,类似于内部类。在局部函数中,可以直接访问外部函数的局部变量。
fun validate(username: String): Boolean {
fun validateInput(input: String?) {
if(input == null || input.isEmpty()) {
throw IllegalArgumentException("must not be empty")
}
}
validateInput(username)
return true
}
尾递归函数
尾调用指一个函数最后一个动作是一个函数调用的情况,即这个调用的返回值直接被当前函数返回。这种情况下称该调用位置为尾位置。若这个函数在尾位置调用本身(或调用本身的其他函数等),则称为尾递归,是递归的一种特殊情形。
尾调用不一定是递归调用(因为可以调用其他函数),但尾递归特别有用
fun sum(n: Int, result: Int): Int = if(n <= 0) result else sum(n - 1, result + n)
fun main() {
println(sum(100000, 0))
}
这时会抛出Exception in thread "main" java.lang.StackOverflowError
的异常,因为在Kotlin中使用尾递归有两个条件:
- 使用
tailrec
关键词修饰函数 - 在函数最后进行递归调用
加上后tailrec
关键字后:
tailrec fun sum(n: Int, result: Int): Int = if(n <= 0) result else sum(n - 1, result + n)
再运行就能编译成功了。
类
与Java不同,Kotlin的类默认都是final
的,如果某一个类需要被其他类继承,就需要使用open
修饰。Kotlin的类有一个共同的超类Any
。
构造函数和初始化块
一个类可以包括一个主构造函数和N个次构造函数。
在Kotlin进阶学习这篇文章中,提到过不使用次构造函数而是使用函数默认参数值的方法。
主构造函数
Kotlin的主构造函数可以借助初始化块对代码进行初始化。Kotlin使用init
关键字作为初始化前缀:
class <类名> {
init { //初始化块
...
}
}
初始化块可以有多个,调用主构造函数时会按照初始化块的顺序执行
上述init
代码实际上等价于使用constructor
关键字作为构造函数的函数名,只不过可以省略:
class <类名> constructor{
init { //初始化块
...
}
}
次构造函数
Kotlin的次构造函数同样使用constructor
作为函数名,但不能省略函数名。次构造函数可以包含代码,调用次构造函数时必须调用主构造函数。
//次构造函数
class Constructor(str: String) {
init {
println(str)
}
constructor(str1: String, str2: String):this(str1) {
println("$str1 $str2")
}
fun foo() = println("this is foo function")
}
fun main() {
val obj = Constructor("hello", "world")
obj.foo()
}
特性:
- 类可以有多个次构造函数
- 主构造函数的属性可以用
var
,val
修饰,而次构造函数则不能进行修饰。 - 每个次构造函数需要委托给主构造函数,调用次构造函数时会先调用主构造函数以及初始化块
属性
声明属性的完整语法:
var <propertyName> [: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
var
可以有getter
和setter
两种方法,而val
只有getter
方法。比如:
data class HttpResponse<T>(
var code: Int = -1,
var message: String? = null,
var data: T? = null
) : Serializable {
val isOkStatus: Boolean
get() = code == 0
}
幕后字段(backing field)
这是Kotlin属性自动生成的字段,只能在当前属性的访问器内部使用,且拓展属性也不能使用backing field。如:
kotlinvar paramValue: Int = 0 get() = paramValue set(value) { this.paramValue = value }
当我们尝试获取值时就会递归调用
getter
。Kotlin为每个属性提供了自动的backing field
,可以使用field
访问,便于在使用getter(),setter()
时替换变量。kotlinvar paramValue: Int = 0 get() = field set(value) { field = value }
抽象类
与Java相同,不多赘述。
嵌套类和内部类
嵌套类
Kotlin嵌套类是指在某一个类内部的类,不能访问外部类成员。
Class Outter {
val str:String = "Hello world"
class Nested {
fun foo() = println("")
}
}
这时候去调用Outter1.Nested().foo()
就会报错。
内部类
我们如果把它声明成内部类,就可以了:
Class Outter {
val str:String = "Hello world"
inner class Nested {
fun foo() = println("")
}
}
枚举类
Kotlin中的枚举类需要用enum
和class
两个关键字修饰:
enum class Color constructor(var colorName: String, var value: Int) {
RED("红色", 1), GREEN("绿色", 2), BLUE("蓝色", 3)
}
对象声明和对象表达式
对象声明、对象表达式、和伴生对象都用到了object
关键字。
对象声明
在object
关键字后面指定对象名称,可以实现单例模式。
object Singleton1 {
fun printlnHelloWorld() = println("hello world")
}
对象表达式
类似于Java匿名内部类,比如在网络请求接口回调时,Callback
接口经常写成对象表达式:
override fun onLogin(username: String, password: String) {
model.login(username, password, object : Callback {
override fun onFailure(call: Call, e: IOException) {
view.showError(e.message.toString())
}
override fun onResponse(call: Call, response: Response) {
...
}
})
}
伴生对象
伴生对象相当于Java的静态代码块,因为Kotlin本身没有static
关键字。
class Student {
companion object {
...
}
}
数据类
Kotlin中用data
关键字修饰类:
data class User(var name: String, var password: String)
数据类不能被继承,那如何在同一超类型的多个数据类之间共享属性、方法呢?可以用抽象类或接口,在父类中共享一个属性使用
abstract
修饰,然后在子类里面覆盖
密封类
Kotlin中的密封类用sealed
关键字修饰。
sealed class Mammal(val name: String)
class Dog(dogName: String): Mammal(dogName)
class Horse(horseName: String) : Mammal(horseName)
class Human(humanName: String, val job: String): Mammal(humanName)
fun greetMammal(mammal: Mammal) = when(mammal) {
is Dog -> "Hello ${mammal.name}"
is Horse -> "Hello ${mammal.name}"
is Human -> "Hello ${mammal.name}, You're working as a ${mammal.job}"
}
fun main() {
println(greetMammal(Dog("wang")))
println(greetMammal(Horse("chitu")))
println(greetMammal(Human("tony", "coder")))
}
密封类特点:
- 密封类是一个抽象类
- 跟
when
表达式配合使用时,如果能覆盖所有情况,则无需添加else
。
Kotlin函数式编程
函数式编程与高阶函数
在函数式编程中,函数是头等对象即头等函数,这意味着一个函数,既可以作为其它函数的输入参数值,也可以从函数中返回值,被修改或者被分配给一个变量。λ演算是这种范型最重要的基础,λ演算的函数可以接受函数作为输入参数和输出返回值。
比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。
高阶函数曾经对高阶函数有过记录,故在这儿放个链接。
lambda表达式
lambda函数式编程lambda表达式同样也记录过
集合,序列和Java中的流
map
对集合执行一个操作并获取其上下文:
listOf("java","kotlin","scala","groovy")
.map{it.toUpperCase()}
.foreach(::println)
flatmap
遍历所有元素,为每一个创建一个集合,最后将集合放在一个集合中。
Sequence
序列是另一种容器类型,类似于集合将集合转成序列只需要listOf().asSequence()
,序列与集合有着相同的函数API。
使用Sequence有助于避免不必要的临时分配开销,可以显著提高复杂处理PipeLines的性能。
序列和流
序列和流都使用的是惰性求值。
惰性求值被称为传需求调用,是计算机编程的一个概念,目的是最小化计算机要做的工作。可以表示为“延迟求值”和“最小化求值”。除了可以提升性能,还可以构造一个无限的数据类型。
内联函数与扩展函数
委托
委托介绍
**委托模式(delegation pattern)**是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。委托模式是一项基本技巧,许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式。委托模式使得我们可以用聚合来替代继承,它还使我们可以模拟mixin。
在Kotlin中支持委托模式,使用by
关键字来委托
//接口
interface Base {
fun print()
}
//实现此接口被委托的类
class BaseImpl(val x: Int) : Base {
override fun print() {
print(x)
}
}
//看成代理类,使用by关键字进行委托
class Derived(b: Base) : Base by b
fun main() {
//委托
val base = BaseImpl(10)
Derived(base).print() //输出10
}
委托属性
顾名思义,就是将自身属性的值的管理委托一个代理类进行统一管理,不再依赖于自己的getter/setter
方法。委托属性的语法:
val/var <property name>: <Type> by <expression>
如:
//委托属性
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "${property.name}: $thisRef"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("value=$value")
}
}
class User {
var name: String by Delegate()
var password: String by Delegate()
}
fun main() {
//委托属性
val u = User()
println(u.name)
u.name = "Tony"
println(u.password)
u.password = "123456"
}
泛型
类型擦除
Kotlin的泛型拥有自己的特点。比如对扩展函数涉及泛型的类,需要指定泛型参数,必须是具体类型或子类型。
fun <T: View> T.longClick(block: (T) -> Boolean) = setOnLongClickListener{block(it as T)}
Java通过类型擦除支持泛型
类型擦除是在使用泛型时,编译时会将类型擦除掉,比如List<String>
等均会被擦除成List<Object>
。
在Kotlin中,我们可以通过声明匿名内部类、反射和内联函数来获得泛型信息,以匿名内部类为例:
object Generic1 {
open class InnerClass<T>
fun main() {
val innerClass = object : InnerClass<Int>() {
//匿名内部类的声明在编译时进行,实例化在运行时进行
}
}
}
型变
类和类型
Kotlin中,类和类型是不一样的概念。类型总结了具有相同特征的一组对象的共同特征。类型可以说是一个抽象接口,它指定了如何使用对象。类表示该类型的实现,它是具体的数据结构和方法集合。比如List
是类,而List<String>
是类型。
String | Yes | Yes |
String? | No | Yes |
List | Yes | Yes |
List<String> | No | Yes |
同理,子类与子类型也有很大区别。
任何时候,如果需要的是A类型值的任何地方,都可以使用B类型值来替换,就可以说B类型是A类型的子类型或者称A类型是B类型的超类型。
型变
型变是指类型转换后的继承关系。分为逆变,协变和不变。
协变
如果A是B的子类型,并且Generic<A>
是Generic<B>
的子类型,那么Generic<T>
可以称为一个协变类。
Java上界通配符<? extends T>
Java协变是通过上界通配符实现。
如果Dog
是Animal
的子类,但List<Dog>
并不是List<Animal>
的子类。例如,下面的代码会在编译时报错:
List<Animal> animals = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
animals = dogs;
而如果我们使用上界通配符后,List<Dog>
变成了List<? extends Animal>
的子类型,即animals
变成了可以放入任何Animal
及其子类的List
。
List<? extends Animal> animals = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
animals = dogs;
Kotlin关键词out
var animals: List<Animal> = ArrayList()
val dogs = ArrayList<Dog>
animals = dogs
上述代码在Kotlin中居然没有编译报错,因为Kotlin的List源码中使用了out
,相当于Java的上界通配符。
当类的参数类型使用了
out
后,该参数只能出现在方法的返回类型中。
@UnsafeVariance
List的contains(),containsAll(),indexOf(),lastIndexOf()
方法中,入参均出现了泛型E
,并使用@UnsafeVariance
修饰,因为这里的修饰,才打破了out
的使用限制,否则编译会报错。
逆变
如果A是B的子类型,并且Generic<B>
是Generic<A>
的子类型,那么Generic<T>
可以称为一个逆变类。
Java的下界通配符<? super T>
Java的逆变通过下界通配符实现。
List<? extends Animal> animals = new ArrayList<>();
animals.add(new Dog());
上面代码会报错,因为他是协变的,无法添加新的对象。(原因:这个数组表示为Animal
的任意一种子类的数组,但不确定具体是哪一种,故编译器无法确定具体类型,所以为了类型安全,禁止添加任何具体类型的对象。)
使用下界通配符后,代码就能编译通过:
List<? super Animal> animals = new ArrayList<>();
animals.add(new Dog());
? super Animal
表示Animal
及其父类。所有animals
可以接受所有Animal
的子类到列表中。
Java的上界通配符和下界通配符符合
PESC(Profucer Extends, Consumer Super)
原则。如果参数化类型是一个生产者,则使用<? extends T>
;如果是一个消费者,则使用<? super T>
。生产者表示频繁往外读取数据T,而不从中添加数据。消费者表示只往里插入数据而不读取数据。
Kotlin的关键词in
相当于Java的下界通配符。同样也是在List
的源码中使用的。
当类的参数类型使用
in
后,该参数只能出现在方法的入参之中。
不变
默认情况下,Kotlin中的泛型类是不变的。这意味着它们既不是协变的,也不是逆变的。
泛型约束,类型投影与星号投影
泛型约束
Java中用extends
关键字指明上界,Kotlin中用:
代替extends
对泛型的类型上界进行约束。
上界
fun <T: Number> sum(vararg param: T) = param.sumByDouble { it.toDouble() }
fun main() {
val result1 = sum(1,10,0.6)
val result2 = sum(1,10,0.6,"kotlin") //compile error
}
上述代码传参只能是Number
及其子类,传其他类型时就会报错。
Kotlin默认的上界是Any?
where
关键字
一个类型参数需要指定多个约束时,Java中使用&
连接多个类和接口。
而在Kotlin中,使用where
关键字来实现多个约束
class MyClass<T>(var variable: Class<T>) where T: Class A, T: ClassB
类型投影
MutableList
是不可变的,可读可写。那假如我们对其使用in
或out
修饰呢?
fun main() {
val list1: MutableList<String> = mutableListOf()
list1.add("hello")
list1.add("world")
val list2:MutableList<out String> = mutableListOf()
list2.add("hello")
list2.add("world") // compile error
val list3: MutableList<in String> = mutableListOf()
list1.add("hello")
list1.add("world")
lateinit var list4: MutableList<String>
list4 = list3 // compile error
}
使用out
时会报错,因为该参数只能出现在方法的返回类型中。而使用in
进行赋值时报错是因为只能出现在入参中。
星号投影
星号投影表示不知道关于泛型实参的任何信息。
类似于Java中的无界类型通配符"?",Kotlin使用星号投影""。""代指了所有类型,相当于Any?
。例如, MutableList<*>
表示MutableList<out Any?>
。
而由于使用out
修饰以及星号投影类型的不确定性,导致写入的任何值都有可能跟原有类型冲突。因此星号类型不能写入,只能读取。