Kotlin学习(一)基本语法

Kotlin是JetBrains开发的一种更高级的语言,可以同java无缝对接,也就是说用kotlin写的代码可以直接调用已有的java库。目前Kotlin越来越流行了,大有代替java的势头。前一段时间简单学习了一下kotlin的使用,一段时间没用感觉忘得差不多了。下面就系统来学习一下吧。学习的参考内容来自于kotlin的官网

定义包名

包名应该定义为源文件的最上面, 所有其它的内容如函数或者类等都要在包名的下面。如下所示,方法baz()的全名就是foo.bar.baz(),Goo的全名就是foo.bar.Goo。如果没有指定包名,则包名默认为没有包名。

1
2
3
4
5
package foo.bar

fun baz() {}

class Goo {}

既然有了包名就可以根据包名来引入内容,这部分同java基本相同,如

1
2
import foo.bar
import foo.*

不同之处就是我们可以使用as关键字给引入的包命名一个别名,如

1
import foo.bar as bBar

这样我们在程序中就可以使用bBar来代替bar。
另外import还可以引入一些其它的东西,如:

定义方法

方法的定义以fun关键字开头,可以有多种形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//定义了一个返回值为Int,并且有两个Int参数的函数
fun sum(a: Int, b: Int): Int {
return a + b
}

//上面方法的简写
fun sum(a: Int, b: Int) = a + b

//没有返回值的函数
fun printSum(a: Int, b: Int): Unit {
print(a + b)
}

//上面方法的简写,没有返回值则Unit可以省略
fun printSum(a: Int, b: Int) {
print(a + b)
}

定义变量和常量

对于变量,我们使用var关键字定义,而对于常量则使用val关键字。对于变量和常量我们需要指定其类型,如果在定义的时候直接初始化,则kotlin可以直接根据你赋值的类型推断出变量的类型;这时,对于类型的指定就可以省略。

1
2
3
4
5
6
7
val a: Int = 1
val b = 1 // `Int` 类型可以推断出来
val c: Int // 没有初始化的话需要指定其类型
c = 1 // 指定值

var x = 5 // `Int` 类型可以推断出来
x += 1

字符串模板

在字符串中可以使用模板表达式,一个模板表达式以美元符号$开头。模板表达式可以让我们很容易的根据变量生成我们想要字符串。

1
2
val i = 10
val s = "i = $i" // s的值为"i=10"

我们甚至可以在模板表达式里面调用变量的方法:

1
2
val s = "abc"
val str = "$s.length is ${s.length}" // str的值为"abc.length is 3"

如果在log中使用字符串模块,那简直棒极了,再也不用各种拼接字符串了。
但是如果字符中要输出美元符号怎么办?可以如下这样

1
2
3
val price = """
${'$'}9.99 //price的值为"$9.99"
"""

条件表达式

同java一样,对条件的判断也是使用if else。不同之处是if else可以直接作为一个表达式赋值给一个变量或者定义为一个方法。

1
2
3
4
//定义一个方法
fun max(a: Int, b: Int) = if (a > b) a else b
//定义一个变量
val max = if (a > b) a else b

null检查

相信所有使用java的同学都会对NullPointerException深恶疼绝,因为绝大部分的系统崩溃都是它引起来的,然后不得不在程序中加了一堆的if(xxx!=null)之类的条件判定语句。

在android studio中选中一个变量,按快捷键ctrl+alt+t,再按4就会自动加上对该变量是否为空的检查

好消息是在Kotlin中我们基本上可以摆脱NullPointerException的纠缠了。如果我们定义个变量,编译器默认其就非空的,自动给我们做检查。如

1
2
var a: String = "abc"
a = null // 编译错误

对于允许为null的变量,我们需要加上关键字?来告诉编译器

1
2
var b: String? = "abc"
b = null // ok

当我们访问变量a的时候,我们就可以完全相信a不为空,因此不需要对a进行空指针的检查,但是b还是可能为空的。

1
2
3
4
//完全OK
val l = a.length
//当b为空的时候仍然会出现NullPointerException
val l = b.length

所以,再访问可以为空的变量时,我们仍然需要使用if(b!=null)来对其进行检查,但是Kotlin提供了一种更加简单的写法,就是使用?.
如仍然是对上面变量b的访问,我们可以使用

1
b?.length

这样当b为null的时候不会抛出NullPointerException,而是直接返回一个null;当b不为null的时候返回b.length,是不是比使用if进行判断简单多了呢?
再更深入一点,如果在java中一个类A有一个类型为B的成员变量b,而B中有一个字符串成员变量c, 我们有了一个A类型的对象a, 当我们要访问c的时候要如何进行null的判断呢?

1
2
3
4
String s = null;
if(a! = null && a.b! == null) {
s = a.b.c
}

是不是很麻烦?如果这种嵌套在深一点对程序员来讲简直是一种折磨,但是在Kotlin中就非常简单了。

1
val s = a?.b?.c

二目运算符(Elvis Operator)

我们都只知道java和c等编程语言中都有三目运算符,其实就是if else一种简写,如在java中判断一个字符串a是否为null,不为null就赋值给b,否则将空字符在赋值给b:

1
String b = (a != null) ? a : "";

