早就听说Kotlin简洁又方便,最近正好准备看《第一行代码》第三版,那就跟着书边学边做笔记吧
一、变量和函数
1、变量:
由于Kotlin的自动推导机制,所以在声明一个变量时,一般只用到两个关键字:val、var
val(value)
:声明不可变的变量,赋值之后不能重新赋值,对应Java中final修饰的变量
var(variable)
:声明可变的变量,赋值后仍可赋值
Kotlin每行代码的结尾不用加分号
例子:
val a = 10
println("a = " + a)
变量延迟赋值时,需要显示的声明变量的类型
//延迟赋值
val a: Int 10
//正常赋值
val a = 10
kotlin中,val声明的变量一旦赋值,便不可再修改,这解决了Java中final关键字没有合理利用的问题(除非一个变量明确允许被修改,否则都应该给他加上final)
使用诀窍:永远优先使用val来声明变量,当val没有办法满足你的需求时再使用var
2、函数
函数语法规则:
fun 函数名(参数名1: 参数类型,参数名2: 参数类型): 返回类型{
}
fun methodName(param1: Int,param2: Int): Int{
return 0
}
如果没有返回值,那么返回类型不必写
Kotlin语法糖:
当一个函数中只有一行代码时,Kotlin允许我们不必编写函数体,可以直接将唯一的一行代码写在函数定义的尾部,中间用=
连接即可
//max()是kotlin的内置函数,直接调用
fun largerNumber(num1: Int,num2: Int) = max(num1,num2)
此处省略了返回值类型、return关键字、函数体
二、程序的逻辑控制
1、if语句
kotlin的条件语句主要两种形式:if和when
if语句相较于Java中的if,增加了返回值这一额外功能
fun largerNumber(num1: Int,num2: Int): Int{
val value = if(num1>num2){
num1
}else{
num2
}
return value
}
可优化之处:
- value变量多余
- 根据上面的kotlin语法糖,可以省略return和方法体
fun largerNumber(num1: Int,num2: Int) = if(num1>num2)
num1
else
num2
2、when条件语句
Kotlin中的when是对Java中switch的改良(由于switch的break常发生错误),when和if类似,也有返回值
用法:
fun getStore(name: String) = when(name){
"Tom" -> 86
"Jim" -> 77
else -> 0
}
注:
- 使用when时,最后也要有个else与之匹配
- when语句允许传入一个任何类型的参数,内部的结构体格式:
匹配值 ->{执行逻辑}
- when语句还允许进行类型匹配:
is关键字类似于Java中的instanceof关键字
when(num){
is Int -> println("num is Int")
is Double -> println("num is Double")
else -> println("num not support")
}
- when语句有时还可以不带参数
kotlin判断字符串或对象是否相等可以直接使用==
关键字,而不用像Java去调用equals()方法
fun getStore(name: String) = when{
name == "Tom" -> 86
name == "Jim" -> 77
else -> 0
}
3、循环语句
kotlin中的while的用法和Java一样,但对for做了大幅度的修改
kotlin对Java中的for-each循环进行了加强,而舍弃了for-in的用法
区间:
val range = 0..10
//区间 [0,10] 两端都是闭区间
val range = 0 until 10
//区间 [0,10) 左开右闭
默认情况下,for-in循环每次执行循环时会在区间范围递增1,若想跳过其中一些元素,可以使用step关键字
for(i in 0 until 10 step 2){//每次循环会在范围区间内递增2
}
除了 ..
和 until
使范围区间升序外,我们也可以创建降序的区间,使用downTo
关键字
for(i in 10 downTo 1){
}
//区间 [10,1]
如果有一些特殊场景使用for-in循环无法实现的话,我们还可以改用while循环的方式进行实现
三、面向对象编程
1、类与对象
kotlin中定义类时,也是 字段+函数
kotlin中实例化一个类的方式和java类似,只是不使用new关键字了
class Person{
var name = ""
var age = 0
fun eat(){
...
}
}
fun main(){
val p = Person()
p.name = "Jack"
p.age = "11"
p.eat
}
这样设计的好处是,当我们调用类的构造函数时,更能突出我们只是对这个类进行实例化的意图
2、继承与构造函数
kotlin中任何一个非抽象类默认都是不可以被继承的,相当于java中给类声明了final关键字。类和变量一样,最好都是不可变的,而一个类允许被继承的话,无法预知其子类会如何实现,会存在一定的风险
如果一个类不是抓men为继承而设计的,那么就应该主动将它加上final声明,禁止它可以继承
为了使一个类能被继承,需要准备2步
- 使用open关键字对类进行修饰
加上open关键字之后,我们就是在主动告诉kotlin编译器,Person这个类是专门为继承而设计的 - 使用
:
(冒号) 去替代java中继承时的extends
关键字
open class Person{
}
class Student : Person(){
}
思考:为什么继承Person时,后面还要接一个括号
首先我们得了解kotlin的构造函数分为:主构造函数和次构造函数
1)主构造函数:
最常用的构造函数,每个类都会默认有一个不带参数的主构造函数。主构造函数的特点是没有函数体,直接定义在类名后面即可
class Student(val sno:String, val grade: Int) : Person(){
}
在这里,表面了对Student类进行实例化的时候,必须传入构造函数中所要求的所有函数,从上面可以看出,每个参数都用val修饰的,因为构造函数的参数都是在创建实例时传入的,不像之前那样还能在函数体中重新赋值,所以都用的val修饰。
val student = Student("a123",11)
由于主构造函数没有函数体,那么我们想要在主构造函数中写一些逻辑,就可以使用kotlin提供的init结构体,所有主函数的逻辑都可以写在里面
class Student(val sno:String, val grade: Int) : Person(){
init{
...
}
}
不过在大多数情况下,我们都不需要编写init结构体
根据继承特性规定,子类构造函数必须调用父类的构造函数。但是kotlin的主构造函数没有函数体,无法实现直接调用父类的构造函数,所以kotlin设计了括号来解决这个问题,子类的主构造函数调用父类中的哪个构造函数,在继承时,通过括号来指定
class Student(val sno:String, val grade: Int) : Person(){
}
Person后的一对空括号表示Student类的主构造函数在初始化时,会调用Person类的无参构造函数。即使在无参数的情况下,这对括号也不能省略
Person中没有无参构造时,需要根据其现有的主构造函数去继承,所以我们在继承Person时,应该给Person传入name和age参数,但是Student没有这两个参数,所以我们可以在Student的主构造函数中加入这两个字段,这样就能传给Person中的构造函数了
open class Person(val name: String,val age: Int){
}
class Student(val sno:String, val grade: Int, name: String,age: Int) : Person(name,age){
}
在Student要传到Person的两个字段中,我们没有使用var或val去对两个变量进行修饰。因为在主构造函数中用val或var修饰的参数将自动成为该类的字段,这样和父类中同名的name和age字段会产生冲突。所以我们不使用var或val对其进行修饰,让他的作用域仅限于主构造函数中。
2)次构造函数
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){//这里的this是调用的主构造函数
}
//无参次构造函数
constructor() : this("",0){//这里的this是调用的上面的次构造函数
}
}
次构造函数是用constructor关键字定义来的,第二个次构造函数中,通过this去调用了第一个次构造函数,而第一个次构造函数通过this调用了主构造函数,这样第二个次构造函数就间接的调用了主构造函数。
val student1 = Student()
val student2 = Student("Jack",12)
val student3 = Student("a123",2,"Jack",12)
主构造函数和次构造函数都能用于对象的实例化
特殊情况
类中只有次构造函数,没有主构造函数
class Student: Person{
constructor(name: String,age: Int): super(name,age){//用super去调用父类的构造函数
}
}
由于Student类没有显示的定义主构造函数,但又定义了次构造函数,所以Student类是没有主构造函数的,所以他继承Person时,也不需要在Person的后面添加括号来表面要继承的构造函数。
由于Student没有主构造函数,所以其次构造函数只能通过super关键字去调用父类的构造函数
3、接口
kotlin中,:
(冒号)不仅替代了java中的extends关键字,也替代了implements关键字,所以类在继承父类和实现接口时都使用:
(冒号),中间用,
分开
interface Studey{
fun readBooks()
}
class Student(name: String,age: Int): Person(name,age),study {
override fun readBooks() {
...
}
}
kotlin使用override关键字来重写父类或者实现接口中的函数
kotlin对接口增加了一个额外的功能:允许对接口中定义的函数进行默认实现
interface Study {
fun readBooks()
fun doHomework(){
println("default")
}
}
如果接口中的一个函数拥有了函数体,这个函数体中的内容就是它的默认实现。当子类去实现这个接口时,会强制要求实现readBooks()函数,而doHomework()函数自由选择实现或不实现(和Java中的抽象类相似)
函数可见修饰符
权限修饰符 | Kotlin | Java |
---|---|---|
public | 所有类可见(默认) | 所有类可见 |
private | 当前类可见 | 当前类可见 |
protected | 当前类和子类可见 | 当前类、子类、同包下的类可见 |
default(什么都不写) | 无 | 同包可见(默认) |
internal | 同一模块的类可见 | 无 |
4、数据类与单例类
1)数据类
java中定义一个数据类时,通常为其添加 字段+setter+getter,还有配合系统类进行处理的equals()、hashCode()、toString()方法等配套方法,但是这些方法只是给数据类提供了功能,并没有实际的逻辑意义。
kotlin创建数据类时,只用一行代码就能完成java中的上述功能,大大减少开发的工作量
data class CellPhone(var brand: String,val price: Double)
data关键字表明你希望这个类是一个数据类,kotlin会自动傍你生成equals()、hashCode()、toString()等无实际逻辑意义的代码
如果没有data关键字修饰这个类,那么输出对象的时候,输出的是一串hash码,因为kotlin没有为其生成toString()方法。
2)单例类
相较于Java创建单例时的私有化构造函数、使用静态方法等操作,kotlin只需要将一个正常类的class
关键字换成object
关键字修饰即可,这样由kotlin背后帮我们创建一个Singleton实例,并保证全局只会存在一个Singleton实例
object Singleton {
fun singletonTest(){
...
}
}
fun main(){
Singleton.singletonTest()
}
四、Lambda编程
1、集合的创建与遍历
1)List
在Java中创建并初始化一个集合时,可能会先实例化一个ArrayList对象,再把元素一个一个add进集合里,kotlin也支持这样的操作,但是太过繁琐,所以kotlin提供了一个内置的listOf()函数来简化这一过程
val list = listOf("Apple","Banana","Orange","Pear")
for (fruit in list) {
}
但是listOf创建的list是一个只能用于读取,但不能对集合进行添加、修改、删除的不可变集合。所以,想要创建一个可变集合,需要使用mutableListOf()函数
val list = mutableListOf("Apple","Banana","Orange","Pear")
list.add("Watermelon")
list.remove("Apple")
2)Set
set的使用,和list很类似,只是把创建的方法改成了setOf和mutableSetOf()函数
3)Map
Map与Java中的语法类似,也可以通过put()和get()方法对Map进行添加和读取数据,但kotlin却不建议使用这两个函数,而是推荐使用一种类似于数组下标的语法结构
val map = HashMap()
//存值
map["Apple"] = 1
//取值
val number = map["Apple"]
和list一样,这样一个一个的存太过繁琐,kotlin提供了一对mapOf()和mutableMapOf()函数来简化这一过程
val map = mapOf("Apple" to 1,"Banana" to 2,"Orange" to 3)
for ((fruit, number) in map) {
}
这里的to并不是关键字,而是infix函数
2、集合的函数式Api
1)Lambda表达式
Lambda就是一小段可以作为参数传递的代码
Lambad语法结构
{参数名1:参数类型,参数名2:参数类型 -> 函数体}
->
符号表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码(但不建议写太长),并且最后一行代码会自动作为Lambda表达式的返回值。
Lambda表达式的简化过程
- 传入一个lambda表达式到maxBy函数中,因为maxBy接收的就是一个lambda类型的参数,所以可以直接传进去。
val maxLengthFruit = list.maxBy({fruit: String -> fruit.length})
- 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 maxLengthFruit = list.maxBy{it.length}
2)filter函数
filter函数是用来过滤集合中数据的,可单独使用,也可配合刚才的map一起使用
val newList = list.filter { it.length <= 5 }
.map { it.toUpperCase() }
先使用filter函数再用map函数,会高效得多,如果先用map,会对集合中的所有元素先映射转换后再过滤;而先用filter函数,会是对filter后的元素进行映射,提高效率
3)any函数、all函数
any函数用于判断集合中是否存在一个元素满足指定条件
all函数用于判断集合中是否所有元素都满足指定条件。
3、Java函数式API的使用
kotlin中调用Java方法时也可以使用函数式Api。
当在Kotlin中调用Java方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式Api
Java单抽象接口:接口中只有一个待实现方法。如Runnable接口
public interface Runnable{
void run();
}
以线程举例:
//java
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
//kotlin
Thread(object : Runnable{
override fun run() {
}
})
由于kotlin完全舍弃了new关键字,因此创建匿名类实例时就不能再使用new了,而是改用object关键字
对kotlin代码进行简化:
- 因为Runnable类中只有一个待实现的方法:
Thread(Runnable {
}).start()
- 如果Java方法的参数列表中不存在一个以上Java单抽象方法接口参数,我们还可以将接口进行省略
Thread({
}).start()
- 和之前kotlin中函数式Api用法类似,当Lambda表达式是方法的最后一个参数时,可将Lambda表达式移到方法括号的外面。并且,当Lambda表达式是方法的唯一一个参数时,可省略方法后面的括号
Thread{
}.start()
此用法在Android中的View设置点击监听时有体现
button.setOnClickListener{
//...
}
五、空指针检查
kotlin为了避免产生空指针异常,所以默认所有的参数和变量都不可为空。kotlin将空指针异常的检查提前到了编译时期,如果我们的程序存在空指针异常的风险,在编译时期就会直接报错
1)空的类型系统 ?
写程序过程中,需要某个参数为null时怎么办呢?
kotlin提供了一套可为空的类型系统
用法:在类型名的后面加上一个问号
fun doStudy(studey: Study?){
if(study != null){
}
}
使用可为空的类型系统后,需要自己处理空指针异常,不然代码不能编译通过。
2)判空辅助工具 ?.
如果每次都用if去处理空指针异常,会让代码显得啰嗦
所以用到了kotlin中的判空辅助工具 —— ?.
操作符
使用:
//java
if(a != null){
a.dosomthing
}
//kotlin
a?.dosomthing
3)判空辅助工具 ?:
同样?:
操作符:这个操作符左右两边都接收一个表达式,如果左边表达式的结果不为空,就返回左边表达式的结果,否则返回右边表达式的结果。
val c = if(a != null){
a
}else{
b
}
val c = a ?: b
综合使用:
fun getTextLength(text: String): Int{
if(text != null){
return text.length
}
return 0
}
fun getTextLength(text: String?) = text?.length ?: 0
4)判空辅助工具 !!
——非空断言工具
当我们非常确信这里的对象不会为空时,想告诉kotlin,不用做空指针检查了
content!!.toUpperCase()
当你想要使用非空断言工具的时候,最好提醒一下自己,是不是有更好的实现方式
5)let
let不是操作符也不是关键字,而是一个函数(标准函数)。let函数的特性配合?.
操作符在空指针检查时可以起到很大作用
//java
fun doStudy(study: Study){
stydy?.readBooks()
stydy?.doHomework()
}
//kotlin
fun doStudy(study: Study){
stydy?.let{
it.readBooks()
it.doHomework()
}
}
let让study对象本身作为参数传到Lambda表达式中,但当Lambda参数列表只有一个参数时,可以省略,并用it关键字代替
let函数是完全可以处理全局变量的判空问题的
当用if(study != null)
这类判断空指针时,由于全局变量随时可能被其他线程修改,所以无法保证if语句中的study变量有没有空指针风险
六、Kotlin中的小魔术
1、 字符串内嵌表达式
对于java中使用+
来连接字符串的方式,kotlin使用内嵌表达式${}
来替代。
”hello,${obj.name}. nice to meet you“
当内嵌表达式只有一个变量时,可以省略其大括号
”hello,$obj.name. nice to meet you“
2、函数的参数默认值
构造函数默认值:在定义函数时给任意函数设定一个默认值,这样当调用此函数时就不会强制要求调用方为此函数传值,在没有传值的情况下自动使用参数的默认值
由于次构造函数在kotlin中很少用,因为kotlin给函数设定参数默认值的功能,使其很大程度上能替代次构造函数的作用
fun printParams(num: Int,str: String = "hello"){
println("num is $num,str is $str")
}
当调用这个函数时,可以选择不传参数给第二个参数printParams(123)
fun printParams(num: Int = 123,str: String){
println("num is $num,str is $str")
}
但当给第一个参数使用默认值时,我们使用printParams("hello")
会出现类型不匹配问题,因为编译器会以为我们是给第一个参数赋值。所以kotlin提供了键值对的方式来传参printParams(str="hello")
函数默认值是如何替代次构造函数的呢?
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进行实例化的方式。当加入默认参数值后,可以直接用一个主构造函数搞定
class Student(val sno: String = "",val grade: Int = 0,name: String = "",age: Int = 0): Person(name,age){
}