变量

val

val(value的简写)用来声明一个不可变的变量,这种变量在初始赋值之后就再也不能重新赋 值,对应Java中的final变量。

var

var(variable的简写)用来声明一个可变的变量,这种变量在初始赋值之后仍然可以再被重新 赋值,对应Java中的非final变量。

fun main(){
val a=10
println("a = " + a)
}

显示的进行变量声明

val a: Int = 10

函数

语法规则

fun methodName(param1: Int, param2: Int): Int{
return 0
}
  • 函数名后面紧跟着一对括号,里面可以声明该函数接收什么参数,参数的数量可以是任意多
    个,例如上述示例就表示该函数接收两个Int类型的参数。参数的声明格式是“参数名: 参数类
    型”,其中参数名也是可以随便定义的,这一点和函数名类似。如果不想接收任何参数,那么写
    一对空括号就可以了
  • 参数括号后面的那部分是可选的,用于声明该函数会返回什么类型的数据,上述示例就表示该
    函数会返回一个Int类型的数据。如果你的函数不需要返回任何数据,这部分可以直接不写。

例子

package com.example.helloworld
import kotlin.math.max
fun main(){
val num1:Int = 10
val num2:Int = 100
val value = largerNumber(num1,num2)
println("larger number is " + value)
}
fun largerNumber(num1:Int,num2:Int):Int{
return max(num1,num2)
}

语法糖

当一个函数中只有一行代码时,Kotlin允许我们不必编写函数体,可以直接将唯一的一行代码写 在函数定义的尾部,中间用等号连接即可。

fun largerNumber(num1:Int,num2:Int):Int =max(num1,num2)

使用这种语法,return关键字也可以省略了,等号足以表达返回值的意思。另外,还记得 Kotlin出色的类型推导机制吗?在这里它也可以发挥重要的作用。由于max()函数返回的是一个 Int值,而我们在largerNumber()函数的尾部又使用等号连接了max()函数,因此Kotlin可 以推导出largerNumber()函数返回的必然也是一个Int值,这样就不用再显式地声明返回值 类型了,代码可以进一步简化成如下形式:

fun largerNumber(num1:Int,num2:Int)=max(num1,num2)

程序的逻辑控制

if条件语句

fun largerNumber(num1:Int,num2:Int):Int{
var value = 0
if(num1>num2){
value=num1
}else{
value=num2
}
return value
}

Kotlin中的if语句相比于Java有一个额外的功能,它是可以有返回值的,返回值就是if语句每 一个条件中最后一行代码的返回值。因此,上述代码就可以简化成如下形式

fun largerNumber(num1:Int,num2:Int):Int{
var value = if(num1>num2){
num1
}else{
num2
}
return value
}

仔细观察上述代码,你会发现value其实也是一个多余的变量,我们可以直接将if语句返回, 这样代码将会变得更加精简,如下所示

fun largerNumber(num1:Int,num2:Int){
return if(num1>num2){
num1
}else{
num2;
}
}

使用语法糖,使得代码更加精简

fun largerNumber(num1:Int,num2:Int) = if(num1>num2){
num1
}else{
num2
}

when条件语句

编写一个查询考试成绩的功能,输入一个学生的姓名,返回该学生考试的分数。我 们先用上一小节学习的if语句来实现这个功能

fun getScore(name:String) = if(name=="Tom"){
86
}else if(name=="Jim"){
77
}else if(name=="Jack"){
95
}else if(name=="Lily"){
100
}else{
0
}

虽然上述代码确实可以实现我们想要的功能,但是写了这么多的if和else,你有没有觉得代码 很冗余?没错,当你的判断条件非常多的时候,就是应该考虑使用when语句的时候,现在我们 将代码改成如下写法

fun getScore(name:String) = when(name){
"Tom"->86
"Jim"->77
"Jack"->95
"Lily"->100
else->0
}

when语句允许传入一个任意类型的参数,然后可以在when的结构体中定义一系列的条件,格式 是

匹配值 -> {执行逻辑}

当你的执行逻辑只有一行代码时,{ }可以省略。这样再来看上述代码就很好理解了吧

除了精确匹配之外,when语句还允许进行类型匹配。什么是类型匹配呢?这里我再举个例子

