2 kotlin语言
编程语言分为两类:编译型语言和解释型语言。编译型语言的特点是编译器会将我们的源代码一次性的变异成计算机可识别的二进制文件,像c、c++等。而解释型语言它有一个解释器,在运行时,解释器会一行一行地读取我们编写的源代码,然后实时地将这些源代码解释成计算机可识别的二进制数据后再执行,因此解释型语言通常效率会差一些,像python和JavaScript都属于解释型语言。
Java也属于解释型语言,只不过Java代码是先编译再运行,Java代码编译之后生成的是一种特殊的class文件,这种class文件只由Java虚拟机(android中叫ART,一种移动优化版的虚拟机)才能识别,而这个Java虚拟机担当的就是解释器的角色,他会在程序运行时将编译后的class文件解释成计算机可识别的二进制数据后再执行,因此Java属于解释型语言。
而kotlin自己做了一个编译器,将他的代码同样编译成class文件,Java虚拟机就可以识别它了。
kotlin比Java代码更简洁,更高级,同时兼容了Java的很多库,因此更适合作为一门安卓开发语言。kotlin无需分号分割,
- 一个在线运行kotlin代码的网站:https://play.kotlinlang.org
2.1 变量
Kotlin中只允许在变量前声明两种关键字:val和var。
val(value),用于声明一个不可变的变量,相当于Java中的final。
var(variable),用于声明一个可变的变量,对应Java中非final的变量。
kotlin中有类型推导机制,无需自己声明数据类型。var a = 10 而如果对一个变量延迟赋值的话,则需要自己显式地声明变量类型。val a:Int = 10
kotlin中的对象数据类型开头首字母都是大写的,这表示kotlin完全抛弃了Java中的基本数据类型,全部使用了对象数据类型。在Java中int是关键字,而在kotlin中,Int变成了一个类,拥有自己的方法和继承结构。
永远优先使用val来声明一个变量,而当val没法满足你的需求时,再使用var,这样设计出出来的程序会更加健壮,也更符合高质量的编码规范。
2.2 函数
kotlin函数的语法规则如下:
fun methodName(paraml:Int,paraml:Int):Int{
return 0
}
括号里接收的是两个参数,声明格式是“参数名:参数类型”,参数括号后面那部分是可以选择的,用语声明该函数会返回什么类型的数据,如果函数不需要返回任何数据,可以不写。
语法糖(Syntactic Sugar)是编程语言中一种语法层面的 “便捷写法”,它不改变语言的核心功能,只是通过更简洁、更易读的语法形式,让开发者编写代码更方便、更高效。
当一个函数中只有一行代码时,kotlin语序我们不必编写函数体,可以直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可。例如上面的函数可以简化成:
fun largeNumber(num1:Int,num2:Int):Int = max(num1,num2)
由于kotlin出色的类型推导机制,还可以进一步简化:
fun largeNumber(num1:Int,num2:Int) = max(num1,num2)
2.3 程序的逻辑控制
2.3.1 if条件语句
kotlin中if的用法与Java几乎没有任何区别,除了一点:kotlin中的if语句是可以有返回值的,返回值就是if语句每一个条件中最后一行代码的返回值。以上面那个代码为例:
fun largeNumber(num1:Int,num2:Int):Int{
val value = if(num1>num2){
num1
}else{
num2
}
return value
}
//进一步简化
fun largeNumber(num1:Int,num2:Int) = if(num1>num2){
num1
}else{
num2
}
//进一步简化
fun largeNumber(num1:Int,num2:Int) = if(num1>num2) num1 else num2
2.3.2 when语句
类似于Java中的switch语句,但是又比switch语句强大很多。
以一个查询考试成绩的功能函数为例。
fun getScore(name:String) = when(name){
"tom"->87
"jim"->67
"bob"->89
else->0
}
when语句和if语句一样,也是可以有返回值的。when语句允许传入一个任意类型的参数,然后可以在when的结构体中定义一系列的条件,格式是:匹配值->{执行逻辑},当执行逻辑只有一行代码时,可以省略{}。
除此之外,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关键字。
此外,when语句还有一种不带参数的用法,不太常用,但有时候能发挥很强的扩展性。拿刚才的getScore()函数举例,如果我们不在when语句中传入参数的话,还可以这么写:
fun getScore(name:String) = when{
name == "tom"->78
name == "bob"->89
name == "jim"->98
else -> 0
}
注意:kotlin中判断字符串或对象是否相等可以直接使用==关键字,而不用像Java那样调用equals()方法。
你可能会觉得这种无参数的when语句写起来比较冗余,但是有些场景必须使用这种写法才能实现。举个例子,假设所有名字以tom开头的人,他的分数都是86,这种场景如果使用带参数的when语句来写就无法实现,而使用不带参数的when语句就可以这样写:
fun getScore(name:String) = when{
name.startsWith("tom") -> 86
name == "jim"->98
name == "bob"->89
else -> 0
}
现在不管你传入的名字是tom还是tommy,只要是以tom开头的名字,他的分数就是86。
2.3.3 循环语句
kotlin中提供了while循环和for循环,其中while循环不管是在语法还是使用技巧上都和Java中的while循环没有任何区别。
而kotlin中的for循环做了很大幅度的修改。java中最常见的for-i循环被舍弃了,而java中另一种for-each循环则被kotlin进行了大幅度的加强,变成了for-in循环。
区间的表示:val range = 0..10
在kotlin中,上述代码表示创建了一个从0到10的区间,并且两端都是闭区间,这意味着0到10这两个端点都是包含在区间中的。一个简单的for-in循环代码:
for(i in 0..10){
println(i)
} //输出0——10
但是在很多情况下,双端闭区间却不如单段闭区间好用。因此kotlin中可以使用until关键字来创建一个左闭右开的区间,如下所示:val range = 0 until 10
如果想跳过其中一些元素,可以使用step关键字,表示每次循环都会在区间范围内递增2:
for(i in 0 until 10 step 2){
println(i)
} //输出0 2 4 6 8
不过,前面所学的…和until关键字都要求区间的左端必须小于等于区间的右端,也就是这两种关键字创建的都是一个升序的区间。如果想创建一个降序的区间,可以使用downTo关键字,用法如下:
for(i in 10 downTo 1){
println(i)
}
2.4 面向对象编程
kotlin也是面向对象的。一段面向对象编程的代码如下:
class Person {
var name = ""
var age = 0
fun eat(){
println(name + " is eating.he is " + age + " years old.")
}
}
fun main(){
val p = Person() //实例化一个类的方式和java基本类似,只是去掉了new关键字
p.name = "Jack"
p.age = 19
p.eat()
} //输出 Jack is eating.he is 19 years old.
2.4.1 继承与构造函数
我们创建一个student类,想让他继承person类,需要做两件事。
**第一件事,使person类可以被继承。**与Java不同的是,kotlin中任何一个非抽象类都是默认不可以被继承的,相当于Java中给类声明了final关键字。因为类和变量一样,最好都是不可变的,而如果一个类可以被继承的,它无法预知子类会如何实现,因此可能会存在一些未知的风险。如果一个类不是专门为继承而设计的,那么就应该主动将它加上final声明,禁止它可以被继承。
kotlin中的抽象类和Java中并无区别,抽象类本身无法创建实例,一定要由子类去继承他才可以创建实例,因此抽象类必须可以被继承才行。
在Person类的前面加上open关键字就可以使他可被继承:open class Person{ ... }
**第二件事,要让student类继承person类。**在Java中继承的关键字是extends,而在kotlin中,变成了一个冒号,写法如下:class Student:Person(){ ... }
注意到,继承person时,在person类的后面还加了一对括号。它涉及主构造函数、次构造函数等方面的知识,下面简单讲解一下。
任何一个面向对象的编程语言都会有构造函数的概念,kotlin中也有,但是kotlin讲构造函数分成了两种:主构造函数和次构造函数。
主构造函数
主构造函数将会是你最常用的构造函数,每个类默认都会有一个不带参数的主构造函数,当然也可以显示地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面即可。比如下面这种写法:
class Student(val sno:String,val grade:Int) : Person(){ ... }
这里我们将学号和年级这两个字段都放进了主构造函数中,这就表明在对Student类进行实例化的时候,必须传入构造函数中要求的所有参数,比如val student = Student("a123",2)。这样我们就创建了一个Student对象,并指明了改学生的学号是a123,年级是2。另外,由于构造函数中的参数是在创建实例的时候传入的,不像之前的写法那样还得重新赋值,因此我们可以将参数全部声明称val。
如果想在主构造函数中写一些逻辑,可以使用kotlin中init结构体,所有主构造函数中的逻辑都可以写在里面:
class Student(val sno:String,val grade:Int) : Person(){
init{
println("sno is " + sno)
println("grade is " + grade)
}
}
在java中,子类中的构造函数必须调用父类中的构造函数,这个规定在kotlin中也要遵守。在kotlin中使用括号来实现。**子类的主构造函数调用父类中的哪个构造函数,在继承的时候通过括号来指定。**再来看这段代码:
class Student(val sno:String,val grade:Int) : Person(){ ... }
在这里,Person类后面的一对括号表示:Student类的主构造函数在初始化的时候,会调用Person类的无参数构造函数,即使在无参数的情况下,这对括号也不能省略。而如果我们的Person函数的主构造函数写成:open class Person(val name:String,val age:Int){ ... }那么Student类会报错。原因是Person类后面的空括号表示要去调用Person类中无参构造函数,但是Person类现在已经没有无参的构造函数了,所以就提示上述错误。
要想解决这个问题,就需要给Person类的构造函数传入name和age字段,可是Student类中也没有这两个字段呀。很简单,没有就加呗。我们可以在Student类的主构造函数中加上name和age这两个参数,再将这两个参数传给Person类的构造函数,代码如下所示:
class Student(val sno:String,val grade:Int,name:String,age:Int) : Person(name,age){
...
}
注意到,我们在Student类的主构造函数中增加name和age这两个字段时,不能再将它们声明成val,因为在主构造函数中声明成val或者var的参数将自动成为该类的字段,这样就会导致和父类同名的name以及age字段造成冲突。因此,这里的name和age参数前面我们不用加任何关键字,让它的作用域仅限定在主构造函数中即可。
现在就可以通过如下代码来创建一个Student类的实例:
val student = Student("a123",2,"jack",19)
次构造函数
**任何一个类只能有一个主构造函数,但是可以有多个次构造函数。**次构造函数也可以用于实例化一个类,这一点和主构造函数没什么不同,只不过他是有函数体的。
**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类的后面没有显式地定义主构造函数(类名后没有括号),同时又因为定义了次构造函数(用 constructor 关键字),所以现在Student类是没有主构造函数的。那么既然没有主构造函数,继承Person类的时候也就不需要再加上括号了。
另外,由于没有主构造函数,次构造函数只能直接调用父类的构造函数,上述代码也是将this关键字换成了super关键字,这部分就很好理解了。
2.4.2 接口
kotlin中的接口部分与java几乎是完全一致的。接口是用于实现多态编程的重要组成部分,在Java中,任何一个类最多只能继承一个父类,但是却可以实现任意多个接口,kotlin也是如此。
新建一个接口,然后在接口中添加几个相关的函数,注意接口中的函数不要求有函数体。在kotlin中,统一使用冒号,中间用逗号进行分割,来表示继承父类和实现接口。一段接口代码:
class Student(name:String,age: Int) :Person(name,age), Study{
override fun readBooks() {
println(name + " is reading.")
}
override fun doHomeWork() {
println(name + " is doing homework.")
}
}
kotlin还增加了一个额外的功能:允许对戒口中定义的函数进行默认实现。
interface Study{
fun readBooks()
fun doHomeWork(){
println("do homework default implementation.")
}
}
可以看到,我们给接口中的函数加了一个函数体,并在里面打印了一行日志。**如果接口中的一个函数拥有了函数体,这个函数中的内容就是她的默认实现。**现在当一个类去实现Study接口时,就只会强制要求实现readBooks()函数了,而doHomeWork()函数则可以自由选择实现或者不实现,不实现时就会自动使用默认的实现逻辑。
2.4.3 函数的可见性修饰符
Java中有public、private、protected和default(什么都不写)这四种可见性修饰符,kotlin中也有4中,分别是public、private、protected和internal,需要哪种修饰符时,直接定义在fun关键字前面即可。
首先 private 修饰符在两种语言中的作用是一模一样的,都表示只对当前类内部可见。public 修饰符的作用虽然也是一致的,表示对所有类都可见,但是在 Kotlin 中 public 修饰符是默认项,而在 Java 中 default 才是默认项。前面我们定义了那么多的函数,都没有加任何的修饰符,所以它们默认都是 public 的。protected 关键字在 Java 中表示对当前类、子类和同一包路径下的类可见,在 Kotlin 中则表示只对当前类和子类可见。Kotlin 抛弃了 Java 中的 default 可见性(同一包路径下的类可见),引入了一种新的可见性概念,只对同一模块中的类可见,使用的是 internal 修饰符。比如我们开发了一个模块给别人使用,但是有一些函数只允许在模块内部调用,不想暴露给外部,就可以将这些函数声明成 internal。

