变量声明

内置数据类型

只读变量
val声明只读变量,但非常量(详见编译时常量);var声明普通变量。
类型推断
变量声明时允许不给出变量类型,编译器会自动推断变量类型。
编译时常量
区别于只读变量,编译时常量只能声明在函数之外,且在编译时值就已经确定。
而只读变量可以在函数内声明,并进行一次赋值。
编译时常量的声明方法是:const val valName
,即在val关键字前加const关键字。
range表达式
使用:varName in a..b
可定义一个range表达式,其返回一个布尔值,用于判定变量是否处于某一范围内。
也可使用varName !in a..b
来判断变量是否不处于某一范围内。
其中a..b
可替换为诸如List或者Array之类的对象。
IntRange
使用:(1..24)
可以返回一个IntRange类型的变量,其代表一个整数范围。
也可以使用until关键字:1 until 24
,其中左右两边的数字可以用变量代替。
其有一个shuffled()成员函数可以用于打乱IntRange内的元素,然后用last()函数可以取出其最后一个元素(相当于随机数)。
when表达式
类似于其他语言中的switch表达式。
其基本语法如下:
//可以基于条件返回对应的值 val varName = when(condition){ "case1" -> "result1" "case2" -> "result2" else -> "default" } //或者只是作为类似switch的功能 when(condition){ 1 ->{ println("1") } //使用range表达式 in 2..4 -> { println("2到4") } else -> { println("default") } }
字符串模板
在字符串引号内,可以直接放入变量。
其使用方式如下:
//使用$符号引入字符串模板 val name = "zxy" println("Your name is $name") //$后还可以使用{}来引入表达式 val flag = true println("The flag is ${if(flag) "true" else "false"}")
函数头