定义一个checkNumber()函数,如下所示

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语句的基本用法就是这些,但其实when语句还有一种不带参数的用法,虽然这种用法可能 不太常用,但有的时候却能发挥很强的扩展性

拿刚才的getScore()函数举例,如果我们不在when语句中传入参数的话,还可以这么写

fun getScore(name:String) = when{
name=="Tom"->86
name=="Jim"->77
name=="Jack"->95
name=="Lily"->100
else->0
}

可以看到,这种用法是将判断的表达式完整地写在when的结构体当中。注意,Kotlin中判断字 符串或对象是否相等可以直接使用==关键字,而不用像Java那样调用equals()方法。可能你 会觉得这种无参数的when语句写起来比较冗余,但有些场景必须使用这种写法才能实现。举个 例子,假设所有名字以Tom开头的人,他的分数都是86分,这种场景如果用带参数的when语句 来写就无法实现,而使用不带参数的when语句就可以这样写:

fun getScore(name:String) = when{
name.startWith("Tom")->86
name=="Jim"->77
name=="Jack"->95
name=="Lily"->100
else->0
}

循环语句

在开始学习for-in循环之前,还得先向你普及一个区间的概念,因为这也是Java中没有的东 西。我们可以使用如下Kotlin代码来表示一个区间

val range=0..10

在开始学习for-in循环之前,还得先向你普及一个区间的概念,因为这也是Java中没有的东 西。我们可以使用如下Kotlin代码来表示一个区间

有了区间之后,我们就可以通过for-in循环来遍历这个区间,比如在main()函数中编写如下 代码:

fun main(){
for(i in 0..10){
println(i)
}
}

Kotlin中可以使用until关键字来创建一个左闭右开的区间,如 下所示:

val range=0 until 10

默认情况下,for-in循环每次执行循环时会在区间范围内递增1,相当于Java for-i循环中 i++的效果,而如果你想跳过其中的一些元素,可以使用step关键字:

fun main(){
for(i in 0 until 10 step 2){
println(i)
}
}

上述代码表示在遍历[0, 10)这个区间的时候,每次执行循环都会在区间范围内递增2,相当于 for-i循环中i = i + 2的效果。

不过,前面我们所学习的..和until关键字都要求区间的左端必须小于等于区间的右端,也就 是这两种关键字创建的都是一个升序的区间。如果你想创建一个降序的区间,可以使用downTo 关键字,用法如下:

fun main(){
for(i in 10 downTo 1){
println(i)
}
}

面向对象

类与对象

package com.example.helloworld

class Person {
var name = ""
var age = 0
fun eat(){
println(name+" is eating. He is "+ age +"years old.")
}
}
fun main(){
val p=Person()
p.name="Jack"
p.age=19
p.eat()
}

继承与构造函数

Student类

package com.example.helloworld

class Student {
var sno=""
var grade = 0
}

第一件事,使Person类可以被继承。可能很多人会觉得奇怪,尤其是有Java编程经验的人。一 个类本身不就是可以被继承的吗?为什么还要使Person类可以被继承呢?这就是Kotlin不同的 地方,在Kotlin中任何一个非抽象类默认都是不可以被继承的,相当于Java中给类声明了final 关键字。之所以这么设计,其实和val关键字的原因是差不多的,因为类和变量一样,最好都是 不可变的,而一个类允许被继承的话,它无法预知子类会如何实现,因此可能就会存在一些未 知的风险。Effective Java这本书中明确提到,如果一个类不是专门为继承而设计的,那么就应 该主动将它加上final声明,禁止它可以被继承

很明显,Kotlin在设计的时候遵循了这条编程规范,默认所有非抽象类都是不可以被继承的。之 所以这里一直在说非抽象类,是因为抽象类本身是无法创建实例的,一定要由子类去继承它才 能创建实例,因此抽象类必须可以被继承才行,要不然就没有意义了。由于Kotlin中的抽象类和 Java中并无区别,这里我就不再多讲了。 既然现在Person类是无法被继承的,我们得让它可以被继承才行,方法也很简单,在Person 类的前面加上open关键字就可以了,如下所示

open class Person{

}