2.5 数据类与单例类
2.5.1 数据类型
在规范的系统里,数据类很重要,能把服务器或数据库里的数据放到内存中,给编程逻辑提供数据模型支持。像MVC、MVP、MVVM这些架构模式里的“M”,说的就是数据类。
数据类一般得重写equals()、hashCode()、toString()这几个方法。equals()用来判断两个数据类是否一样;hashCode()得和equals()一起重写,不然HashMap、HashSet这些和哈希有关的系统类就没法正常用;toString()能让打印数据类时显示更清楚的信息,不然默认打印的是内存地址,程序员看不懂。
而在kotlin中,实现数据类的方法非常简单,只需要在创建的类中编写如下代码:
data class Cellphone(val brand:String,val price:Double)
只需要一行代码!神奇的地方就在于data这个关键字,当在一个类前面声明了data关键字时,就表明你希望这个类是一个数据类,kotlin会根据主构造函数中的参数帮你将equals()、hashCode()、toString ()等固定且无实际逻辑意义的方法自动生成,从而大大减少了开发的工作量。
在一个类中没有任何代码时,尾部的大括号可以省略!
2.5.2 单例类
在Java中,存在单例模式,它可以用于避免创建重复的对象,比如我们希望某个类在全局最多只能拥有一个实例,这时候就可以使用单例模式。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void singletonTest() {
System.out.println("singletonTest is called.");
}
}
```
设计思路:以单例singleton为例,为了禁止外部创建singleton的实例,我们需要用private关键字将singleton的构造函数私有化,然后给外部提供一个getInstance()静态方法用于获取singleton的实例。在getInstance()方法中,我们判断如果当前缓存的singleton实例为null,就创建一个新的实例,否则直接返回缓存的实例即可,这就是单例模式的工作机制。
在kotlin中,创建一个单例类非常简单,只需要将class关键字改成object关键字即可!
想要调用单例类中的函数也很简单,比较类似于Java中静态方法的调用方式:类名.方法()。这种写法虽然看上去像是静态方法的调用,但其实kotlin在背后自动帮我们创建了一个该类的实例,并且保证全局只会存在一个该实例。
2.6 Lambda编程
集合的函数式API是用来入门lambda编程的绝佳示例,不过在此之前,需要先学习创建集合的方式才行。
2.6.1 集合的创建与遍历
传统的集合有list、set、map这几种最经典的集合类型,下面一次对其进行讲解。
kotlin提供了一个内置的listOf()函数来简化初始化集合的写法:
val list = listOf("apple","orange","banana","pear")
还可以使用for-in循环来遍历这个集合:
fun main(){
val list = listOf("apple","banana","orange","pear")
for(fruit in list){
println(fruit)
}
}
需要注意的是,使用listOf()创建的集合是不可变的(只能进行读取,不能增删改),而创建一个可变的集合需要使用mutableListOf()函数。
fun main(){
val list = mutableListOf("apple","banana","orange","pear")
list.add("watermelon")
for(fruit in list){
println(fruit)
}
//set集合的用法几乎与此一模一样,只是Set集合中不可以存放重复元素,如果存放了多个,只会保留其中一份
val set = setOf("apple","banana","orange","pear")
for(fruit in set){
println(fruit)
}
}
虽然kotlin中,也可以像Java一样使用put()和get()方法来对Map进行添加和读取操作,但是不建议用。更加推荐使用一种类似于数组下标的语法结构,比如向Map中添加一条数据可以这么写:
map["Apple"] = 1
而从Map中读取一条数据可以这么写:
val number = map["Apple"]
与此同时,kotlin提供了一对mapOf()和mutableMapOf()函数来继续简化Map的用法,在mapOf()函数中,可以直接传入初始化的键值对组合来完成对Map的创建:
val map = mapOf("apple" to 1,"banana" to 2,"orange" to 3)
这里键值对组合看上去好像是使用to这个关键字来进行关联的,但其实to并不是关键字,而是一个infix函数。
fun main(){
val map = mapOf("apple" to 1,"banana" to 2,"orange" to 3)
for((fruit,number) in map){
print("fruit is " + fruit + ",number is " + number)
}
}
2.6.2 集合的函数式API
**lambda定义:就是一段可以作为参数传递的代码。**例如:val maxLengthFruit = list.maxBy{it.length}
lambda表达式的语法结构:{参数名1:参数类型,参数名2:参数类型 -> 函数体}
首先最外层是一个大括号,如果有参数传入到lambda表达式中的话,我们还需要声明参数列表。参数列表的结尾使用一个->符号,表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码(虽然不建议编写太长的代码,会影响代码的可读性),并且最后一行代码会自动作为lambda表达式的返回值。
val list = listOf("apple","orange","banana","pear")
val lambda = {fruit:String -> fruit.length}
val maxLengthFruit = list.maxBy(lambda) //可以看到,maxBy函数实质上就是接收了一个lambda参数而已
//简化一下
...
val maxLengthFruit = list.maxBy({fruit:String -> fruit.length})
//kotlin规定,当lambda参数是函数的最后一个参数时,可以将lambda表达式移到函数括号的外面
val maxLengthFruit = list.maxBy(){fruit:String -> fruit.length}
//如果lambda参数是函数唯一一个参数的话,还可以将括号省略
val maxLengthFruit = list.maxBy{fruit:String -> fruit.length}
//由于出色的类型推导机制,lambda表达式中的参数列表大多数情况下不必声明参数类型
val maxLengthFruit = list.maxBy{fruit -> fruit.length}
//如果lambda表达式中的参数列表只有一个参数,也不必声明参数名,而是可以直接用it关键字来替代
val maxLengthFruit = list.maxBy{it.length}
集合中的map函数是最常用的一种函数式API,它用于将集合中的每个元素都映射成另外的值,映射的规则卸载lambda表达式中,最终生成一个新的集合。比如,这里我们希望让所有的水果名都变成大写模式。
fun main(){
val list = listOf("apple","orange","banana","pear")
val newList = list.map{it.uppercase()}
for(fruit in newList){
println(fruit)
}
}
- 另一个比较常用的函数式API——filter函数。用于过滤集合中的数据。如果想实现,保留5个字母以内的水果,那么上述代码可以改为:
val newList = list.filter{it.length<=5}.map{it.uppercase()} - 以及另外两个比较常用的函数式API——any和all函数。其中any函数用于判断集合中是否至少存在一个元素满足指定条件,all函数用于判断集合中是否所有元素都满足指定条件。代码如下:
fun main(){
val list = listOf("apple","orange","banana","pear")
val anyResult = list.any{it.length <= 5}
val allResult = list.all{it.length <= 5}
println("anyResult is " + anyResult + ",allResult is " + allResult) //分别输出true和false
}
2.6.3 Java函数式API的使用
如果我们在kotlin代码中调用了一个Java方法,并且该方法接收一个Java单抽象方法接口参数(是指接口中只有一个待实现方法),就可以使用函数式API。
例如Java中的最为常见的单抽象方法接口——Runnable接口。这个接口中只有一个待实现的run()方法。
因此,对于任何一个Java方法,只要他接收Runnable参数,就可以使用函数式API。有很多Java方法接收了runnable参数,这里以java的线程类Thread来学习一下。
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 中匿名类的写法和 Java 有一点区别,由于 Kotlin 完全舍弃了 new 关键字,因此创建匿名类实例的时候就不能再使用 new 了,而是改用了 object 关键字。这种写法虽然算不上复杂,但是相比于 Java 的匿名类写法,并没有什么简化之处。
但是别忘了,目前 Thread 类的构造方法是符合 Java 函数式 API 的使用条件的,下面我们就看看如何对代码进行精简,如下所示:
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()
后面有很多需要惊颤使用的java函数式API,android SDK还是使用java语言编写的,当我们在kotlin中调用这些SDK接口时,很可能会用到这种写法。
2.7 空指针检查
2.7.1 可空类型系统
kotlin利用编译时判空检查的机制几乎杜绝的空指针异常。虽然编译时判空检查的机制有时候会导致代码变得比较难写,但是kotlin也提供了一系列的辅助工具,让我们轻松处理各种判空情况。
可为空的类型系统,就是在类名的后面加上一个问号。比如Int?。代码示例如下:
fun doStdy(study:Study?){
if(study!=null){
study.readBooks()
study.doHomeWork()
}
}
2.7.2 判空辅助工具
如果每处代码都用if判断,会导致代码很啰嗦,而且if判断语句处理不了全局变量的判空问题。因此kotlin提供了一系列工具。
首先是最常用的?.操作符。表示当对象不为空时正常调用,当对象为空时什么都不做。上面的代码可以简化为:
fun doStudy(study:Study?){
study?.readBooks()
study?.doHomeWork()
}
其次是?:操作符。这个操作符的左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。例如:var c = a ?: b。
二者结合可以写出很简洁的代码:fun getTextLength(text:String?) = text?.length ?: 0
Kotlin 的空指针检查机制并非总是智能。有时开发者从逻辑上已处理空指针异常,但编译器不知情,会导致编译失败。例如下面的代码:
var content: String? = "hello"
fun main() {
if (content != null) {
printUpperCase()
}
}
fun printUpperCase() {
val upperCase = content.toUpperCase()
println(upperCase)
}
问题:printUpperCase() 函数不知道外部已对 content 做非空检查,调用 toUpperCase() 方法时,编译器认为存在空指针风险,无法编译通过。
若要强行通过编译,可使用非空断言工具,在对象后加 !!,修改后的 printUpperCase() 函数如下:
fun printUpperCase() {
val upperCase = content!!.toUpperCase()
println(upperCase)
}
告诉 Kotlin,开发者非常确信此处对象不会为空,无需编译器做空指针检查。若对象实际为空,运行时会直接抛出空指针异常,后果由开发者承担。这是有风险的写法。使用非空断言工具时,需谨慎思考是否有更好的实现方式,因为开发者自认为对象不为空时,可能恰恰是潜在空指针异常发生的时候。
最后一个比较与众不同的辅助工具——let。他是一个函数,这个函数提供了函数式API的编程接口,并将原始调用对象作为参数传递到lambda表达式中。示例代码如下:
obj.let{obj2 ->
//编写具体的业务逻辑
}
可以看到,这里调用了obj对象的let函数,然后lambda表达式中的代码就会立即执行,并且这个obj对象本身还会作为参数传递到lambda表达式中。为了防止变量重名,可以改一下参数名。
使用?.和let来对代码进行优化,例如:
fun doStudy(study:Study?){
study?.let{ stu->
stu.readBooks()
stu.doHomeWork()
}
}
//简化,当lambda表达式的参数列表中只有一个参数时,可以不用声明参数名,直接用it关键字来代替
fun doStudy(study:Study?){
study?.let{
it.readBooks()
it.doHomeWork()
}
}
2.8 kotlin中的小魔术
2.8.1 字符串内嵌表达式
语法规则:hello,${obj.name}.nice to meet you!
如果表达式中只有一个变量,还可以将大括号省略。
2.8.2 函数的参数默认值
在定义函数的时候给人以参数设定一个默认值,那么当调用此函数时就不会强制要求调用方为此参数传值。例如:
fun printParams(num:Int,str:String = "hello"){
println("num is $num,str is $str")
}
同时,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) {
}
}
- 代码中有一个主构造函数和两个次构造函数。
- 次构造函数的作用:提供用更少参数实例化
Student类的方式。无参次构造函数调用两参数次构造函数,并给参数赋初始值;两参数次构造函数调用四参数主构造函数,给缺失的两个参数也赋初始值。
而这种通过次构造函数实现的逻辑,在 Kotlin 中可通过给主构造函数参数设定默认值来更简洁地实现,代码如下:
class Student(val sno: String = "", val grade: Int = 0, name: String = "", age: Int = 0) : Person(name, age) {
}
给主构造函数的每个参数都设定默认值后,可使用任何传参组合的方式对 Student 类进行实例化,自然也包含了之前两种次构造函数的使用场景。给函数(构造函数)设定参数默认值这一技巧,能极大简化类的实例化逻辑,在很大程度上替代次构造函数的作用。
2195

被折叠的 条评论
为什么被折叠?



