Kotlin必知必会(中)
1.1 再讲构造器
- 主构造器、次构造器、初始化代码块、构造属性
// 在类名后面直接写的构造器称为主构造器,age直接在构造器里声明的,称为构造属性
class Person constructor(name: String, private var age: Int) {
companion object {
const val TAG = "Person"
}
var name: String? = null
private var sex: String? = null
private var weight: Float? = null
// 初始化代码块
init {
sex = "男"
}
// 次级构造函数,必须要要调用主构造器通过this(参数列表)
constructor(na:String, age:Int, weight:Float): this(na, age) {
this.name = na
this.age = age
this.weight = weight
}
fun introduce() {
Log.d(TAG, "my name is $name,age is $age, sex is $sex, weight is $weight")
}
}
正如上面注释所写到的,Kotlin 中的类可以有一个 主构造器 (primary constructor), 以及一个或多个 次构造器 (secondary constructor). 主构造器是类头部的一部分, 位于类名称(以及可选的类型参数)之后,并且有一点与Java不同,就是在主构造器中我们可以直接声明成员属性。
当然还有点不同的是,kotlin为我们提供了一个init关键字进行初始化,主构造器中不能包含任何代码. 初始化代码可以放在 初始化代码块中,需要注意的是初始化代码块中待初始化的属性,一定要放到初始化块的上方。
如果类有主构造器, 那么每个次级构造器都必须委托给主构造器, 要么直接委托, 要么通过其他次级构造器间接委托. 委托到同一个类的另一个构造器时, 使用 this 关键字实现。 到这里,我想大家一定会好奇,init代码块和次级构造函数的执行顺序是什么?
初始化代码段中的代码实际上会成为主构造器的一部分. 对主构造器的委托调用, 会作为次级构造器的第一条语句来执行, 因此所有初始化代码段中的代码, 以及属性初始化代码, 都会在次级构造器的函数体之前执行. 即使类没有定义主构造器, 也会隐含地委托调用主构造器, 因此初始化代码段肯定会在次级构造器执行之前调用。
1.2 数据类型(data class)
这个设计也是kotlin的一个亮点,我们在实际项目当中一定会大量的使用的数据类(bean),然后会去做比较相等、复制等操作,对于Java来说就要去重写equals、hashCode、toString等函数。
kotlin比较灵性的一点是对于使用data class声明的类,会自动的根据构造属性生成对应的函数。
代码语言:javascript复制data class User(var name: String?, var age: Int) {
}
class SomeThing {
fun test() {
val user = User("Bob", 18)
var other = User("Kotlin", 18)
// copy()函数只是拷贝的值,并不是引用
val user2 = user.copy()
// Kotlin的"=="相当于Java的equals,比较的
// 是值,如果需要比较引用可以使用三等号"==="
// when如果不加参数,分支条件都是布尔表达式
when {
user == user2 -> println("111")
user === user2 -> println("222")
}
// copy可以指定更改部分属性
other = user.copy(name = "Kotlin")
println("other == user = ${other == user}")
println("other === user = ${other === user}")
// 解构
val (name, age) = user
print(name)
print(age)
}
}
使用data class声明的数据类型,会根据构造器中声明的属性自动推断出equals方法,在kotlin中equals方法等同于双等号“==”,而双等号在Java中是比较引用(地址)是否相等,在kotlin中需要使用三等号“===”。
解构这个是不是蛮有意思,kotlin会按照数据类型User中构造器中声明的属性顺序,拆开来逐个赋值给你的变量。再也不用再一个一个去手动赋值啦。
1.3 吃一波糖
- ?:
我们一定使用过一些在判断一些属性是空时,给它赋上一个默认值的经历,这样的代码写起来也是挺无聊的,kotlin通过?:可以为我们做一些简化,user.name ?: "Kotlin"
代码语言:javascript复制```java
// Java中这么写
if (user.name != null && user.name.length > 3) {}
// 那么利用语法糖可以这么简写
if (user.name?.length ?: 0 > 3) {}
```
> 稍微解释下,上面的user.name如果是null,那么user.name?.length整个被看做为null,那么就会被使用0这个默认值,但是这个简化看上去也没有简化太多,而且可读性显然变的更差了,所以并不要求一定要这么写,但是别人这么写要能看懂。
- when
上一篇我们已经讲过了when,这里再补充一点,when表达式是有返回值的,满足条件的分支的返回值将成为整个表达式的值. 如果用作流程控制语句, 各个分支的返回值将被忽略. (与 if 类似, 各个分支可以是多条语句组成的代码段, 代码段内最后一个表达式的值将成为整个代码段的值.)
如果其他所有分支的条件都不成立, 则会执行 else 分支. 如果 when 被用作表达式, 则必须有 else 分支, 除非编译器能够证明其他分支的条件已经覆盖了所有可能的情况
- 循环和lambda
假设有个User列表,然后找出年龄小于20岁的部分:
代码语言:javascript复制 fun showYoungPerson(users: List<User>) {
val youngPersonList = ArrayList<User>()
for (person in users) {
if (person.age < 20) {
youngPersonList.add(person)
}
}
}
对于里面的for循环我们可以使用foreach函数:
代码语言:javascript复制 users.forEach({ person: User ->
if (person.age < 20) {
youngPersonList.add(person)
}
})
我们可以看到,函数的参数是一个lambda表达式,在kotlin中,如果函数的最后一个参数是lambda表达式,可以直接移到外面来:
代码语言:javascript复制 users.forEach(){ person: User ->
if (person.age < 20) {
youngPersonList.add(person)
}
}
而如果只要一个lambda的参数,foreach后面的小括号都可以直接省略掉的,就变成这样:
代码语言:javascript复制 users.forEach{ person: User ->
if (person.age < 20) {
youngPersonList.add(person)
}
}
如果lambda只有一个参数也是可以省略的,kotlin直接有默认的it指代当前遍历的对象:
代码语言:javascript复制 users.forEach{
if (it.age < 20) {
youngPersonList.add(it)
}
}
其实我们还能简化,kotlin中的容器类提供过滤的函数filter(),他会直接返回符合条件的集合:
代码语言:javascript复制 val youngPersonList = users.filter { it.age < 20 }
kotlin还提供了一个过滤函数partition,他可以返回两个集合,一个是满足条件的,另一个是不满足条件的:
代码语言:javascript复制 val (youngPersonList, oldPersonList) = users.partition { it.age < 20 }
对于单纯做100次循环的操作,kotlin还可以使用repeat函数:
代码语言:javascript复制 repeat(100) {
println(it)
}
- 局部函数
说白了就是函数里面再嵌套个函数,局部函数里面可以访问外部函数的属性,有人可能要问设计这种函数有啥用?我想了下,使用的场景可能就是你的一个函数里想再抽出一个函数,但这个函数只有你这个外部函数会调用,不想被类中的其他地方调用,这个时候你就可以抽成一个局部函数。 需要注意的是,这种嵌套的方式每次在外部函数被调用的时候会生成一个额外的对象,所以如果调用比较平凡的函数还是不要在里面嵌套内部函数。
代码语言:javascript复制 fun outerMethod() {
val name = "kotlin"
fun innerMethod() {
print(name)
}
// 调用内部函数
innerMethod()
}
- 函数默认值
使用函数默认值可以为我们节省写重载函数的时间:
代码语言:javascript复制 // 带默认值的参数
fun test(name: String, age: Int = 18) {
println(name)
println(age)
}
fun call() {
test("kotlin")
test("kotlin", 19)
}
当使用一个参数的时候第二个参数就会使用默认的参数值
- 扩展
这是我个人很喜欢的特性,kotlin中我们可以向任意类扩展函数或者属性,比如我觉得MutableList应该具备可以任意交换数据的函数,那我们就可以自己为MutableList类扩展一个这样的函数,然后我们就可以在所有MutableList的对象中使用这个函数:
代码语言:javascript复制 fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' 指代 list 实例
this[index1] = this[index2]
this[index2] = tmp
}
fun call() {
val list = mutableListOf<String>("A", "B", "C")
list.swap(0, 2)
}
扩展函数中的this代指调用此扩展函数的对象,本质上其实我们并没有为类真的新增一个成员函数,当然如果类的成员函数和你的扩展函数一样,会优先调用成员函数的,其本质是什么呢?其实就是在你自己的类里面新增了一个函数,并且可以通过点号标记法的形式, 对这个数据类型的变量调用这个新函数,我们把上面的扩展函数反编译一下:
代码语言:javascript复制 public final void swap(@NotNull List $this$swap, int index1, int index2) {
Intrinsics.checkParameterIsNotNull($this$swap, "$this$swap");
Object tmp = $this$swap.get(index1);
$this$swap.set(index1, $this$swap.get(index2));
$this$swap.set(index2, tmp);
}
另外需要注意的是,扩展函数的调用是静态的,是根据调用对象的表达式决定的,并不会在根据运行时的类型选择,举个例子:
代码语言:javascript复制 open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
// 表达式中是Shape类型
fun printClassName(s: Shape) {
println(s.getName())
}
// 我们传入的是Rectangle对象
printClassName(Rectangle())
所以输出结果是“Shape”,而并不会是Rectangle。
- inline内联函数
简单点讲,就是使用inline声明的函数,在调用的时候,直接将内联函数体直接copy到调用的位置,也就是说如果这个函数有多处调用,就会多次的将同样的代码插入到调用的位置。如果你的函数比较大,调用的地方又比较多的话,显然不太适合。可能有人问,这个函数有什么好处,好处就是减少了方法栈的一次入栈和退栈操作。