第2章 Kotlin 语法基础

第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 流程控制语句

流程控制语句是编程语言中的核心之一。可分为:

分支语句(ifwhen)
循环语句(forwhile )
跳转语句 (returnbreakcontinuethrow)

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

breakcontinue都是用来控制循环结构的,主要是用来停止循环(中断跳转),但是有区别,下面我们分别介绍。

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

可以看出,sumfmaxf的返回值是函数类型:

() -> 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)Intmaxv: (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标签来控制 returnbreakcontinue的跳转(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)

方括号转换为调用带有适当数量参数的 getset

调用操作符

表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类在同一个包命名空间下,可以直接调用,不需要 importMotorbike类跟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、函数式编程、编程思想等相关主题。

开发者社区 QRCode.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容