Kotlin的函数支持默认参数(给参数指定缺省值)和具名传参(在传递参数时指定要传给的形参名)
Unit类型
相当于其他语言中的void类型。但特点是可以作为类型在泛型中使用。
Nothing类型
类似Java中的封装类Void。区别于Unit,函数返回一个Nothing类型时,其不可被访问,也不能在泛型中使用。但是Nothing?类型可以存放一个null值。
反引号函数名
用反引号括起来的函数名当中可以使用特殊字符诸如空格、中文,等等;还可以使用Kotlin中的关键字作为函数名。
比如:`~~**一个 fun**~~`
匿名函数
匿名函数通常用于传给其他函数或作为返回值返回,其语法如下:
//自定义一个数相运算的方法 fun countFunc(op1:Int, op2:Int, method:(Int, Int)->Int){ return method(op1,op2) } //传入参数1和2,并要求用给定方法返回值(看起来多余,只是用于演示) val sum= countFunc(1, 2, {a , b -> a + b })
countFunc的第三个参数接受一个“接受两个int类型参数,并返回一个int类型”的函数。
在传入参数时,可以传入匿名函数。
匿名函数用大括号括起,形参列表位于箭头左侧,箭头之后是表达式,匿名函数的返回值由最后一行表达式的值确定,所以return需要省略。
另外值得注意的是,当匿名函数只需要一个参数时,箭头和箭头前面的形参列表可以省略,且形参会自动用it表示。而不需要参数时,箭头也可以省略。
还有就是,当一个函数的参数有且只有一个,且接受的是函数类型时,调用该函数时传递参数时,圆括号可以省略。
lambda
匿名函数称为lambda,它的定义称为lambda表达式,它返回的数据称为lambda结果。
函数内联
lambda在JVM中会创建一个独立的对象,会带来很大的开销,kotlin用函数内联的机制来优化这个问题。
一般做法是,在接受参数中有函数的函数前的fun关键字前,加上inline关键字。
函数引用
当想将具名函数作为参数传入一个接受函数作为参数的函数时,可以使用函数引用。
具体做法是,在传参时,用函数名前加上双引号:
fun funcA(pram:(Int)->Int):Int{ //... } fun funcB(pram:Int):Int{ //... } fun main(){ funcA(::funcB) }
闭包
闭包的概念与匿名函数联系紧密。匿名函数运行时,引用了其作用域之外的数据时,那些数据也会被囊括进匿名函数当中。这种现象称为闭包。
安全调用操作符
可空类型在调用其成员函数时,需要用到安全调用操作符,即把.
换成?.
,表明我们明确知道可能会发生空指针异常的风险。当可空类型变量的值为null时,该调用会返回一个null。
对象的标准函数
Kotlin中对象内置5个标准函数,分别是:let、also、with、run、apply。
标准函数的实现基于lambda。
let函数
// 作用1:使用it替代object对象去访问其公有的属性 & 方法 object.let{ it.todo() } // 作用2:判断object为null的操作 object?.let{//表示object不为null的条件下,才会去执行let函数体 it.todo() } // 注:返回值 = 最后一行 / return的表达式
also函数
类似let函数,但区别在于返回值:
- let函数:返回值 = 最后一行 / return的表达式
- also函数:返回值 = 传入的对象的本身
// let函数 var result = mVar.let { it.function1() it.function2() it.function3() 999 } // 最终结果 = 返回999给变量result // also函数 var result = mVar.also { it.function1() it.function2() it.function3() 999 } // 最终结果 = 返回一个mVar对象给变量result
with函数
作用
调用同一个对象的多个方法 / 属性时,可以省去对象名重复,直接调用方法名 / 属性即可
应用场景
需要调用同一个对象的多个方法 / 属性
// 此处要调用people的name 和 age属性 val people = People("carson", 25) with(people) { println("my name is $name, I am $age years old") }
run函数
run函数结合了let、with两个函数的作用,即:
- 调用同一个对象的多个方法 / 属性时,可以省去对象名重复,直接调用方法名 / 属性即可
- 定义一个变量在特定作用域内
- 统一做判空处理
// 此处要调用people的name 和 age属性,且要判空 val people = People("carson", 25) people?.run{ println("my name is $name, I am $age years old") }
apply函数
作用 & 应用场景
与run函数类似,但区别在于返回值:
- run函数返回最后一行的值 / 表达式
- apply函数返回传入的对象的本身
应用场景
对象实例初始化时需要对对象中的属性进行赋值 & 返回该对象
// run函数 val people = People("carson", 25) val result = people?.run{ println("my name is $name, I am $age years old") 999 } // 最终结果 = 返回999给变量result // apply函数 val people = People("carson", 25) val result = people?.apply{ println("my name is $name, I am $age years old") 999 } // 最终结果 = 返回一个people对象给变量result
takeIf函数
takeIf函数接受一个lambda表达式,当lambda表达式返回的值为true时返回对象本身,为false时返回null。
val result = File("D:/a.txt").takeIf{ it.exist() && it.canRead }?.readText()
takeUnless函数
正好和takeIf函数相反,当lambda表达式返回的值为false时返回对象本身,为true时返回null。
非空断言操作符
当一个对象为空,调用这个对象的成员函数时,我们不想其返回null而是抛出异常时,需要用到非空断言操作符:!!.
,当对象真为空时,会抛出异常。
空合并操作符
空合并操作符:?:
,不同于c++,这个操作符是个二元操作符,左侧对象为空时,该表达式的值会是右边的值;左侧对象不为空时,该表达式就是左边的值。
先决条件函数

==与===
当比较两个字符串时,==用于比较内容是否相同,===用于比较是否是同一对象。
List对象
创建一个List对象通常调用listOf方法,listOf方法接受元素作为参数,返回一个List对象。
获取List中的元素时,可以使用脚标,也可以使用安全函数,如list.getOrElse(index){ "default" }
,还有list.getOrNull(index)
可变List
listOf创建的List不可修改,要创建可修改的List,要调用mutableListOf函数。
另外,toList和toMutableList函数可以用来在两者之间转换。
添加或删除元素可以使用add和remove函数。
或者使用+=或-=
field关键字
kotlin在定义类成员变量时,会自动生成对应的setter和getter,如果要覆盖自动生成的getter和setter,则要在变量声明下写set和get函数,get和set函数当中,用field关键字访问变量对象,具体如下:
class ClassName{ var varName = "name" get() = field.capitalize()//获取变量时返回首字母大写的版本 set(value){ field = value.trim()//赋值时去掉空格 } } fun main(){ var obj = ClassName() print(obj.varName)//会自动调用变量的getter方法 obj.varName = " hello "//会自动调用变量的setter方法 print(obj.varName)//会自动调用变量的getter方法 }
主构造函数
Kotlin当中的构造函数可以直接将类声明写成函数的形式,括号中给定参数列表,其中参数的名字开头用下划线,表示是临时变量:
class ExampleClass( _a:Int, _b:Int ){ val a = _a val b = _b } fun main(){ val obj = ExampleClass(1,2) }
也可以直接在圆括号中定义属性:
class ExampleClass( var a:Int, var b:Int ){ //... } fun main(){ val obj = ExampleClass(1,2) }
次构造函数
除了主构造函数,在类声明里还可以添加次构造函数,需要用到关键字constructor,具体语法如下:
class ExampleClass( var a:Int, var b:Int ){ constructor(_a:Int):this(_a,0){ //自定义的逻辑 this.a = _a + 1 } } fun main(){ val obj1 = ExampleClass(1,2) val obj2 = ExampleClass(1)//使用次构造函数 }
其中this()是调用主构造函数。
初始化块
在类声明中使用init关键字的代码块会在构造函数运行时运行:
class ExampleClass( var a:Int, var b:Int ){ constructor(_a:Int):this(_a,0){ //自定义的逻辑 this.a = _a + 1 } init{ //初始化操作 } } fun main(){ val obj1 = ExampleClass(1,2) val obj2 = ExampleClass(1)//使用次构造函数 }
初始化顺序
这么多初始化的方法,他们的执行顺序是怎样的?

通过反编译成java代码,一切就一目了然了:

延迟初始化
lateinit关键字用于声明延迟初始化,允许类构造时不初始化该变量。加在var关键字之前。
这里会牵扯到一个检查变量是否初始化的问题,使用::varName.isInitialized
,可以返回一个布尔值,表明变量是否已经初始化。
惰性初始化
区别于延迟初始化,惰性初始化需要实现给出初始化方法,但是在变量首次被用到的时候才会被初始化。
声明惰性初始化使用by lazy
关键字,具体使用方法:
class ExampleClass{ val varName by lazy{loadVar()} private fun loadVar(){ return 123 } }
上面的代码中varName变量在初次被使用时会通过loadVar函数初始化。
类的继承
Kotlin当中类默认不可被继承,需要类可以被继承要使用open关键字。
open关键字加在class关键字之前。
继承方式是类名后加冒号,冒号后接基类名。
基类名后接圆括号,可以直接调用基类的构造函数来初始化派生类。
同样的,基类中的函数前也要加上open关键字才能被覆写。
派生类中覆写基类的函数需要在函数前加上override关键字。
类型检测
Kotlin当中可以用is关键字来判断变量是否是某个类型:varName is TypeName
,这个表达式会返回一个布尔值
类型转换
使用as关键字可以进行类型转换:varName as TypeName
object关键字创建单例
将class改成object,可以生成一个该类的单例对象。
object还可以用来声明一个匿名对象,类型与java的匿名内部类,具体用法如下:
open class player{ open fun load() = "loading nothing..." } fun main(){ val p = object : player(){//派生自player类的子类,但是只在这里使用 override fun load() = "anonymous class load..." } println(p.load()) }
伴生对象
当想将某个对象的初始化和一个类实例捆绑在一起,可以考虑使用伴生对象,声明伴生对象使用companion修饰符,一个类里只能有一个伴生对象:
open class ConfigMap{ //只有初始化ConfigMap类或调用load函数时,伴生对象的内容才会载入 //而且无论ConfigMap实例化多少次,伴生对象始终只有一个实例存在 companion object{ private const val PATH = "XXX" fun load() = File(PATH).readBytes() } } fun main(){ //可以直接通过类名访问到伴生对象中的内容 ConfigMap.load() }
数据类
在class关键字前加data关键字可将一个类声明为数据类
数据类只用于储存数据,不储存方法。
使用数据类可以得到重写版本的toString方法、equals方法(可以直接用==判断两个数据类所存的数据是否相同)
数据类还提供一个copy函数,可以简单地得到类实例的副本,copy函数的参数可以是主构造函数的参数(不必给出全部参数),提供参数可以初始化新的内容,剩余内容会和被拷贝的对象一致。需要注意的是copy函数调用给你的是主构造函数,不会调用次构造函数。
数据类还默认支持解构语法↓
解构语法
一个类的对象想要给多个变量赋值,需要用到解构语法,需要写component函数:
//普通类支持解构语法需要写component函数 class Position(val x:Int, val y:Int){ //数字需要按顺序 operator fun component1() = x operator fun component2() = y } //数据类默认支持解构语法 data class PositionData(val x:Int, val y:Int){ } fun main(){ val (x,y) = Position(100,200)//给x和y两个变量赋值 val (x,y) = PositionData(100,200)//给x和y两个变量赋值 }
数据类默认支持解构语法!
运算符重载
运算符重载需要重写对应的函数,用operator fun
关键字:

data class PositionData(val x:Int, val y:Int){ operator fun plus(other:PositionData) = PositionData(x+other.x, y+y.other) } fun main(){ val (x,y) = PositionData(100,200) + PositionData(200,100) //可以得到x=300,y=300 }
枚举类
class关键字前加enum关键字,可将类声明为枚举类:
enum class Direction{ EAST, WEST, NORTH, SOUTH } fun main(){ println(Direciton.EAST) println(Direciton.EAST is Direciton)//true }