第2章 Kotlin 语法基础
人与人之间通过语言来交流沟通,互相协作。人与计算机之间怎样“交流沟通”呢?答案是编程语言。一门语言有词、短语、句子、文章等,对应到编程语言中就是关键字、标识符、表达式、源代码文件等。通常一门编程语言的基本构成如下图所示
本章我们学习 Kotlin语言的基础语法。
2.1 变量和标识符
变量(数据名称)标识一个对象的地址,我们称之为标识符。而具体存放的数据占用内存的大小和存放的形式则由其类型来决定。
在Kotlin中, 所有的变量类型都是引用类型。Kotlin的变量分为 val
(不可变的) 和var
(可变的) 。可以简单理解为:
val
是只读的,仅能一次赋值,后面就不能被重新赋值。
var
是可写的,在它生命周期中可以被多次赋值;
使用关键字 val 声明不可变变量
>>> val a:Int = 1
>>> a
1
另外,我们可以省略后面的类型Int,直接声明如下
>>> val a = 1 // 根据值 1 编译器能够自动推断出 `Int` 类型
>>> a
1
用val声明的变量不能重新赋值
>>> val a = 1
>>> a++
error: val cannot be reassigned
a++
^
使用 var 声明可变变量
>>> var b = 1
>>> b = b + 1
>>> b
2
只要可能,尽量在Kotlin中首选使用val
不变值。因为事实上在程序中大部分地方只需要使用不可变的变量。使用val变量可以带来可预测的行为和线程安全等优点。
变量名就是标识符。标识符是由字母、数字、下划线组成的字符序列,不能以数字开头。下面是合法的变量名
>>> val _x = 1
>>> val y = 2
>>> val ip_addr = "127.0.0.1"
>>> _x
1
>>> y
2
>>> ip_addr
127.0.0.1
跟Java一样,变量名区分大小写。命名遵循驼峰式规范。
2.2 关键字与修饰符
通常情况下,编程语言中都有一些具有特殊意义的标识符是不能用作变量名的,这些具备特殊意义的标识符叫做关键字(又称保留字),编译器需要针对这些关键字进行词法分析,这是编译器对源码进行编译的基础步骤之一。
Kotlin中的修饰符关键字主要分为:
类修饰符、访问修饰符、型变修饰符、成员修饰符、参数修饰符、类型修饰符、函数修饰符、属性修饰符等。这些修饰符如下表2-1所示
表2-1 Kotlin中的修饰符
类修饰符
类修饰符 | 说明 |
---|---|
abstract | 抽象类 |
final | 不可被继承final类 |
enum | 枚举类 |
open | 可继承open类 |
annotation | 注解类 |
sealed | 密封类 |
data | 数据类 |
成员修饰符
成员修饰符 | 说明 |
---|---|
override | 重写函数(方法) |
open | 声明函数可被重写 |
final | 声明函数不可被重写 |
abstract | 声明函数为抽象函数 |
lateinit | 延迟初始化 |
访问权限修饰符
访问权限修饰符 | 说明 |
---|---|
private | 私有,仅当前类可访问 |
protected | 当前类以及继承该类的可访问 |
public | 默认值,对外可访问 |
internal | 整个模块内可访问(模块是指一起编译的一组 Kotlin 源代码文件。例如,一个 Maven 工程, 或 Gradle 工程,通过 Ant 任务的一次调用编译的一组文件等) |
协变逆变修饰符
协变逆变修饰符 | 说明 |
---|---|
in | 消费者类型修饰符,out T 等价于 ? extends T |
out | 生产者类型修饰符,in T 等价于 ? super T |
函数修饰符
函数修饰符 | 说明 |
---|---|
tailrec | 尾递归 |
operator | 运算符重载函数 |
infix | 中缀函数。例如,给Int定义扩展中缀函数 infix fun Int.shl(x: Int): Int |
inline | 内联函数 |
external | 外部函数 |
suspend | 挂起协程函数 |
属性修饰符
属性修饰符 | 说明 |
---|---|
const | 常量修饰符 |
参数修饰符
参数修饰符 | 说明 |
---|---|
vararg | 变长参数修饰符 |
noinline | 不内联参数修饰符,有时,只需要将内联函数的部分参数使用内联Lambda,其他的参数不需要内联,可以使用“noinline”关键字修饰。例如:inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit)
|
crossinline | 当内联函数不是直接在函数体中使用lambda参数,而是通过其他执行上下文。这种情况下可以在参数前使用“crossinline”关键字修饰标识。 |
代码实例如下。
crossinline代码实例:
inline fun f(crossinline body: () -> Unit) {
val f = object: Runnable {
override fun run() = body()
}
}
类型修饰符 | 说明 |
---|---|
reified | 具体化类型参数 |
除了上面的修饰符关键字之外,还有一些其他特殊语义的关键字如下表2-2所示
表2-2 Kotlin中的关键字
关键字 | 说明 |
---|---|
package | 包声明 |
as | 类型转换 |
typealias | 类型别名 |
class | 声明类 |
this | 当前对象引用 |
super | 父类对象引用 |
val | 声明不可变变量 |
var | 声明可变变量 |
fun | 声明函数 |
for | for 循环 |
null | 特殊值 null |
true | 真值 |
false | 假值 |
is | 类型判断 |
throw | 抛出异常 |
return | 返回值 |
break | 跳出循环体 |
continue | 继续下一次循环 |
object | 单例类声明 |
if | 逻辑判断if |
else | 逻辑判断, 结合if使用 |
while | while 循环 |
do | do 循环 |
when | 条件判断 |
interface | 接口声明 |
file | 文件 |
field | 成员 |
property | 属性 |
receiver | 接收者 |
param | 参数 |
setparam | 设置参数 |
delegate | 委托 |
import | 导入包 |
where | where条件 |
by | 委托类或属性 |
get | get函数 |
set | set 函数 |
constructor | 构造函数 |
init | 初始化代码块 |
try | 异常捕获 |
catch | 异常捕获,结合try使用 |
finally | 异常最终执行代码块 |
dynamic | 动态的 |
typeof | 类型定义,预留用 |
这些关键字定义在源码 org.jetbrains.kotlin.lexer.KtTokens.java 中。
2.3 流程控制语句
流程控制语句是编程语言中的核心之一。可分为:
分支语句(
if
、when
)
循环语句(for
、while
)
跳转语句 (return
、break
、continue
、throw
)
2.3.1 if表达式
if-else语句是控制程序流程的最基本的形式,其中else是可选的。
在 Kotlin 中,if 是一个表达式,即它会返回一个值(跟Scala一样)。
代码示例:
package com.easy.kotlin
fun main(args: Array<String>) {
println(max(1, 2))
}
fun max(a: Int, b: Int): Int {
// 表达式返回值
val max = if (a > b) a else b
return max
}
另外,if 的分支可以是代码块,最后的表达式作为该块的值:
fun max3(a: Int, b: Int): Int {
val max = if (a > b) {
print("Max is a")
a // 最后的表达式作为该代码块的值
} else {
print("Max is b")
b // 同上
}
return max
}
if作为代码块时,最后一行为其返回值。
另外,在Kotlin中没有类似true? 1: 0
这样的三元表达式。对应的写法是使用if else
语句:
if(true) 1 else 0
if-else语句规则:
- if后的括号不能省略,括号里表达式的值须是布尔型。
代码反例:
>>> if("a") 1
error: type mismatch: inferred type is String but Boolean was expected
if("a") 1
^
>>> if(1) println(1)
error: the integer literal does not conform to the expected type Boolean
if(1)
^
- 如果条件体内只有一条语句需要执行,那么if后面的大括号可以省略。良好的编程风格建议加上大括号。
>>> if(true) println(1) else println(0)
1
>>> if(true) { println(1)} else{ println(0)}
1
编程实例:用 if - else 语句判断某年份是否是闰年。
fun isLeapYear(year: Int): Boolean {
var isLeapYear: Boolean
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
isLeapYear = true
} else {
isLeapYear = false
}
return isLeapYear
}
fun main(args: Array<String>) {
println(isLeapYear(2017)) // false
println(isLeapYear(2020)) // true
}
2.3.2 when表达式
when表达式类似于 switch-case 表达式。when会对所有的分支进行检查直到有一个条件满足。但相比switch而言,when语句要更加的强大,灵活。
Kotlin的极简语法表达风格,使得我们对分支检查的代码写起来更加简单直接:
fun casesWhen(obj: Any?) {
when (obj) {
0,1,2,3,4,5,6,7,8,9 -> println("${obj} ===> 这是一个0-9之间的数字")
"hello" -> println("${obj} ===> 这个是字符串hello")
is Char -> println("${obj} ===> 这是一个 Char 类型数据")
else -> println("${obj} ===> else类似于Java中的 case-switch 中的 default")
}
}
fun main(args: Array<String>) {
casesWhen(1)
casesWhen("hello")
casesWhen('X')
casesWhen(null)
}
输出
1 ===> 这是一个0-9之间的数字
hello ===> 这个是字符串hello
X ===> 这是一个 Char 类型数据
null ===> else类似于Java中的 case-switch 中的 default
像 if 一样,when 的每一个分支也可以是一个代码块,它的值是块中最后的表达式的值。
如果其他分支都不满足条件会到 else 分支(类似default)。
如果我们有很多分支需要用相同的方式处理,则可以把多个分支条件放在一起,用逗号分隔:
0,1,2,3,4,5,6,7,8,9 -> println("${obj} ===> 这是一个0-9之间的数字")
我们可以用任意表达式(而不只是常量)作为分支条件
fun switch(x: Int) {
val s = "123"
when (x) {
-1, 0 -> print("x == -1 or x == 0")
1 -> print("x == 1")
2 -> print("x == 2")
8 -> print("x is 8")
parseInt(s) -> println("x is 123")
else -> { // 注意这个块
print("x is neither 1 nor 2")
}
}
}
我们也可以检测一个值在 in 或者不在 !in 一个区间或者集合中:
val x = 1
val validNumbers = arrayOf(1, 2, 3)
when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
else -> print("none of the above")
}
编程实例: 用when语句写一个阶乘函数。
fun fact(n: Int): Int {
var result = 1
when (n) {
0, 1 -> result = 1
else -> result = n * fact(n - 1)
}
return result
}
fact(10) // 3628800
2.3.3 for循环
for 循环可以对任何提供迭代器(iterator)的对象进行遍历,语法如下:
for (item in collection) {
print(item)
}
如果想要通过索引遍历一个数组或者一个 list,可以这么做:
for (i in array.indices) {
print(array[i])
}
或者使用库函数 withIndex
:
for ((index, value) in array.withIndex()) {
println("the element at $index is $value")
}
另外,范围(Ranges)表达式也可用于循环当中:
if (i in 1..10) { // 等同于1 <= i && i <= 10
println(i)
}
简写
(1..10).forEach { print(it) }
其中的操作符形式的 1..10 等价于 1.rangeTo(10) 函数调用 ,由in和!in进行连接。
编程实例: 编写一个 Kotlin 程序在屏幕上输出1!+2!+3!+……+10!的和。
我们使用上面的fact函数,代码实现如下
fun sumFact(n: Int): Int {
var sum = 0
for (i in 1..n) {
sum += fact(i)
}
return sum
}
sumFact(10) // 4037913
2.3.4 while循环
while 和 do .. while使用方式跟C、Java语言基本一致。
代码示例
package com.easy.kotlin
fun main(args: Array<String>) {
var x = 10
while (x > 0) {
x--
println(x)
}
var y = 10
do {
y = y + 1
println(y)
} while (y < 20) // y的作用域包含此处
}
2.3.5 break 和 continue
break
和continue
都是用来控制循环结构的,主要是用来停止循环(中断跳转),但是有区别,下面我们分别介绍。
break
break
用于完全结束一个循环,直接跳出循环体,然后执行循环后面的语句。
问题场景:
打印数字1-10,只要遇到偶数就结束打印。
代码示例:
for (i in 1..10) {
println(i)
if (i % 2 == 0) {
break
}
} // break to here
输出:
1
2
continue
continue
是只终止本轮循环,但是还会继续下一轮循环。可以简单理解为,直接在当前语句处中断,跳转到循环入口,执行下一轮循环。而break
则是完全终止循环,跳转到循环出口。
问题场景:
打印1-10中的奇数。
代码示例:
for (i in 1..10) {
if (i % 2 == 0) {
continue
}
println(i)
}
输出
1
3
5
7
9
2.3.6 return返回
在Java、C语言中,return语句使我们再常见不过的了。虽然在Scala,Groovy这样的语言中,函数的返回值可以不需要显示用return来指定,但是我们仍然认为,使用return的编码风格更加容易阅读理解 (尤其是在分支流代码块中)。
在Kotlin中,除了表达式的值,有返回值的函数都要求显式使用return
来返回其值。
代码示例
fun sum(a: Int,b: Int): Int{
return a+b // 这里的return不能省略
}
fun max(a: Int, b: Int): Int {
if (a > b){
return a // return不能省略
} else{
return b // return不能省略
}
我们在Kotlin中,可以直接使用=
符号来直接返回一个函数的值,这样的函数我们称为函数字面量。
代码示例
>>> fun sum(a: Int,b: Int) = a + b
>>> fun max(a: Int, b: Int) = if (a > b) a else b
>>> sum(1,10)
11
>>> max(1,2)
2
>>> val sum=fun(a:Int, b:Int) = a+b
>>> sum
(kotlin.Int, kotlin.Int) -> kotlin.Int
>>> sum(1,1)
2
后面的函数体语句有没有大括号 {}
意思完全不同。加了大括号,意义就完全不一样了。
>>> val sumf = fun(a:Int, b:Int) = {a+b}
>>> sumf
(kotlin.Int, kotlin.Int) -> () -> kotlin.Int
>>> sumf(1,1)
() -> kotlin.Int
>>> sumf(1,1).invoke()
2
我们再通过下面的代码示例清晰的看出:
>>> fun sumf(a:Int,b:Int) = {a+b}
>>> sumf(1,1)
() -> kotlin.Int
>>> sumf(1,1).invoke()
2
>>> fun maxf(a:Int, b:Int) = {if(a>b) a else b}
>>> maxf(1,2)
() -> kotlin.Int
>>> maxf(1,2).invoke()
2
可以看出,sumf
,maxf
的返回值是函数类型:
() -> kotlin.Int
() -> kotlin.Int
这点跟Scala是不同的。在Scala中,带不带大括号{}
,意思一样:
scala> def maxf(x:Int, y:Int) = { if(x>y) x else y }
maxf: (x: Int, y: Int)Int
scala> def maxv(x:Int, y:Int) = if(x>y) x else y
maxv: (x: Int, y: Int)Int
scala> maxf(1,2)
res4: Int = 2
scala> maxv(1,2)
res6: Int = 2
我们可以看出maxf: (x: Int, y: Int)Int
跟maxv: (x: Int, y: Int)Int
签名是一样的。在这里,Kotlin跟Scala在大括号的使用上,是完全不同的。
然后,调用函数方式是直接调用invoke()
函数:sumf(1,1).invoke()。
kotlin 中 return
语句会从最近的函数或匿名函数中返回,但是在Lambda表达式中遇到return,则直接返回最近的外层函数。例如下面两个函数是不同的:
val intArray = intArrayOf(1, 2, 3, 4, 5)
intArray.forEach {
if (it == 3) return // 在Lambda表达式中的return 直接返回最近的外层函数
println(it)
}
输出:
1
2
遇到 3 时会直接返回(有点类似循环体中的break
行为)。
而我们给forEach传入一个匿名函数 fun(a: Int) ,这个匿名函数里面的return不会跳出forEach循环,有点像continue的逻辑:
val intArray = intArrayOf(1, 2, 3, 4, 5)
intArray.forEach(fun(a: Int) {
if (a == 3) return // 从最近的函数中返回
println(a)
})
输出
1
2
4
5
为了显式的指明 return
返回的地址,kotlin 还提供了 @Label
(标签) 来控制返回语句,且看下节分解。
2.3.7 标签(label)
在 Kotlin 中任何表达式都可以用标签(label)来标记。 标签的格式为标识符后跟 @
符号,例如:abc@
、_isOK@
都是有效的标签。我们可以用Label标签来控制 return
、break
或 continue
的跳转(jump)行为。
代码示例:
val intArray = intArrayOf(1, 2, 3, 4, 5)
intArray.forEach here@ {
if (it == 3) return@here // 指令跳转到 lambda 表达式标签 here@ 处。继续下一个it=4的遍历循环
println(it)
}
输出:
1
2
4
5
我们在 lambda 表达式开头处添加了标签here@
,我们可以这么理解:该标签相当于是记录了Lambda表达式的指令执行入口地址, 然后在表达式内部我们使用return@here
来跳转至Lambda表达式该地址处。这样代码更加易懂。
另外,我们也可以使用隐式标签更方便。 该标签与接收该 lambda 的函数同名。
代码示例
val intArray = intArrayOf(1, 2, 3, 4, 5)
intArray.forEach {
if (it == 3) return@forEach // 返回到 @forEach 处继续下一个循环
println(it)
}
输出:
1
2
4
5
接收该Lambda表达式的函数是forEach, 所以我们可以直接使用 return@forEach
,来跳转到此处执行下一轮循环。
2.3.8 throw表达式
在 Kotlin 中 throw 是表达式,它的类型是特殊类型 Nothing。 该类型没有值。跟C、Java中的void
意思一样。
>>> Nothing::class
class java.lang.Void
我们在代码中,用 Nothing 来标记无返回的函数:
>>> fun fail(msg:String):Nothing{ throw IllegalArgumentException(msg) }
>>> fail("XXXX")
java.lang.IllegalArgumentException: XXXX
at Line57.fail(Unknown Source)
另外,如果把一个throw表达式的值赋值给一个变量,需要显式声明类型为Nothing
, 代码示例如下
>>> val ex = throw Exception("YYYYYYYY")
error: 'Nothing' property type needs to be specified explicitly
val ex = throw Exception("YYYYYYYY")
^
>>> val ex:Nothing = throw Exception("YYYYYYYY")
java.lang.Exception: YYYYYYYY
另外,因为ex变量是Nothing类型,没有任何值,所以无法当做参数传给函数。
2.4 操作符与重载
Kotlin 允许我们为自己的类型提供预定义的一组操作符的实现。这些操作符具有固定的符号表示(如 +
或 *
)和固定的优先级。这些操作符的符号定义如下:
KtSingleValueToken LBRACKET = new KtSingleValueToken("LBRACKET", "[");
KtSingleValueToken RBRACKET = new KtSingleValueToken("RBRACKET", "]");
KtSingleValueToken LBRACE = new KtSingleValueToken("LBRACE", "{");
KtSingleValueToken RBRACE = new KtSingleValueToken("RBRACE", "}");
KtSingleValueToken LPAR = new KtSingleValueToken("LPAR", "(");
KtSingleValueToken RPAR = new KtSingleValueToken("RPAR", ")");
KtSingleValueToken DOT = new KtSingleValueToken("DOT", ".");
KtSingleValueToken PLUSPLUS = new KtSingleValueToken("PLUSPLUS", "++");
KtSingleValueToken MINUSMINUS = new KtSingleValueToken("MINUSMINUS", "--");
KtSingleValueToken MUL = new KtSingleValueToken("MUL", "*");
KtSingleValueToken PLUS = new KtSingleValueToken("PLUS", "+");
KtSingleValueToken MINUS = new KtSingleValueToken("MINUS", "-");
KtSingleValueToken EXCL = new KtSingleValueToken("EXCL", "!");
KtSingleValueToken DIV = new KtSingleValueToken("DIV", "/");
KtSingleValueToken PERC = new KtSingleValueToken("PERC", "%");
KtSingleValueToken LT = new KtSingleValueToken("LT", "<");
KtSingleValueToken GT = new KtSingleValueToken("GT", ">");
KtSingleValueToken LTEQ = new KtSingleValueToken("LTEQ", "<=");
KtSingleValueToken GTEQ = new KtSingleValueToken("GTEQ", ">=");
KtSingleValueToken EQEQEQ = new KtSingleValueToken("EQEQEQ", "===");
KtSingleValueToken ARROW = new KtSingleValueToken("ARROW", "->");
KtSingleValueToken DOUBLE_ARROW = new KtSingleValueToken("DOUBLE_ARROW", "=>");
KtSingleValueToken EXCLEQEQEQ = new KtSingleValueToken("EXCLEQEQEQ", "!==");
KtSingleValueToken EQEQ = new KtSingleValueToken("EQEQ", "==");
KtSingleValueToken EXCLEQ = new KtSingleValueToken("EXCLEQ", "!=");
KtSingleValueToken EXCLEXCL = new KtSingleValueToken("EXCLEXCL", "!!");
KtSingleValueToken ANDAND = new KtSingleValueToken("ANDAND", "&&");
KtSingleValueToken OROR = new KtSingleValueToken("OROR", "||");
KtSingleValueToken SAFE_ACCESS = new KtSingleValueToken("SAFE_ACCESS", "?.");
KtSingleValueToken ELVIS = new KtSingleValueToken("ELVIS", "?:");
KtSingleValueToken QUEST = new KtSingleValueToken("QUEST", "?");
KtSingleValueToken COLONCOLON = new KtSingleValueToken("COLONCOLON", "::");
KtSingleValueToken COLON = new KtSingleValueToken("COLON", ":");
KtSingleValueToken SEMICOLON = new KtSingleValueToken("SEMICOLON", ";");
KtSingleValueToken DOUBLE_SEMICOLON = new KtSingleValueToken("DOUBLE_SEMICOLON", ";;");
KtSingleValueToken RANGE = new KtSingleValueToken("RANGE", "..");
KtSingleValueToken EQ = new KtSingleValueToken("EQ", "=");
KtSingleValueToken MULTEQ = new KtSingleValueToken("MULTEQ", "*=");
KtSingleValueToken DIVEQ = new KtSingleValueToken("DIVEQ", "/=");
KtSingleValueToken PERCEQ = new KtSingleValueToken("PERCEQ", "%=");
KtSingleValueToken PLUSEQ = new KtSingleValueToken("PLUSEQ", "+=");
KtSingleValueToken MINUSEQ = new KtSingleValueToken("MINUSEQ", "-=");
KtKeywordToken NOT_IN = KtKeywordToken.keyword("NOT_IN", "!in");
KtKeywordToken NOT_IS = KtKeywordToken.keyword("NOT_IS", "!is");
KtSingleValueToken HASH = new KtSingleValueToken("HASH", "#");
KtSingleValueToken AT = new KtSingleValueToken("AT", "@");
KtSingleValueToken COMMA = new KtSingleValueToken("COMMA", ",");
2.4.1 操作符优先级
Kotlin中操作符的优先级(Precedence)如下表所示
表2-3 操作符的优先级
优先级 | 标题 | 符号 |
---|---|---|
最高 | 后缀(Postfix ) |
++ , -- , . , ?. , ?
|
前缀(Prefix) |
- , + , ++ , -- , ! , labelDefinition @
|
|
右手类型运算(Type RHS,right-hand side class type (RHS) ) |
: , as , as?
|
|
乘除取余(Multiplicative) |
* , / , %
|
|
加减(Additive ) |
+ , -
|
|
区间范围(Range) | .. |
|
Infix函数 | 例如,给Int 定义扩展 infix fun Int.shl(x: Int): Int {...} ,这样调用 1 shl 2 ,等同于1.shl(2)
|
|
Elvis操作符 | ?: |
|
命名检查符(Named checks) |
in , !in , is , !is
|
|
比较大小(Comparison) |
< , > , <= , >=
|
|
相等性判断(Equality) |
== , != , === , !==
|
|
与 (Conjunction) | && |
|
或 (Disjunction) | || |
|
最低 | 赋值(Assignment) |
= , += , -= , *= , /= , %=
|
为实现这些的操作符,Kotlin为二元操作符左侧的类型和一元操作符的参数类型,提供了相应的函数或扩展函数。重载操作符的函数需要用 operator
修饰符标记。中缀操作符的函数使用infix
修饰符标记。
2.4.2 一元操作符
一元操作符(unary operation) 有前缀操作符、递增和递减操作符等。
前缀操作符
前缀操作符放在操作数的前面。它们分别如表2-4所示
表2-4 前缀操作符
表达式 | 翻译为 |
---|---|
+a |
a.unaryPlus() |
-a |
a.unaryMinus() |
!a |
a.not() |
以下是重载一元减运算符的示例:
package com.easy.kotlin
data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus() = Point(-x, -y)
测试代码:
package com.easy.kotlin
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class OperatorDemoTest {
@Test
fun testPointUnaryMinus() {
val p = Point(1, 1)
val np = -p
println(np) //Point(x=-1, y=-1)
}
}
递增和递减操作符
表2-5 递增和递减操作符
表达式 | 翻译为 |
---|---|
a++ |
a.inc() 返回值是a
|
a-- |
a.dec() 返回值是a
|
++a |
a.inc() 返回值是a+1
|
--a |
a.dec() 返回值是a-1
|
inc()
和 dec()
函数必须返回一个值,它用于赋值给使用
++
或 --
操作的变量。
2.4.3 二元操作符
Kotlin中的二元操作符有算术运算符、索引访问操作符、调用操作符、计算并赋值操作符、相等与不等操作符、Elvis 操作符、比较操作符、中缀操作符等。下面我们分别作介绍。
算术运算符
表2-6 算术运算符
表达式 | 翻译为 |
---|---|
a + b |
a.plus(b) |
a - b |
a.minus(b) |
a * b |
a.times(b) |
a / b |
a.div(b) |
a % b |
a.rem(b) 、 a.mod(b)
|
a..b |
a.rangeTo(b) |
代码示例
>>> val a=10
>>> val b=3
>>> a+b
13
>>> a-b
7
>>> a/b
3
>>> a%b
1
>>> a..b
10..3
>>> b..a
3..10
字符串的+
运算符重载
先用代码举个例子:
>>> ""+1
1
>>> 1+""
error: none of the following functions can be called with the arguments supplied:
public final operator fun plus(other: Byte): Int defined in kotlin.Int
public final operator fun plus(other: Double): Double defined in kotlin.Int
public final operator fun plus(other: Float): Float defined in kotlin.Int
public final operator fun plus(other: Int): Int defined in kotlin.Int
public final operator fun plus(other: Long): Long defined in kotlin.Int
public final operator fun plus(other: Short): Int defined in kotlin.Int
1+""
^
从上面的示例,我们可以看出,在Kotlin中1+""
是不允许的(这地方,相比Scala,写这样的Kotlin代码就显得不大友好),只能显式调用toString
来相加:
>>> 1.toString()+""
1
自定义重载的 +
运算符
下面我们使用一个计数类 Counter 重载的 +
运算符来增加index的计数值。
代码示例
data class Counter(var index: Int)
operator fun Counter.plus(increment: Int): Counter {
return Counter(index + increment)
}
测试类
package com.easy.kotlin
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class OperatorDemoTest
@Test
fun testCounterIndexPlus() {
val c = Counter(1)
val cplus = c + 10
println(cplus) //Counter(index=11)
}
}
in
操作符
表2-7 in操作符
表达式 | 翻译为 |
---|---|
a in b |
b.contains(a) |
a !in b |
!b.contains(a) |
in操作符等价于函数contains 。
索引访问操作符
表2-8 索引访问操作符操作符
表达式 | 翻译为 |
---|---|
a[i] |
a.get(i) |
a[i] = b |
a.set(i, b) |
方括号转换为调用带有适当数量参数的 get
和 set
。
调用操作符
表2-9 调用操作符
表达式 | 翻译为 |
---|---|
a() |
a.invoke() |
a(i) |
a.invoke(i) |
圆括号转换为调用带有适当数量参数的 invoke
。
计算并赋值操作符
表2-10 计算并赋值操作符
表达式 | 翻译为 |
---|---|
a += b |
a.plusAssign(b) |
a -= b |
a.minusAssign(b) |
a *= b |
a.timesAssign(b) |
a /= b |
a.divAssign(b) |
a %= b |
a.modAssign(b) |
对于赋值操作,例如 a += b
,编译器会试着生成 a = a + b
的代码(这里包含类型检查:a + b
的类型必须是 a
的子类型)。
相等与不等操作符
Kotlin 中有两种类型的相等性:
- 引用相等
===
!==
(两个引用指向同一对象) - 结构相等
==
!=
( 使用equals()
判断)
表2-11 相等与不等操作符
表达式 | 翻译为 |
---|---|
a == b |
a?.equals(b) ?: (b === null) |
a != b |
!(a?.equals(b) ?: (b === null)) |
这个 ==
操作符有些特殊:它被翻译成一个复杂的表达式,用于筛选 null
值。
意思是:如果 a 不是 null 则调用 equals(Any?)
函数并返回其值;否则(即 a === null
)就计算 b === null
的值并返回。
当与 null 显式比较时,a == null
会被自动转换为 a=== null
注意:===
和 !==
不可重载。
Elvis 操作符 ?:
在Kotin中,Elvis操作符特定是跟null比较。也就是说
y = x?:0
等价于
val y = if(x!==null) x else 0
主要用来作null
安全性检查。
Elvis操作符 ?:
是一个二元运算符,如果第一个操作数为真,则返回第一个操作数,否则将计算并返回其第二个操作数。它是三元条件运算符的变体。命名灵感来自猫王的发型风格。
Kotlin中没有这样的三元运算符 true?1:0
,取而代之的是if(true) 1 else 0
。而Elvis操作符算是精简版的三元运算符。
我们在Java中使用的三元运算符的语法,你通常要重复变量两次, 示例:
String name = "Elvis Presley";
String displayName = (name != null) ? name : "Unknown";
取而代之,你可以使用Elvis操作符
String name = "Elvis Presley";
String displayName = name?:"Unknown"
我们可以看出,用Elvis操作符(?:)可以把带有默认值的if/else结构写的及其短小。用Elvis操作符不用检查null(避免了NullPointerException
),也不用重复变量。
这个Elvis操作符功能在Spring 表达式语言 (SpEL)中提供。
在Kotlin中当然就没有理由不支持这个特性。
代码示例:
>>> val x = null
>>> val y = x?:0
>>> y
0
>>> val x = false
>>> val y = x?:0
>>> y
false
>>> val x = ""
>>> val y = x?:0
>>> y
>>> val x = "abc"
>>> val y = x?:0
>>> y
abc
比较操作符
表2-12 比较操作符
表达式 | 翻译为 |
---|---|
a > b |
a.compareTo(b) > 0 |
a < b |
a.compareTo(b) < 0 |
a >= b |
a.compareTo(b) >= 0 |
a <= b |
a.compareTo(b) <= 0 |
所有的比较都转换为对 compareTo
的调用,这个函数需要返回 Int
值
用infix函数自定义中缀操作符
我们可以通过自定义infix函数来实现中缀操作符。
代码示例
data class Person(val name: String, val age: Int)
infix fun Person.grow(years: Int): Person {
return Person(name, age + years)
}
测试代码
package com.easy.kotlin
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class InfixFunctionDemoTest {
@Test fun testInfixFuntion() {
val person = Person("Jack", 20)
println(person.grow(2))
println(person grow 2)
}
}
输出
Person(name=Jack, age=22)
Person(name=Jack, age=22)
2.5 包声明
我们在*.kt
源文件开头声明package
命名空间。例如在PackageDemo.kt源代码中,我们按照如下方式声明包
package com.easy.kotlin
fun what(){ // 包级函数
println("This is WHAT ?")
}
fun main(args:Array<String>){ // 一个包下面只能有一个main函数
println("Hello,World!")
}
class Motorbike{ // 包里面的类
fun drive(){
println("Drive The Motorbike ...")
}
}
Kotlin中的目录与包的结构无需匹配,源代码文件可以在文件系统中的任意位置。
如果一个测试类PackageDemoTest跟PackageDemo在同一个包下面,我们就不需要单独去import 类和包级函数,可以在代码里直接调用
package com.easy.kotlin
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class PackageDemoTest {
@Test
fun testWhat() {
what()
}
@Test
fun testDriveMotorbike(){
val motorbike = Motorbike()
motorbike.drive()
}
}
其中,what()
函数跟PackageDemoTest
类在同一个包命名空间下,可以直接调用,不需要 import
。Motorbike
类跟PackageDemoTest
类同理分析。
如果不在同一个package下面,我们就需要import对应的类和函数。例如,我们在 src/test/kotlin
目录下新建一个package com.easy.kotlin.test
, 使用package com.easy.kotlin
下面的类和函数,示例如下
package com.easy.kotlin.test
import com.easy.kotlin.Motorbike // 导入类Motorbike
import com.easy.kotlin.what // 导入包级函数what
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class PackageDemoTest {
@Test
fun testWhat() {
what()
}
@Test
fun testDriveMotorbike() {
val motorbike = Motorbike()
motorbike.drive()
}
}
Kotlin会会默认导入一些基础包到每个 Kotlin 文件中:
kotlin.*
kotlin.annotation.*
kotlin.collections.*
kotlin.comparisons.* (自 1.1 起)
kotlin.io.*
kotlin.ranges.*
kotlin.sequences.*
kotlin.text.*
根据目标平台还会导入额外的包:
JVM:
java.lang.*
kotlin.jvm.*
JS:
kotlin.js.*
本章小结
Kotlin 开发者社区
国内第一Kotlin 开发者社区公众号,主要分享、交流 Kotlin 编程语言、Spring Boot、Android、React.js/Node.js、函数式编程、编程思想等相关主题。