kotlin
变量
val
val(value的简写)用来声明一个不可变的变量,这种变量在初始赋值之后就再也不能重新赋 值,对应Java中的final变量。
var
var(variable的简写)用来声明一个可变的变量,这种变量在初始赋值之后仍然可以再被重新 赋值,对应Java中的非final变量。
fun main(){ |
显示的进行变量声明
val a: Int = 10 |
函数
语法规则
fun methodName(param1: Int, param2: Int): Int{ |
- 函数名后面紧跟着一对括号,里面可以声明该函数接收什么参数,参数的数量可以是任意多
个,例如上述示例就表示该函数接收两个Int类型的参数。参数的声明格式是“参数名: 参数类
型”,其中参数名也是可以随便定义的,这一点和函数名类似。如果不想接收任何参数,那么写
一对空括号就可以了 - 参数括号后面的那部分是可选的,用于声明该函数会返回什么类型的数据,上述示例就表示该
函数会返回一个Int类型的数据。如果你的函数不需要返回任何数据,这部分可以直接不写。
例子
package com.example.helloworld |
语法糖
当一个函数中只有一行代码时,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{ |
Kotlin中的if语句相比于Java有一个额外的功能,它是可以有返回值的,返回值就是if语句每 一个条件中最后一行代码的返回值。因此,上述代码就可以简化成如下形式
fun largerNumber(num1:Int,num2:Int):Int{ |
仔细观察上述代码,你会发现value其实也是一个多余的变量,我们可以直接将if语句返回, 这样代码将会变得更加精简,如下所示
fun largerNumber(num1:Int,num2:Int){ |
使用语法糖,使得代码更加精简
fun largerNumber(num1:Int,num2:Int) = if(num1>num2){ |
when条件语句
编写一个查询考试成绩的功能,输入一个学生的姓名,返回该学生考试的分数。我 们先用上一小节学习的if语句来实现这个功能
fun getScore(name:String) = if(name=="Tom"){ |
虽然上述代码确实可以实现我们想要的功能,但是写了这么多的if和else,你有没有觉得代码 很冗余?没错,当你的判断条件非常多的时候,就是应该考虑使用when语句的时候,现在我们 将代码改成如下写法
fun getScore(name:String) = when(name){ |
when语句允许传入一个任意类型的参数,然后可以在when的结构体中定义一系列的条件,格式 是
匹配值 -> {执行逻辑} |
当你的执行逻辑只有一行代码时,{ }可以省略。这样再来看上述代码就很好理解了吧
除了精确匹配之外,when语句还允许进行类型匹配。什么是类型匹配呢?这里我再举个例子
定义一个checkNumber()函数,如下所示
fun checkNumber(num:Number){ |
上述代码中,is关键字就是类型匹配的核心,它相当于Java中的instanceof关键字。由于 checkNumber()函数接收一个Number类型的参数,这是Kotlin内置的一个抽象类,像Int、 Long、Float、Double等与数字相关的类都是它的子类,所以这里就可以使用类型匹配来判断传入的参数到底属于什么类型,如果是Int型或Double型,就将该类型打印出来,否则就打 印不支持该参数的类型。
when语句的基本用法就是这些,但其实when语句还有一种不带参数的用法,虽然这种用法可能 不太常用,但有的时候却能发挥很强的扩展性
拿刚才的getScore()函数举例,如果我们不在when语句中传入参数的话,还可以这么写
fun getScore(name:String) = when{ |
可以看到,这种用法是将判断的表达式完整地写在when的结构体当中。注意,Kotlin中判断字 符串或对象是否相等可以直接使用==关键字,而不用像Java那样调用equals()方法。可能你 会觉得这种无参数的when语句写起来比较冗余,但有些场景必须使用这种写法才能实现。举个 例子,假设所有名字以Tom开头的人,他的分数都是86分,这种场景如果用带参数的when语句 来写就无法实现,而使用不带参数的when语句就可以这样写:
fun getScore(name:String) = when{ |
循环语句
在开始学习for-in循环之前,还得先向你普及一个区间的概念,因为这也是Java中没有的东 西。我们可以使用如下Kotlin代码来表示一个区间
val range=0..10 |
在开始学习for-in循环之前,还得先向你普及一个区间的概念,因为这也是Java中没有的东 西。我们可以使用如下Kotlin代码来表示一个区间
有了区间之后,我们就可以通过for-in循环来遍历这个区间,比如在main()函数中编写如下 代码:
fun main(){ |
Kotlin中可以使用until关键字来创建一个左闭右开的区间,如 下所示:
val range=0 until 10 |
默认情况下,for-in循环每次执行循环时会在区间范围内递增1,相当于Java for-i循环中 i++的效果,而如果你想跳过其中的一些元素,可以使用step关键字:
fun main(){ |
上述代码表示在遍历[0, 10)这个区间的时候,每次执行循环都会在区间范围内递增2,相当于 for-i循环中i = i + 2的效果。
不过,前面我们所学习的..和until关键字都要求区间的左端必须小于等于区间的右端,也就 是这两种关键字创建的都是一个升序的区间。如果你想创建一个降序的区间,可以使用downTo 关键字,用法如下:
fun main(){ |
面向对象
类与对象
package com.example.helloworld |
继承与构造函数
Student类
package com.example.helloworld |
第一件事,使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(){ |
任何一个面向对象的编程语言都会有构造函数的概念,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(){ |
果我们将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和age参数,然后它又通过this关键字调用了主构造函数,并将sno和 grade这两个参数赋值成初始值;第二个次构造函数不接收任何参数,它通过this关键字调用 了我们刚才定义的第一个次构造函数,并将name和age参数也赋值成初始值,由于第二个次构 造函数间接调用了主构造函数,因此这仍然是合法的。
那么现在我们就拥有了3种方式来对Student类进行实体化,分别是通过不带参数的构造函数、 通过带两个参数的构造函数和通过带4个参数的构造函数,对应代码如下所示
val student1 = Student() |
那么接下来我们就再来看一种非常特殊的情况:类中只有次构造函数,没有主构造函数。这种 情况真的十分少见,但在Kotlin中是允许的。当一个类没有显式地定义主构造函数且定义了次构 造函数时,它就是没有主构造函数的。我们结合代码来看一下
class Student:Person{ |
注意这里的代码变化,首先Student类的后面没有显式地定义主构造函数,同时又因为定义了 次构造函数,所以现在Student类是没有主构造函数的。那么既然没有主构造函数,继承 Person类的时候也就不需要再加上括号了。其实原因就是这么简单,只是很多人在刚开始学习 Kotlin的时候没能理解这对括号的意义和规则,因此总感觉继承的写法有时候要加上括号,有时 候又不要加,搞得晕头转向的,而在你真正理解了规则之后,就会发现其实还是很好懂的
接口
接口是用于实现多态编程的重要组成部分。我们都知道,Java是单继承结构的语言,任何一个 类最多只能继承一个父类,但是却可以实现任意多个接口,Kotlin也是如此
Study接口
interface Study{ |
接下来就可以让Student类去实现Study接口了,这里我将Student类原有的代码调整了一 下,以突出继承父类和实现接口的区别
class Student(name:String,age:Int):Person(name,age),Study{ |
现在我们可以在main()函数中编写如下代码来调用这两个接口中的函数
fun main(){ |
Java和Kotlin函数可见性修饰符对照表
Lambda编程
集合的创建和遍历
现在我们提出一个需求,创建一个包含许多水果名称的集合。如果是在Java中你会怎么实现? 可能你首先会创建一个ArrayList的实例,然后将水果的名称一个个添加到集合中。当然,在 Kotlin中也可以这么做
val list = ArrayList<String>() |
但是这种初始化集合的方式比较烦琐,为此Kotlin专门提供了一个内置的listOf()函数来简化 初始化集合的写法,如下所示
val list = listOf("Apple","Banana","Orange","Pear","Grape") |
可以看到,这里仅用一行代码就完成了集合的初始化操作
还记得我们在学习循环语句时提到过的吗?for-in循环不仅可以用来遍历区间,还可以用来遍 历集合。现在我们就尝试一下使用for-in循环来遍历这个水果集合,在main()函数中编写如 下代码:
fun main(){ |
不过需要注意的是,listOf()函数创建的是一个不可变的集合。你也许不太能理解什么叫作不 可变的集合,因为在Java中这个概念不太常见。不可变的集合指的就是该集合只能用于读取, 我们无法对集合进行添加、修改或删除操作
至于这么设计的理由,和val关键字、类默认不可继承的设计初衷是类似的,可见Kotlin在不可 变性方面控制得极其严格。那如果我们确实需要创建一个可变的集合呢?也很简单,使用 mutableListOf()函数就可以了,示例如下
fun main(){ |
前面我们介绍的都是List集合的用法,实际上Set集合的用法几乎与此一模一样,只是将创建 集合的方式换成了setOf()和mutableSetOf()函数而已。大致代码如下
fun main(){ |
最后再来看一下Map集合的用法。Map是一种键值对形式的数据结构,因此在用法上和List、 Set集合有较大的不同。传统的Map用法是先创建一个HashMap的实例,然后将一个个键值对数 据添加到Map中。比如这里我们给每种水果设置一个对应的编号,就可以这样写
val map = HashMap<String,Int>() |
我之所以先用这种写法,是因为这种写法和Java语法是最相似的,因此可能最好理解。但其实 在Kotlin中并不建议使用put()和get()方法来对Map进行添加和读取数据操作,而是更加推荐 使用一种类似于数组下标的语法结构,比如向Map中添加一条数据就可以这么写
map["Apple"]=1 |
而从Map中读取一条数据就可以这么写:
val number = map["Apple"] |
因此,上述代码经过优化过后就可以变成如下形式
val map = HashMap<String,Int>() |
当然,这仍然不是最简便的写法,因为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(){ |
下Lambda表达式的语法结构
{参数名1:参数类型,参数名2:参数类型 ->函数体} |
这是Lambda表达式最完整的语法结构定义。首先最外层是一对大括号,如果有参数传入到 Lambda表达式中的话,我们还需要声明参数列表,参数列表的结尾使用一个->符号,表示参 数列表的结束以及函数体的开始,函数体中可以编写任意行代码(虽然不建议编写太长的代 码),并且最后一行代码会自动作为Lambda表达式的返回值
理解了maxBy函数的工作原理之后,我们就可以开始套用刚才学习的Lambda表达式的语法结 构,并将它传入到maxBy函数中了,如下所示
val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon") |
下面我们就开始对这段代 码一步步进行简化。
首先,我们不需要专门定义一个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(){ |
接下来我们继续学习两个比较常用的函数式API——any和all函数。其中any函数用于判断集 合中是否至少存在一个元素满足指定条件,all函数用于判断集合中是否所有元素都满足指定条 件。由于这两个函数都很好理解,我们就直接通过代码示例学习了
fun main(){ |
Java函数式API的使用
这里我们就通过Java的线程类Thread来学习一下。
new Thread(new Runnable(){ |
而如果直接将这段代码翻译成Kotlin版本,写法将如下所示:
Thread(object:Runnale{ |
Kotlin中匿名类的写法和Java有一点区别,由于Kotlin完全舍弃了new关键字,因此创建匿名类 实例的时候就不能再使用new了,而是改用了object关键字。这种写法虽然算不上复杂,但是 相比于Java的匿名类写法,并没有什么简化之处
但是别忘了,目前Thread类的构造方法是符合Java函数式API的使用条件的,下面我们就看看 如何对代码进行精简,如下所示:
Thread(Runnable{ |
另外,如果一个Java方法的参数列表中有且仅有一个Java单抽象方法接口参数,我们还可以将 接口名进行省略,这样代码就变得更加精简了:
Thread({println("Thread is running")}).start() |
空指针检查
Kotlin将空指针异常的检查提前到了编译时期,如果我们的程序存在空指针异常的风 险,那么在编译的时候会直接报错,修正之后才能成功运行,这样就可以保证程序在运行时期 不会出现空指针异常了
可为空的类型系统
那么可为空的类型系统是什么样的呢?很简单,就是在类名的后面加上一个问号。比如,Int表 示不可为空的整型,而Int?就表示可为空的整型;String表示不可为空的字符串,而 String?就表示可为空的字符串
判空辅助工具
首先学习最常用的?.操作符。这个操作符的作用非常好理解,就是当对象不为空时正常调用相 应的方法,当对象为空时则什么都不做。比如以下的判空处理代码:
if(a!=null){ |
这段代码使用?.操作符就可以简化成:
a?.doSomething() |
了解了?.操作符的作用,下面我们来看一下如何使用这个操作符对doStudy()函数进行优化, 代码如下所示
fun doStudy(study:Study?){ |
下面我们再来学习另外一个非常常用的?:操作符。这个操作符的左右两边都接收一个表达式, 如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。观察 如下代码:
val c = if(a!=null){ |
这段代码的逻辑使用?:操作符就可以简化成:
val c = a?:b |
使用非空断言工具,写法是在对象的后面加 上!!
fun printUpperCase(){ |
最后我们再来学习一个比较与众不同的辅助工具——let。let既不是操作符,也不是什么关键 字,而是一个函数。这个函数提供了函数式API的编程接口,并将原始调用对象作为参数传递到 Lambda表达式中。示例代码如下
obj.let{obj2> |
Kotlin中的小魔术
字符串内嵌表达式
首先来看一下Kotlin中字符串内嵌表达式的语法规则
"hello, ${obj.name}. nice to meet you!" |
println("Cellphone(brand=" + brand + ", price=" + price + ")") |
函数参数的默认值
fun printParams(num: Int, str: String = "hello") { |
Kotlin提供了另外一种神奇的机制,就是可以通过键值对的方式来传参,从而不 必像传统写法那样按照参数定义的顺序来传参。比如调用printParams()函数,我们还可以这 样写
fun printParams(num: Int = 100, str: String) { |