加上open关键字之后,我们就是在主动告诉Kotlin编译器,Person这个类是专门为继承而设计 的,这样Person类就允许被继承了

第二件事,要让Student类继承Person类。在Java中继承的关键字是extends,而在Kotlin 中变成了一个冒号,写法如下

class Student : Person(){
var sno = ""
var grade = 0
}

任何一个面向对象的编程语言都会有构造函数的概念,Kotlin中也有,但是Kotlin将构造函数分 成了两种:主构造函数和次构造函数

主构造函数将会是你最常用的构造函数,每个类默认都会有一个不带参数的主构造函数,当然 你也可以显式地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面即 可。比如下面这种写法:

calss Student(val sno:String,val grade:Int):Person(){

}

主构造函数将会是你最常用的构造函数,每个类默认都会有一个不带参数的主构造函数,当然 你也可以显式地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面即 可。比如下面这种写法:

val student = Student("a123",5)

你可能会问,主构造函数没有函数体,如果我想在主构造函数中编写一些逻辑,该怎么办呢? Kotlin给我们提供了一个init结构体,所有主构造函数中的逻辑都可以写在里面:

class Student(val sno:String,val grade:Int):Person(){
init{
println("sno is "+sno)
println("grade is "+grade)
}
}

果我们将Person改造一下,将姓名和年龄都放到主构造函数当中,如下所示

open class Person(val name:String,val age:Int){

}

我们可以在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",5,"Jack",19)

任何一个类只能有一个主构造函数,但是可以有多个次构造函数。次构造函数也可 以用于实例化一个类,这一点和主构造函数没有什么不同,只不过它是有函数体的。

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类的时候也就不需要再加上括号了。其实原因就是这么简单,只是很多人在刚开始学习 Kotlin的时候没能理解这对括号的意义和规则,因此总感觉继承的写法有时候要加上括号,有时 候又不要加,搞得晕头转向的,而在你真正理解了规则之后,就会发现其实还是很好懂的

接口

接口是用于实现多态编程的重要组成部分。我们都知道,Java是单继承结构的语言,任何一个 类最多只能继承一个父类,但是却可以实现任意多个接口,Kotlin也是如此

Study接口

interface Study{
fun readBooks()
fun doHomework()
}

接下来就可以让Student类去实现Study接口了,这里我将Student类原有的代码调整了一 下,以突出继承父类和实现接口的区别

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

现在我们可以在main()函数中编写如下代码来调用这两个接口中的函数

fun main(){
val student=Studnet("Jack",19)
doStudy(student)
}
fun doStudy(study:Study){
study.redBooks()
study.doHomework()
}

Java和Kotlin函数可见性修饰符对照表

Lambda编程

集合的创建和遍历

现在我们提出一个需求,创建一个包含许多水果名称的集合。如果是在Java中你会怎么实现? 可能你首先会创建一个ArrayList的实例,然后将水果的名称一个个添加到集合中。当然,在 Kotlin中也可以这么做

val list = ArrayList<String>()
list.add("Apple")
list.add("Banana")
list.add("Orange")
list.add("Pear")
list.add("Grape")

但是这种初始化集合的方式比较烦琐,为此Kotlin专门提供了一个内置的listOf()函数来简化 初始化集合的写法,如下所示

val list = listOf("Apple","Banana","Orange","Pear","Grape")

可以看到,这里仅用一行代码就完成了集合的初始化操作

还记得我们在学习循环语句时提到过的吗?for-in循环不仅可以用来遍历区间,还可以用来遍 历集合。现在我们就尝试一下使用for-in循环来遍历这个水果集合,在main()函数中编写如 下代码:

fun main(){
val list = listOf("Apple","Banana","Orange","Pear","Grape")
for(fruit in list){
println("fruit")
}
}

不过需要注意的是,listOf()函数创建的是一个不可变的集合。你也许不太能理解什么叫作不 可变的集合,因为在Java中这个概念不太常见。不可变的集合指的就是该集合只能用于读取, 我们无法对集合进行添加、修改或删除操作

至于这么设计的理由,和val关键字、类默认不可继承的设计初衷是类似的,可见Kotlin在不可 变性方面控制得极其严格。那如果我们确实需要创建一个可变的集合呢?也很简单,使用 mutableListOf()函数就可以了,示例如下