在kotlin中也有类似的运算符?:,将一个表达式分为前后两个部分,前面的部分是进行null判断并且非null时的赋值,后面部分是当前面部分为null时的赋值,所以我们就暂时叫它二目运算符吧。上面的例子在kotlin中可以这样写:

1
val b = a ?: ""

是不是十分方便?如果用在对函数参数的判断上也是十分的方便的。

1
2
3
4
5
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ...
}

!!运算符

当我们直接使用可以为空的引用时,需要加上!!运算符,如b!!, kotlin会自动对b的值进行判断,当b不为null的时候返回b,而b为空的时候会自动帮你抛出了一个NullPointerException。

1
val l = b!!.length()

类型检查及自动类型转化

同java一样,kotlin使用关键字is进行类型的检查,但是在一些特定的情况下使用is对一个变量进行类型检查后会自动将其转化为对应的类型,如下面的这个例子:

1
2
3
if (obj is String) {
print(obj.length)
}

当obj使用is关键字进行类型检查是String类型后,obj就自动被转化成了String类型,因此可以直接调用其length方法。自动类型转化不仅可以用在if里面,还可以用 ||, &&, when, while等操作符里面,如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// x会被自动转化为String
if (x !is String) return
print(x.length)

// 在`||`操作符的右边,x会被自动转化为String
if (x !is String || x.length == 0) return

// 在`&&`操作符的右边,x会被自动转化为String
if (x is String && x.length > 0)
print(x.length)

//每种类型判断成功后,都会自动转化为对应的类型
when (x) {
is Int -> print(x + 1)
is String -> print(x.length + 1)
is IntArray -> print(x.sum())
}

自动类型转化似乎很好用,可以大大地减少我们的代码量,但是当编译器不能确定变量的类型时就不能应用自动转化了。自动类型转化的规则如下:

  • val类型的局部常量 - 可以自动转化;
  • val类型的属性 - 只有当该属性是private或者internal或者类型检查和属性的声明是在同一个module里面的时候才可以自动转化。 对于公有的属性和那些有getter方法的属性,自动转化是不能用的;
  • var类型的局部变量 - 需要同时满足这两个条件才可以自动转化:类型检查和使用之间变量没有改变;变量没有在一个lambda里面被改变;
  • var类型的属性 - 不可以转化 (因为变量的类型可能会被别的代码改变).

除了自动类型转化,我们还可以使用关键字as对一个变量进行类型转化,如:

1
val x: String = y as String

但是当y为null的时候,上面的类型转化就会抛出异常了,所以我们可以改进一下:

1
val x: String? = y as String?

如果当类型转化失败的时候就会抛出ClassCastException,这也是我们经常会遇到的问题。这时我们可以使用关键字as?进行类型转化,失败的时候会直接返回null而不是抛出异常:

1
val x: String? = y as? String

循环

循环是使用for或者while来实现的,同java的用法一样,这里就不多说了。

when

kotlin使用when来代替了switch,并且比switch更加地强大。基本的用法如下:

1
2
3
4
5
6
7
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { // Note the block
print("x is neither 1 nor 2")
}
}

需要注意的是除非是各个分支能包括所有的条件,否则else分支是必须的。when不仅可以用来做各种条件的判断,还可以作为一个表达式并且有返回值,返回值就是满足条件的那条分支的值。而且,由于使用了上文的自动类型转化,在满足条件的分支上不需要做额外的工作就可以直接使用对应的类型。如下面所示,满足条件 is String的时候我们就可以直接调用x的startWith方法,因为x已经被自动转化为String类型了。而hasPrefix的值就是when作为一个表达式最终返回的值。

1
2
3
4
 val hasPrefix = when(x) {
is String -> x.startsWith("prefix")
else -> false
}

另外,when的分支判断条件也极大地扩展了,不仅仅局限于单个数字,可以是用逗号分隔开的多个数字;可以是一个range范围;或者是一个表达式等等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//逗号分隔开的多个数字
when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}

//一个表达式
when (x) {
parseInt(s) -> print("s encodes x")
else -> print("s does not encode x")
}

//在一个range范围内或外
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")
}

相信大家都写过长长的if…else if…的表达式吧?使用when也可以将其替代:

1
2
3
4
5
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}

范围操作符

范围操作符可以用来做迭代操作,是用in来实现的。基本的使用如下:

1
2
3
if (i in 1..10) { // 等同于 1 <= i && i <= 10
println(i)
}

如果要倒序迭代,可以使用downTo操作符:

1
for (i in 4 downTo 1) print(i) // prints "4321"

我们还可以控制迭代的步长,使用step:

1
2
3
for (i in 1..4 step 2) print(i) // prints "13"

for (i in 4 downTo 1 step 2) print(i) // prints "42"

collections

我们同样可以迭代一个collections:

1
2
for (name in names)
println(name)

或者用来检测collections是否包含某个元素:

1
2
if (text in names) // names.contains(text) is called
print("Yes")

对于collections还可以使用lambda进行filter和map等操作:

1
2
3
4
5
names
.filter { it.startsWith("A") }
.sortedBy { it }
.map { it.toUpperCase() }
.forEach { print(it) }

怎么感觉跟rxjava很像啊?看来天下编程语言也是一大抄啊,也不知道到底是谁抄谁。好处就是如果我们了解rxjava的话,理解上面的代码也就毫无压力了。