第2章 Kotlin 语法基础第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.*

本章小结

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏老九学堂

【必读】超全的C语言基础知识大全

我们用一个简单的c程序例子,介绍c语言的基本构成、格式、以及良好的书写风格,加深小伙伴们对C语言的认识。

39820
来自专栏开发之途

重拾Java(0)-基础知识点

33140
来自专栏程序员与猫

常见的正则表达式解读释义

又名:写给正在学习前端的女友看的常见正则表达式解读。原文发布于 https://www.zybuluo.com/wddpct/note/728081 。 ...

22490
来自专栏向治洪

Kotlin之基本语法

在今年Google IO大会上Google已经明确kotlin作为为Android第一官方语言的地位。我相信Google的决意,就像当初毫不犹豫的抛弃eclip...

26870
来自专栏静默虚空的博客

[Java 基础]运算符和表达式

Java运算符 (1)赋值操作符 赋值操作符(=)表示:取右边的值(即右值),把它复制给左边(即左值)。 右值可以是任意的常量、变量或表达式(只要可以生成一个值...

19990
来自专栏向治洪

Kotlin语法基础之运算符

运算符 计算机程序中最小的程序单位成为表达式,每个表达式都可以由两部分组成,即操作数和运算符。操作数可以是变量、常量、类、数组、方法等,甚至是其他表达式。而运算...

32950
来自专栏一个会写诗的程序员的博客

第3章 Kotlin 可空类型与类型系统第3章 Kotlin 可空类型与类型系统

我们在编程语言中使用类型的目的是为了让编译器能够确定类型所关联的对象需要分配多少空间。

14520
来自专栏老九学堂

【必读】C语言基础知识大全

C语言程序的结构认识 用一个简单的c程序例子,介绍c语言的基本构成、格式、以及良好的书写风格,使小伙伴对c语言有个初步认识。 例1:计算两个整数之和的c程...

89080
来自专栏Golang语言社区

Go 语言的基本数据类型

0)变量声明 var 变量名字 类型 = 表达式 例: var num int = 10 复制代码 其中“类型”或“= 表达式”两个部分可以省略其中的一个。 1...

438110
来自专栏Android群英传

Swift vs. Kotlin 漫谈系列之类与继承

23340

扫码关注云+社区

领取腾讯云代金券