fun main(){
val list = mutableListOf("Apple","Banana","Orange","Pear","Grape")
list.add("Watermelon")
for(fruit in list){
println(fruit)
}
}

前面我们介绍的都是List集合的用法,实际上Set集合的用法几乎与此一模一样,只是将创建 集合的方式换成了setOf()和mutableSetOf()函数而已。大致代码如下

fun main(){
val set = setOf("Apple","Banana","Orange","Pear","Grape")
for(fruit in set){
println(fruit)
}
}

最后再来看一下Map集合的用法。Map是一种键值对形式的数据结构,因此在用法上和List、 Set集合有较大的不同。传统的Map用法是先创建一个HashMap的实例,然后将一个个键值对数 据添加到Map中。比如这里我们给每种水果设置一个对应的编号,就可以这样写

val map = HashMap<String,Int>()
map.put("Apple",1)
map.put("Banana",2)
map.put("Orange",3)
map.put("Pear",4)
map.put("Grape",5)

我之所以先用这种写法,是因为这种写法和Java语法是最相似的,因此可能最好理解。但其实 在Kotlin中并不建议使用put()和get()方法来对Map进行添加和读取数据操作,而是更加推荐 使用一种类似于数组下标的语法结构,比如向Map中添加一条数据就可以这么写

map["Apple"]=1

而从Map中读取一条数据就可以这么写:

val number = map["Apple"]

因此,上述代码经过优化过后就可以变成如下形式

val map = HashMap<String,Int>()
map["Apple"]=1
map["Banana"]=2
map["Orange"]=3
map["Pear"]=4
map["Grape"]=5

当然,这仍然不是最简便的写法,因为Kotlin毫无疑问地提供了一对mapOf()和 mutableMapOf()函数来继续简化Map的用法。在mapOf()函数中,我们可以直接传入初始化 的键值对组合来完成对Map集合的创建:

val map = mapOf("Apple" to 1,"Banana" to 2,"Orange" to 3,"Pear" to 4,"Grape" to 5)

这里的键值对组合看上去好像是使用to这个关键字来进行关联的,但其实to并不是关键字,而 是一个infix函数,我们会在本书第9章的Kotlin课堂中深入探究infix函数的相关内容。

最后再来看一下如何遍历Map集合中的数据吧,其实使用的仍然是for-in循环。在main()函 数中编写如下代码

fun main(){
val map = mapOf("Apple" to 1,"Banana" to 2,"Orange" to 3,"Pear" to 4,"Grape" to 5)
for((fruit,number) in map){
println("fruit is "+fruit+", number is "+number)
}
}

下Lambda表达式的语法结构

{参数名1:参数类型,参数名2:参数类型 ->函数体}

这是Lambda表达式最完整的语法结构定义。首先最外层是一对大括号,如果有参数传入到 Lambda表达式中的话,我们还需要声明参数列表,参数列表的结尾使用一个->符号,表示参 数列表的结束以及函数体的开始,函数体中可以编写任意行代码(虽然不建议编写太长的代 码),并且最后一行代码会自动作为Lambda表达式的返回值

理解了maxBy函数的工作原理之后,我们就可以开始套用刚才学习的Lambda表达式的语法结 构,并将它传入到maxBy函数中了,如下所示

val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
val lambda = {fruit:String->fruit.length}
val maxLengthFruit = list.maxBy(lambda)

下面我们就开始对这段代 码一步步进行简化。

首先,我们不需要专门定义一个lambda变量,而是可以直接将lambda表达式传入maxBy函数 当中,因此第一步简化如下所示

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}

这样代码看起来就变得清爽多了吧?但是我们还可以继续进行简化。由于Kotlin拥有出色的类型 推导机制,Lambda表达式中的参数列表其实在大多数情况下不必声明参数类型,因此代码可 以进一步简化成

val maxLengthFruit = list.maxBy{fruit->fruit.length}

最后,当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it 关键字来代替,那么代码就变成了

val maxLengthFrut = list.maxBy{it.length}

