这是我参与「第四届青训营 」笔记创作活动的第4天
今天继续学习kotlin语言,本系列文章大概更新到下周二就会结束。
逻辑控制
条件语句
if语句
fun largerNumber(num1: Int, num2: Int): Int {
var value = 0
if (num1 > num2) {
value = num1
} else {
value = num2
}
return value
}
语句非常简单,在学过JAVA/PYTHON/C++等语言的基础上,可以轻松看懂。
Kotlin中的if语句相比于Java有一个额外的功能,它是可以有返回值的,返回值就是if语句每 一个条件中最后一行代码的返回值。因此,上述代码就可以简化成如下形式:
fun largerNumber(num1: Int, num2: Int): Int {
val value = if (num1 > num2) {
num1
} else {
num2
}
return value
}
if语句使用每个条件的最后一行代码作为返回值,并将返回值赋值给了 value变量。由于现在没有重新赋值的情况了,因此可以使用val关键字来声明value变量,最终将value变量返回。
value其实也是一个多余的变量,我们可以直接将if语句返回, 这样代码将会变得更加精简,如下所示:
fun largerNumber(num1: Int, num2: Int): Int {
return if (num1 > num2) {
num1
} else {
num2
}
}
如果考虑昨天的语法糖,还可以做出如下简化:
fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) {
num1
} else {
num2
}
当然,最后有个极致简化版:
fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) num1 else num2
when语句
Kotlin中的when语句有点类似于Java中的switch语句,但它又远 比switch语句强大得多。
when语句允许传入一个任意类型的参数,然后可以在when的结构体中定义一系列的条件,格式是:
匹配值 -> { 执行逻辑 }
when语句和if语句一样, 也是可以有返回值的,因此我们仍然可以使用单行代码函数的语法糖。当你的执行逻辑只有一行代码时,{ }可以省略。
下面是一个例子:
fun getScore(name: String) = when (name) {
"Tom" -> 86
"Jim" -> 77
"Jack" -> 95
"Lily" -> 100
else -> 0
}
除了精确匹配之外,when语句还允许进行类型匹配。
fun checkNumber(num: Number) {
when (num) {
is Int -> println("number is Int")
is Double -> println("number is Double")
else -> println("number not support")
}
}
上述代码中,is关键字就是类型匹配的核心,它相当于Java中的instanceof关键字。由于 checkNumber()函数接收一个Number类型的参数,这是Kotlin内置的一个抽象类,像Int、 Long、Float、Double等与数字相关的类都是它的子类,所以这里就可以使用类型匹配来判断传入的参数到底属于什么类型,如果是Int型或Double型,就将该类型打印出来,否则就打印不支持该参数的类型。
when语句还有一种不带参数的用法,虽然这种用法可能 不太常用,但有的时候却能发挥很强的扩展性。
fun getScore(name: String) = when {
name == "Tom" -> 86
name == "Jim" -> 77
name == "Jack" -> 95
name == "Lily" -> 100
else -> 0
}
Kotlin中判断字 符串或对象是否相等可以直接使用==关键字。
循环语句
while
与其他语言相同。这里不再赘述
for
只需要学习for-in循环的用法即可。
我们可以使用如下Kotlin代码来表示一个区间:
val range = 0..10
在Kotlin中,它表示创建了一个0到10的区间,并且两端都是闭区间,这意味着0到10这两个端点都是包含在区间中的,用数学的方式表达出来就是。 其中,..是创建两端闭区间的关键字,在..的两边指定区间的左右端点就可以创建一个区间.
有了区间之后,我们就可以通过for-in循环来遍历这个区间,比如在main()函数中编写如下 代码:
fun main() {
for (i in 0..10) {
println(i)
}
}
Kotlin中可以使用until关键字来创建一个左闭右开的区间,如 下所示:
val range = 0 until 10
上述代码表示创建了一个0到10的左闭右开区间,它的数学表达方式是。
修改main()函数中的代码,使用until替代..关键字。
默认情况下,for-in循环每次执行循环时会在区间范围内递增1,相当于Java for-i循环中 i++的效果,而如果想跳过其中的一些元素,可以使用step关键字:
fun main() {
for (i in 0 until 10 step 2) {
println(i)
}
}
上述代码表示在遍历这个区间的时候,每次执行循环都会在区间范围内递增2,相当于 for-i循环中i = i + 2的效果。
如果你想创建一个降序的区间,可以使用downTo 关键字,用法如下:
fun main() {
for (i in 10 downTo 1) {
println(i)
}
}
OOP
Kotlin中使用class关键字来声明一个类。
这个类命名为Person
class Person {
var name = ""
var age = 0
fun eat() {
println(name + " is eating. He is " + age + " years old.")
}
}
这里使用var关键字创建了name和age这两个字段,这是因为我们需要在创建对象之后再指定具体的姓名和年龄,而如果使用val关键字的话,初始化之后就不能再重新赋值 了。接下来定义了一个eat()函数,并在函数中打印了一句话.
Person类已经定义好了,接下来我们对这个类进行实例化,代码如下所示:
val p = Person()
上述代码将实例化后的类赋值到了p这个变量上面,p就可以称为Person类的一个实例,也可以称为一个对象。
下面我们开始在main()函数中对p对象进行一些操作:
fun main() {
val p = Person()
p.name = "Jack"
p.age = 19 p.eat()
}
继承与构造函数
这里我们先创建一个学生类:
class Student {
var sno = ""
var grade = 0
}
现在Student和Person这两个类之间是没有任何继承关系的,想要让Student类继承Person 类,我们得做两件事才行。 第一件事,使Person类可以被继承。这里是Kotlin和JAVA不同的地方,在Kotlin中任何一个非抽象类默认都是不可以被继承的,相当于Java中给类声明了final关键字。之所以这么设计,其实和val关键字的原因是差不多的,因为类和变量一样,最好都是不可变的,而一个类允许被继承的话,它无法预知子类会如何实现,因此可能就会存在一些未 知的风险。Effective Java这本书中明确提到,如果一个类不是专门为继承而设计的,那么就应该主动将它加上final声明,禁止它可以被继承。 很明显,Kotlin在设计的时候遵循了这条编程规范,默认所有非抽象类都是不可以被继承的。之所以这里一直在说非抽象类,是因为抽象类本身是无法创建实例的,一定要由子类去继承它才 能创建实例,因此抽象类必须可以被继承才行,要不然就没有意义了。
既然现在Person类是无法被继承的,我们得让它可以被继承才行,方法也很简单,在Person 类的前面加上open关键字就可以了,如下所示:
open class Person { ... }
加上open关键字之后,我们就是在主动告诉Kotlin编译器,Person这个类是专门为继承而设计 的,这样Person类就允许被继承了。
第二件事,要让Student类继承Person类。在Kotlin中继承的关键字是冒号:,写法如下:
class Student : Person() {
var sno = ""
var grade = 0
}
接下来解释Person后面括号的含义
任何一个面向对象的编程语言都会有构造函数的概念,Kotlin中也有,但是Kotlin将构造函数分 成了两种:主构造函数和次构造函数。 主构造函数将会是你最常用的构造函数,每个类默认都会有一个不带参数的主构造函数,当然 你也可以显式地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面即 可。比如下面这种写法:
class Student(val sno: String, val grade: Int) : Person() { }
这里我们将学号和年级这两个字段都放到了主构造函数当中,这就表明在对Student类进行实 例化的时候,必须传入构造函数中要求的所有参数。比如:
val student = Student("2013021", 5)
这样我们就创建了一个Student的对象,同时指定该学生的学号是2013021,年级是5。另外,由 于构造函数中的参数是在创建实例的时候传入的,不像之前的写法那样还得重新赋值,因此我们可以将参数全部声明成val。
Kotlin给我们提供了一个init结构体,所有主构造函数中的逻辑都可以写在里面:
class Student(val sno: String, val grade: Int) : Person() {
init {
println("sno is " + sno)
println("grade is " + grade)
}
}
kotlin规定,子类中的构造函数必须调用父类中的构造函数。根据继承特性的规定,子类的 构造函数必须调用父类的构造函数,可是主构造函数并没有函数体,因此子类的主构造函数调用父类中的哪个构造函数,在继承的时候通过括号来指定。
因此,本案例中,Person类后面的一对空括号表示Student类的主构造函数在初始化的时候会调用 Person类的无参数构造函数,即使在无参数的情况下,这对括号也不能省略。
次构造函数
平时几乎是用不到次构造函数的,Kotlin提供了一个给函数设定参数默认值的功能,基本上可以替代次构造函数的作用。任何一个类只能有一个主构造函数,但是可以有多个次构造函数。次构造函数也可以用于实例化一个类,这一点和主构造函数没有什么不同,只不过它是有函数体的。 Kotlin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。下面给出一个例子:
class Student(val sno: String, val grade: Int, name: String, age: Int) :
Person(name, age) {
constructor(name: String, age: Int) : this("", 0, name, age) {
}
constructor() : this("", 0) {
}
}
次构造函数是通过constructor关键字来定义的,这里我们定义了两个次构造函数:第一个次 构造函数接收name和age参数,然后它又通过this关键字调用了主构造函数,并将sno和 grade这两个参数赋值成初始值;第二个次构造函数不接收任何参数,它通过this关键字调用 了我们刚才定义的第一个次构造函数,并将name和age参数也赋值成初始值,由于第二个次构 造函数间接调用了主构造函数,因此这仍然是合法的。
那么现在我们就拥有了3种方式来对Student类进行实体化,分别是通过不带参数的构造函数、 通过带两个参数的构造函数和通过带4个参数的构造函数,对应代码如下所示:
val student1 = Student()
val student2 = Student("Jack", 19)
val student3 = Student("a123", 5, "Jack", 19)
类中只有次构造函数,没有主构造函数。这种 情况真的十分少见,但在Kotlin中是允许的。当一个类没有显式地定义主构造函数且定义了次构 造函数时,它就是没有主构造函数的。
class Student : Person {
constructor(name: String, age: Int) : super(name, age) {
}
}
首先Student类的后面没有显式地定义主构造函数,同时又因为定义了 次构造函数,所以现在Student类是没有主构造函数的。那么既然没有主构造函数,继承Person类的时候也就不需要再加上括号了。由于没有主构造函数,次构造函数只能直接调用父类的构造函数,上述代码也是将this 关键字换成了super关键字。
总结
本部分是kotlin的难点,事实上也是所有面向对象编程的语言的难点,我记得我在学C++的时候这一部分就面临巨大苦难,因此应该好好掌握。同时,考虑到kotlin的独特语法,比如主构造函数和次构造函数等等,需要更加熟悉才行。因此,大量练习是非常重要的。