Kotlin 语言基础学习
简单介绍
定义包
包的声明应处于源文件顶部:
1 | package my.demo |
目录与包的结构无需匹配:源代码可以放在文件系统的任意位置。
定义函数
带有两个Int
参数,返回Int
函数:
1 | fun sum (a:Int,b:Int):Int{ |
将表达式作为函数体,返回值类型自动推送的函数:
1 | fun sum (a:Int,b:Int) = a + b |
函数返回无意义的值:
1 | fun printSum(a:Int,b:Int){ |
Unit
返回类型可以省略:
1 | fun printSum(a:Int,b:Int){ |
定义变量
定义只读局部变量使用关键字val
定义。只能为其赋值一次。
1 | val a:Int = 1 //立即赋值 |
可重新赋值的变量使用var
关键字:
1 | var x = 5 |
顶层变量:
1 | val PI = 3.14 |
注释
正如Java,Kotlin支持行注释及块注释。
1 | // 这是一个行注释 |
与Java不同的是,Kotlin的块注释可以嵌套。
使用字符串模板
使用字符串模板的符号为($
)。在$
符号后面加上变量名或大括号中的表达式
1 | var a = 1 |
执行结果:
1 | a was 1, but now is 2 |
声明可空变量
在Kotlin中当我们不确定某个属性或变量一定不为空时,我们就把它声明为可空变量.
可空变量的特点:
- 在声明的时候一定要用标准的声明格式定义。不能用可推断类型的简写;
- 变量类型后面的
?
符号不能省略。不然就和普通的变量没区别了; - 其初始化的值可以为
null
或确定的变量值。
1 | class Test{ |
常量的用法
在Kotlin中val
修饰的还不是常量,他只是个不能修改的变量。常量的定义还需要再val
关键字前加上const
关键字。即:
1 | const val NUM_A = 6 |
其特点:**const
只能修饰val
,不能修饰var
**。
声明常量的三种方式:
- 在顶层声明;
- 在
object
修饰的类中声明,在kotlin
中称为对象声明; - 在伴生对象中声明。
例如:
1 | // 1.顶层声明 |
数据类型
数值类型
1. 数字的内置类型
Byte
=> 字节 => 8位Short
=> 短整型 => 16位Int
=> 整型 => 32位Long
=> 长整型 => 64位Float
=> 浮点型 => 32位Double
=> 双精度浮点型 => 64位
注意:
- 长整型在数值末用大写字母L标记
- 单精度浮点型由字母F(大写小写无妨)标记
2. 进制数
Kotlin不支持八进制数
3. 数字类型字面常量的下划线
作用:分割数字进行分组,使数字常量更易读
例如:
1 | val oneMillion = 1_000_000 |
4. 装箱与拆箱
装箱就是值类型转换为object类型,拆箱相反:object转化为值类型。在kotlin
中,存在数字的装箱,但是没有拆箱。因为kotlin
是没有基本数据类型的,Kotlin
是万物皆对象的原则。
在kotlin
中要实现装箱操作,首先要了解可空引用。即类似Int?
(只限数值类型)此类的:
1 | val numValue:Int = 1 |
两个数值的比较
- 判断两个数值是否相等
==
- 判断两个数值在内存中的地址是否相等
===
5. 转换
显式转换
较小的类型不会被隐式转换位更大的类型,故而系统提供了显式转换:
toByte()
=> 转换为字节型toShort()
=> 转换为短整型toInt()
=> 转换为整型toLong()
=> 转换为长整型toFloat()
=> 转换为浮点型toDouble()
=> 转换为双精度浮点型toChar()
=> 转换为字符型toString()
=> 转换为字符串型
隐式转换
类型是从上下文推断出来的,即算术运算则被重载为适当的转换:
1
2
3val num = 30L + 12
println(num)
// 30L + 12 -> Long + Int => Long
6. 位运算符
// 。。。。。
布尔类型
1. 关键字
Boolean
关键字表示布尔类型,并且其值有ture
和false
1 | var isNum:Boolean |
2. 逻辑操作符
- ‘||’ => 逻辑或
- ‘&&’ => 逻辑与
- ‘ ! ’ => 逻辑非
字符型
1. 关键字
Char
表示字符型,字符变量用单引号(‘’)表示。并且不能直接视为数字,不过可以通过显式转换为数字:
1 | var char_1:Char |
2. 显示转换为其他类型
字符型的变量不仅会可以转换为数字,同时也可转换为其他类型:
1 | var var1 = char_1.toByte() |
除了类型转换,当变量为英文字母时还支持大小写转换:
1 | var charA:Char = 'a' |
3. 字符转义
\t
=> 制表符\n
=> 换行符\b
=> 退格键\r
=> 回车键\\
=> 反斜杠\'
=> 单引号\$
=> 美元符号- **其他的任何字符请使用Unicode转义序列语法,例如:’\uFF00’
字符串类型
1. 关键字
String
表示字符串类型。其实不可变的。所以字符串的元素可以通过索引操作的字符:str[index]
来访问。可以使用for
循环迭代字符串:其中str[index]
中的str
要为目标字符串,index
才能为索引:
1 | val str:Sting = "kotlin" |
2. 字符串字面量
在Kotlin
中,字符串字面量有两种类型:
- 包含转义字符的字符串,转义包括(\t, \n等),不包括转义字符串的也同属此类型
- 包含任意字符的字符串,有三重引号(
"""..."""
)表示
数组型
Kotlin中
数组由Array<T>表示
- 创建数组的三个函数:
arrayOf()
arrayOfNulls()
- 工厂函数(
Array()
)
1. arrayOf()
创建一个数组,参数是一个可变参数的泛型对象:
1 | val arr1 = arrayOf(1,2,3,4,5) |
2. arrayOfNulls()
用于创建一个指定数据类型且可以为空元素的,给定元素个数的数组:
1 | var arr2 = arrayOfNulls<Int>(3) |
3. 工厂函数
- 使用工厂函数
Array()
,它使用数组大小和返回给定其索引的每个数组元素的初始值的函数。 Array()
=> 第一个参数表示的个数,第二个参数则为使用其下标组成的表达式
1 | var arr3 = Array(5,{index -> (index*2).toString()}) |
执行结果:
1 | 0 2 4 6 8 |
4. 原始类型数组
kotlin
还有专门的类来表示原始类型的数组,没有装箱开销,它们分别是:
ByteArray
=> 表示字节型数组ShortArray
=> 表示短整型数组IntArray
=> 表示整型数组LongArray
=> 表示长整型数组BooleanArray
=> 表示布尔型数组CharArray
=> 表示字符型数组FloatArray
=> 表示浮点型数组DoubleArray
=> 表示双精度浮点型数组
Kotlin
不支持字符串类型这种原始类型的数组
1 | var intArr:IntArray = intArrayOf(1,2,3,4,5) |
控制语句
if语句
kotlin
中的if
语句很灵活,除了普通的判断,还可以实现表达式(实现三元运算符),及作为一个块的作用
1. 传统写法
1 | var numA = 2 |
2. Kotlin
中的三元运算符
在Kotlin
中其实不存在三元运算符(condition ? then : else)这种运算,那是因为if语句的特性:if
表达式会返回一个值,所以不需要三元运算符。
1 | var numB:Int = if (numA > 2) 3 else 5 |
3. 作为一个块结构,并且最后一句表达式为块的值
1 | var numA:Int = 2 |
for语句
for
循环提供迭代器用来遍历任何东西for
循环数组被编译为一个基于索引的循环,它不会创建一个迭代器对象
规则
递增
关键字:
until
范围:
until[n,m]
=> 大于等于n,小于m例如:
1
2
3
4// 循环5次,且步长为1的递增
for (i in 0 until 5){
print(i)
}递减
- 关键字:
downTo
范围:
downTo[n,m]
=> 大于等于n,小于等于m,n > m例如:
1
2
3
4// 循环5次,且步长为1的递减
for(i in 15 downTo 11){
print(i)
}
- 关键字:
符号(
..
)表示递增循环的另外一种操作
使用符号
..
范围:
..[n,m]
=> 即大于等于n,小于等于m
和
until
有区别,但更为简便,且范围不同例如:
1
2
3for (i in 20..25){
print(i)
}
设置步长
关键字:
step
例如:
1
2
3for (i in 10 until 16 step 2){
print(i)
}
遍历字符串
例如:
1 | for (i in "asdfghjk"){ |
遍历数组
例如:
1 | var arrayList = arrayOf(1,3,5,7,9) |
1 | var arrayList = arrayOf(1,3,5,7,9) |
1 | var arrayList = arrayOf(1,3,5,7,9) |
使用列表或数组的扩展函数遍历
- 数组或列表有一个成员或扩展函数
iterator()
实现了Iterator<T>
接口,且该接口提供了next()
与hasNext()
两个成员或扩展函数 - 其一般和
while
循环一起使用
例如:
1 | var arrayList = arrayOf(2,'a',3,false,9) |
when语句
when
语句类似于C类语言中的switch
语句,不过比它更强大。
实现switch语句功能
1 | when(5){ |
与逗号结合
1 | when(1){ |
条件可以使用任意表达式
1 | var num:Int = 5 |
检查值是否存在于集合或数组中
操作符
in
在!in
不在
限定:只适用于数值类型
例如:
1 | var arrayList = arayOf(1,2,3,4,5) |
检查值是否为指定类型的值
操作符
- 是
is
- 不是
!is
- 是
注意:
kotlin
的智能转换可以访问类型的方法和属性
例如:
1 | when("abc"){ |
不使用表达式的when语句
表示为最简单的布尔表达式
1 | var array = arrayOfNulls<String>(3) |
其它语句
while语句
do while语句
跳转语句:return,break,continue
与C类语言一致,不做赘述。
操作符
一元操作符
简单一元
操作符 | 重载 |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
复杂一元
操作符 | 重载 | 表示 |
---|---|---|
a++ | a.inc() | a = a.also{ a.inc() } |
a– | a.dec() | a = a.also{ a.dec() } |
++a | a.inc() | a = a.inc().also{ a = it } |
–a | a.dec() | a = a.dec().also{ a = it } |
二元操作符
简单二元
操作符 | 重载 |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.tiems(b) |
a / b | a.div(b) |
a % b | a.rem(b) |
a .. b | a.rangTo(b) |
复杂二元
操作符 | 表示 | 重载 |
---|---|---|
a += b | a = a + b | a = a.plus(b) |
a -= b | a = a - b | a = a.minus(b) |
a *= b | a = a * b | a = a.tiems(b) |
a /= b | a = a / b | a = a.div(b) |
a %= b | a = a % b | a = a.rem(b) |
区间操作
区间操作符:..
,注意两个操作数都是整型
操作符 | 表示 | 重载 |
---|---|---|
a .. b | a 到 b 中间的值 | a.rangeTo(b) |
进阶操作
可空类型,空安全
定义一个可空类型的变量
修饰符 变量名 : 类型? = 值
判断可空类型的两种方式
if…else… 判断
使用符号
?.
判断该符号的用法为:
可空类型变量?.属性/方法
。如果可空类型变量为null是,返回null;这种方法大量用于链式操作的用法中,能有效避免
空引用异常
,因为只要链式中有一个null
,则整个表达式都为null
。
高阶函数
let
let
扩展函数的实际上是一个作用域函数,当你需要去定义一个变量在一个特定的作用域范围内,let
函数的是一个不错的选择;
1 | obj.let { |
使用 let 函数处理需要针对一个可 null 的对象统一做判空处理;
需要去明确一个变量所处特定的作用域范围内可以使用;
with
和 let
类似,又和 let
不同,with
最后也包含一段函数块,也是将最后的计算的结果返回。
但是 with
不是以拓展的形式存在的。其将某个对象作为函数的参数,并且以 this
指代,可以类比 js 。
1 | whith(obj) { |
如果我们使用 with
函数的话,由于代码块中传入的是 this
,而不是 it
,那么我们就可以直接写出函数名(属性)来进行相应的设置:
1 | if (textView == null) return |
这段代码唯一的缺点就是要事先判空了,有没有既能像 let 那样能优雅的判空,又能写出这样的便利的代码呢?
run
刚刚说到,我们想能有 let
函数那样又优雅的判空,又能有 with
函数省去同一个对象多次设置属性的便捷写法。
没错,就是这就非我们 run
函数莫属了。run
函数基本是 let
和 with
的结合体,对象调用 run
函数,接收一个 lambda
函数为参数,传入 this
并以闭包形式返回,返回值是最后的计算结果。
1 | obj.run { |
那么上面 TextView 设置各种属性的优化写法就是这样的:
1 | textView?.run { |
像上面这个例子,在需要多次设置属性,但设置属性后返回值不是该对象(或无返回值:Unit)不能链式调用的时候,就非常适合使用 run
函数。
apply
apply
函数和 run
函数很像,但是 apply
最后返回的是调用对象自身。
1 | val result = obj.apply { |
由于 apply
函数返回的是调用对象自身,我们可以借助 apply
函数的特性进行多级判空。
also
和 let
函数类似,唯一的区别就是 also
函数的返回值是调用对象本身:
1 | val result = obj.also { |
总结
函数定义见下表:
函数名 | 实现 |
---|---|
let | public inline fun <T, R> T.let(block: (T) -> R): R = block(this) |
with | public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block() |
run | public inline fun <T, R> T.run(block: T.() -> R): R = block() |
apply | public inline fun T.apply(block: T.() -> Unit): T { block(); return this } |
also | public inline fun T.also(block: (T) -> Unit): T { block(this); return this } |
具体的调用情况见下图:
1 | graph TB |
面向对象
kotlin 中的类成员包括:构造函数、初始化代码块、成员函数、属性、内部类和嵌套类、以及对象表达式声明。
构造函数
在 Kotlin 中的一个类可以有一个主构造函数以及一个或多个次构造函数,主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后;
如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字:
1
2class Person constructor(firstName: String) { /*……*/ }
class Person(firstName: String) { /*……*/ }主构造函数不能包含任何的代码。初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中,主构造的参数可以在初始化块中使用。它们也可以在类体内声明的属性初始化器中使用:
1
2
3
4
5
6class Person constructor(firstName: String) {
var name = firstName
init {
println("name: ${firstName}")
}
}类也可以声明前缀有 constructor的次构造函数,每个次构造函数需要委托给主构造函数, 可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用
this
关键字即可;初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块与属性初始化器中的代码都会在次构造函数体之前执行。即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块;
属性
声明一个属性的完整语法:
1 | var <propertyName>[: <PropertyType>] [= <property_initializer>] |
其初始器(initializer)、getter 和 setter 都是可选的。属性类型如果可以从初始器中推断出来,也可以省略。
1 | val isEmpty: Boolean |
继承
- 在 Kotlin 中所有类都有一个共同的超类
Any
,这对于没有超类型声明的类是默认超类; - 默认情况下,Kotlin 类是最终(final)的:它们不能被继承。 要使一个类可继承,请用
open
关键字标记它;
接口
Kotlin 的接口可以既包含抽象方法的声明也包含实现。与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现。
1 | interface MyInterface { |
数据类
编译器自动从主构造函数中声明的所有属性导出以下成员:
equals()
/hashCode()
对;toString()
格式是"User(name=John, age=42)"
;componentN()
函数按声明顺序对应于所有属性;copy()
函数;
1 | data class User(val name: String, val age: Int) |
- 主构造函数需要至少有一个参数。
- 主构造函数的所有参数需要标记为
val
或var
。 - 数据类不能是抽象、开放、密封或者内部的。