map函数的功能非常强大,它可以按照我们的需求对集合中的元素进行任意的映射转换,上面只 是一个简单的示例而已。除此之外,你还可以将水果名全部转换成小写,或者是只取单词的首 字母,甚至是转换成单词长度这样一个数字集合,只要在Lambda表示式中编写你需要的逻辑 即可。

接下来我们再来学习另外一个比较常用的函数式API——filter函数。顾名思义,filter函数 是用来过滤集合中的数据的,它可以单独使用,也可以配合刚才的map函数一起使用。 比如我们只想保留5个字母以内的水果,就可以借助filter函数来实现,代码如下所示

fun main(){
val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
val newList = list.filter{it.length<=5}
.map{it.toUpperCase()}
for(fruit in newList){
println(fruit)
}
}

接下来我们继续学习两个比较常用的函数式API——any和all函数。其中any函数用于判断集 合中是否至少存在一个元素满足指定条件,all函数用于判断集合中是否所有元素都满足指定条 件。由于这两个函数都很好理解,我们就直接通过代码示例学习了

fun main(){
val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
val anyResult = list.any{it.length<=5}
val allResult = lsit.all{it.length<=5}
println("anyResult is "+anyResult +", allResult is "+allResult)
}

Java函数式API的使用

这里我们就通过Java的线程类Thread来学习一下。

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

而如果直接将这段代码翻译成Kotlin版本,写法将如下所示:

Thread(object:Runnale{
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()

另外,如果一个Java方法的参数列表中有且仅有一个Java单抽象方法接口参数,我们还可以将 接口名进行省略,这样代码就变得更加精简了:

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

空指针检查

Kotlin将空指针异常的检查提前到了编译时期,如果我们的程序存在空指针异常的风 险,那么在编译的时候会直接报错,修正之后才能成功运行,这样就可以保证程序在运行时期 不会出现空指针异常了

可为空的类型系统

那么可为空的类型系统是什么样的呢?很简单,就是在类名的后面加上一个问号。比如,Int表 示不可为空的整型,而Int?就表示可为空的整型;String表示不可为空的字符串,而 String?就表示可为空的字符串

判空辅助工具

首先学习最常用的?.操作符。这个操作符的作用非常好理解,就是当对象不为空时正常调用相 应的方法,当对象为空时则什么都不做。比如以下的判空处理代码:

if(a!=null){
a.doSomething()
}

这段代码使用?.操作符就可以简化成:

a?.doSomething()

了解了?.操作符的作用,下面我们来看一下如何使用这个操作符对doStudy()函数进行优化, 代码如下所示

fun doStudy(study:Study?){
study?.readBooks()
study?.doHomework()
}

下面我们再来学习另外一个非常常用的?:操作符。这个操作符的左右两边都接收一个表达式, 如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。观察 如下代码:

val c = if(a!=null){
a
}else{
b
}

这段代码的逻辑使用?:操作符就可以简化成:

val c = a?:b

使用非空断言工具,写法是在对象的后面加 上!!

fun printUpperCase(){
vla upperCase = content!!.toUpperCase()
println(upperCase)
}

最后我们再来学习一个比较与众不同的辅助工具——let。let既不是操作符,也不是什么关键 字,而是一个函数。这个函数提供了函数式API的编程接口,并将原始调用对象作为参数传递到 Lambda表达式中。示例代码如下

obj.let{obj2>
//编写具体的业务逻辑
}

Kotlin中的小魔术

字符串内嵌表达式

首先来看一下Kotlin中字符串内嵌表达式的语法规则

"hello, ${obj.name}. nice to meet you!"
println("Cellphone(brand=" + brand + ", price=" + price + ")") 
println("Cellphone(brand=$brand, price=$price)")

函数参数的默认值

fun printParams(num: Int, str: String = "hello") { 
println("num is $num , str is $str")
}
fun main() {
printParams(123)
}

Kotlin提供了另外一种神奇的机制,就是可以通过键值对的方式来传参,从而不 必像传统写法那样按照参数定义的顺序来传参。比如调用printParams()函数,我们还可以这 样写

fun printParams(num: Int = 100, str: String) { 
println("num is $num , str is $str")
}
fun main() {
printParams(str = "world")
}