Kotlin 语言由程序语言开发工具的知名供应商 JetBrains 构思于 2010 年,它是一种针对 Java 平台的新编程语言 (基于 JVM 的语言)。Kotlin 简洁、安全、务实,并且专注于与 Java 代码的互操作性。它几乎可以用在现在 Java 使用的任何地方:服务端开发、Android 应用等等。Kotlin 可以很好地和所有现存的 Java 库和框架一起工作,且性能水平和 Java 旗鼓相当,同时作为一种新语言,它包含了许多新的特性,由此也决定着 Kotlin 的代码风格。本文先来学习下 Kotlin 的基础语法,包括变量、函数、流程控制和智能转换等,这些基础语法是程序最基本的元素。
本节来学习怎样用 Kotlin 声明开发过程中经常使用的一些程序最基本的要素:函数声明、变量、基础类型、流程控制、类、枚举以及属性、智能转换和异常处理等。
先来回顾一段经典代码,还记得刚开始学 Java 时怎么打印输出 Hello World ! 的吗?下面使用 Kotlin 语言来实现,代码如下:
fun main(args: Array<String>) {
println("Hello World!")
}
这么简洁?是的相比于 Java 代码的实现,Kotlin 就是这么简洁。运行这段代码,最快的方式就是使用 Kotlin 官方提供的在线工具 Playground。不过还是推荐使用 IntelliJ IDEA,这是 Kotlin 官方提供的集成开发工具,也是世界上最好的 IDE 之一。
在 Kotlin 中函数的声明与 Java 有所不同,来看一段示例代码:
/*
关键字 函数名称 参数类型 返回值类型
↓ ↓ ↓ ↓ */
fun helloFun(name: String): String {
return "Hello $name !"
}/* ↑
花括号内为:函数体 - 代码块体
*/
上面的函数体是由单个表达式构成的,因此可以用这个表达式作为完整的函数体,并去掉花括号和 return 语句,直接使用 “=” 连接,将其变成一种类似变量赋值的函数形式,简化后代码如下:
/*
关键字 函数名称 参数类型 返回值类型 表达式体
↓ ↓ ↓ ↓ ↓ */
fun helloFun(name: String): String = "Hello $name !"
如果函数体写在花括号中,则表示这个函数有代码块体。如果函数直接返回了一个表达式,则表示其有表达式体,也称其为单一表达式函数。
此外,由于 Kotlin 支持类型推导,在使用单一表达式函数形式的时候,返回值的类型也可以省略,简化后代码如下:
/*
关键字 函数名称 参数类型 表达式体
↓ ↓ ↓ ↓ */
fun helloFun(name: String) = "Hello $name !"
注意:只有表达式体函数的返回类型可以省略,对于有返回值的代码块体函数,必须显式地写出返回类型和 return 语句。
Kotlin 的优势不仅仅体现在函数声明上,在函数调用的地方,也有很多独到之处,如果要调用上面声明的函数,可以通过代码:
helloFun("Kotlin")
不过,Kotlin 提供了一些新的特性,如:命名参数,简单理解就是,它允许在调用函数的时候传入形参的名称。
helloFun(name = "Kotlin")
来看一个更具体、更复杂的函数调用,函数声明如下:
fun createUser(
name: String,
age: Int,
gender: Int,
address: String,
feedCount: Int,
likeCount: Long,
commentCount: Int
) {
//..
}
声明一个包含了很多参数的函数,在 Kotlin 中,针对参数较多的函数,一般会以纵向的方式排列,这样的代码更符合我们从上到下的阅读习惯,省去从左往右翻的麻烦。
有了命名参数,可以这样来调用函数:
createUser(
name = "Tom",
age = 30,
gender = 1,
address = "Kotlin",
feedCount = 2093,
likeCount = 10937,
commentCount = 3285
)
把函数的形参和实参用 = 连接,建立两者的对应关系,使得代码的可读性更强,如果将来要修改 likeCount 这个参数,也是一目了然,很方便就能定位到,体现了代码的易维护性 (参数较多的时候,Java 代码中需按参数顺序来查找并修改)。
此外,Kotlin 还支持参数默认值,这个特性在参数较多的情况下同样有很大的优势。
fun createUser(
name: String,
age: Int,
gender: Int = 1,
address = "Kotlin",
feedCount: Int = 0,
likeCount: Long = 0L,
commentCount: Int = 0
) {
//..
}
在函数的参数列表中,gender、address 等参数都被赋予了默认值,这样的好处是在调用的时候可以简化函数的入参,代码如下:
createUser(
name = "Tom",
age = 30,
commentCount = 3285
)
在调用函数时,只传了 3 个参数,剩余的参数没有传,但是 Kotlin 编译器会自动填充上默认值。对于无默认值的参数,编译器会强制要求在调用时必须传参。对于有默认值的参数,则可传可不传 (有助于提升开发效率)。
在学习 Java 时,如果我们要声明变量,必须要声明它的类型,后面跟着变量的名称和对应的值,然后以分号结尾,代码如下:
// 定义变量 age,其类型为 Integer,值为 18
Integer age = 18;
而 Kotlin 则不一样,因为许多变量声明的类型都可以省略,因此在 Kotlin 中以关键字开始,然后是变量名称,最后可以加上类型,代码如下:
/*
关键字 变量类型
↓ ↓ */
var price: Int = 100; /*
↑ ↑
变量名称 变量值 */
和表达式函数一样,由于 Kotlin 支持类型推导,如果不指定变量的类型,编译器会分析初始化器表达式的值,并把它的类型作为变量的类型。注意:上面代码末尾的分号可以省略。简化后代码如下:
/*
关键字 变量类型默认推导为 Int
↓ */
var price = 100 /*
↑ ↑
变量名称 变量值 */
声明变量的关键字有两个:
默认情况下,应该尽可能地使用 val 关键字来声明所有的 Kotlin 变量,仅在必要的时候使用 var 关键字来声明变量。
注意:尽管 val 引用自身是不可变的,但是它指向的对象可能是可变的。
/*
关键字 声明不可变引用
↓ ↓ */
val languages = arrayListOf("Java")
languages.add("Kotlin") // 改变引用指向的对象
注意:即使 var 关键字允许变量改变自己的值,但是它的类型却是改变不了的。
var answer = 42
answer = "no answer" // 错误:类型不匹配
提示:如果需要在变量中存储不匹配类型的值,必须手动把值转换或强制转换到正确的类型。
声明变量后,自然是要使用的,下面看一下怎么使用变量的值,代码如下:
fun main(args: Array<String>) {
val name = "Kotlin"
println("Hello $name !") // 打印输出“Hello Kotlin !”
}
在代码中,声明一个变量 name,并在后面的字符串字面值中使用。和许多脚本语言一样,Kotlin 可以在字符串字面值中引用局部变量,只需要在变量名称前面加上字符 $。这等价于 Java 中使用 + 来拼接字符串,效率一样但是更紧凑。注意:
Kotlin 还可以引用更复杂的表达式,不仅限于简单的变量名称,只需要把表达式用花括号括起来即可,代码如下:
fun main(args: Array<String>) {
if (args.isNotEmpty()) {
println("Hello ${args[0]} !") // 使用 ${} 的语法插入 args 数组中的第一个元素
}
}
此外:还可以在双引号中直接嵌套双引号,只要它们处在某个表达式的范围内 (即花括号内)。
fun main(args: Array<String>) {
println("Hello, ${if (args.isNotEmpty()) args[0] else "someone"}!")
}
在 Kotlin 中一切皆是对象,因此对象就有可能为空,那么可不可以给一个变量赋空值呢?参考下面代码:
val score: Double = null // 编译器报错:null 不能赋值给 Double 类型的非空变量
由于 Kotlin 强制要求在定义变量的时候,需指定这个变量是否可能为 null,对于可能为 null 的变量,在声明的时候要在变量类型后面加一个问号 “?”,代码如下:
val score: Double? = null // 编译通过
注意:Kotlin 对可能为空的变量类型做了强制区分,即可能为空的变量无法直接赋值给不可为空的变量。不过,反向赋值是可以的。
var grade: Double? = null
var score: Double = 148.toDouble()
grade = score // 编译器报错
score = grade // 编译通过
在 Java 中,基础类型分为原始类型 (Primitive Type) 和包装类型 (Wrapper Type)。如:整型会有对应的 int 和 Integer,前者是原始类型,后者是包装类型。
Java 这样设计,是因为原始类型的开销小、性能高,但它不是对象,无法很好地融入到面向对象的系统中。而包装类型的开销大、性能相对较差,但它是对象,有成员变量以及成员方法,可以很好地发挥面向对象的特性。
Kotlin 中的基础类型,包括:数字类型、布尔类型、字符类型、及由前面这些类组成的数组等。在 Kotlin 语言体系中,没有原始类型这个概念,也就是在 Kotlin 中一切皆是对象。看一段代码:
/*
关键字 变量名 变量类型 调用 148 的成员方法
↓ ↓ ↓ ↓ */
val score: Double = 148.toDouble()
这里由于整型数字 “148” 被看作是对象,因此可以调用它的成员方法 toDouble(),这在 Java 中是不允许的。
在数字类型上,Kotlin 和 Java 几乎是一致的,包括它们对数字“字面量”的定义方式。通过一段代码及注释来介绍:
val int = 1 // 整数默认会被推导为 Int 类型
val long = 1234567L // Long 类型需要使用 L 后缀
val double = 13.14 // 小数默认会被推导为 Double 类型,但不需要使用 D 后缀
val float = 13.14F // Float 类型需要使用 F 后缀
val hexadecimal = 0xAF // 使用 0x 前缀代表十六进制字面量
val binary = 0b01010101 // 使用 0b 前缀代表二进制字面量
对于数字类型的转换,Kotlin 与 Java 的转换行为是不一样的。Java 可以隐式转换数字类型,而 Kotlin 更推崇显式转换。比如,在 Java 中经常直接把 int 类型的值赋值给 long 类型的变量,此时编译器会自动做类型转换。但需注意的是:不同类型数据之间的互相转换是存在精度问题的,尤其是当这样的转换代码掺杂在复杂的逻辑中时,在碰到一些边界条件的情况下,即使出现 Bug 也不容易排查出来。
int i = 100;
long j = i; // 编译不会报错,存在精度问题
同样的代码,在 Kotlin 中是行不通的,代码如下:
val i = 100
val j: Long = i // 编译器报错 - 类型不匹配
在 Kotlin 中,抛弃了隐式转换,推崇使用显示转换,即调用 Int 类型的 toLong() 函数进行转换后赋值:
val i = 100
val j: Long = i.toLong() // 编译通过,Kotlin 提供了很多类似的函数
通过这种显式的转换,使得代码的可读性更强,同时代码也更容易维护。
布尔类型用 Boolean 来表示,该类型只有两种值分别是 true 和 false。布尔类型支持一些逻辑操作:
字符类型用 Char 来表示字母 (大写和小写)、数字和其它符号,每个字符只是一个符号,包含在单引号中。
val c: Char = 'A'
val cha: Char = 'a'
val money: Char = '¥'
字符串类型用 String 来表示,字符串,顾名思义,就是一连串的字符序列,在大部分情况下,使用双引号来表示字符串的字面量。注意:和 Java 一样,Kotlin 中的字符串也是不可变的。
val str = "Hello Kotlin!"
此外,Kotlin 还新增了一个原始字符串,是用三个双引号来表示其字面量的。可以用于存放复杂的多行文本,并且它定义的时候是什么格式,最终打印也会是对应的格式。所以当我们需要复杂文本的时候,就不需要像 Java 那样写一堆的加号和换行符。
val str = """
当我们的字符串有复杂的格式时
原始字符串非常的方便
因为它可以做到所见即所得。"""
Kotlin 中的数组与 Java 相比有一些改变,在 Kotlin 中,一般使用 arrayOf() 函数来创建数组,括号当中可以用于传递数组元素进行初始化,同时 Kotlin 编译器也会根据传入的数组元素进行类型推导。
val arrayInt = arrayOf(1, 2, 3) // 类型推导为 整形数组
val arrayString = arrayOf("Java", "Kotlin") // 类型推导为 字符串数组
在 Java 中,数组和其它集合的操作是不一样的,如获取数组的长度,使用 Array # length() 方法,如果获取集合 List 的大小,则使用 List # size() 方法,这主要是因为数组不属于 Java 集合。
而 Kotlin 的数组虽然也不属于集合,但它的一些操作是跟集合统一的,比如获取数组的长度,使用的是 Array # size() 方法,示例代码如下:
val array = arrayOf("Java", "Kotlin")
println("Size is ${array.size}")
println("First element is ${array[0]}")
Kotlin 中,流程控制主要有 if、when、while 和 for,使用它们可以控制代码的执行流程,也是体现代码逻辑的关键。
在程序开发中 if 语句主要是用于逻辑判断,代码如下:
val i = 1
if (i > 0) { // 如果变量 i 的值大于0则打印 Big 否则打印 Small
print("Big")
} else {
print("Small")
}
此外,Kotlin 的 if,并不是程序语句那么简单,它还可以作为表达式来使用。
val i = 1
val message = if (i > 0) "Big" else "Small"
代码中把 if 当作表达式,并将 if 判断的结果赋值给变量 message,同时 Kotlin 编译器会根据 if 表达式的结果自动推导出变量 message 的类型为 String,使得代码更加简洁。
另外,Kotlin 还提供了一种简写,叫做 Elvis 表达式,示例代码如下:
fun getLength(text: String?): Int {
return text?.length ?: 0
}
通过 Elvis 表达式,针对可空变量,不必再写 “if (xxx != null) xxx else xxx” 这样的代码逻辑,提高代码可读性和编码效率。
和 if 相似,Kotlin 中的 when 主要也是用于逻辑判断的,它可以被认为是 Java 中的 switch case,但是又比 switch case 更强大,使用的也更频繁。
val i: Int = 1
when(i) {
1 -> print("一")
2 -> print("二")
3 -> print("三")
else -> print("其它")
}
示例代码可以看到,确实跟 Java 的 switch case 语句很像,但是比 switch case 强在哪里呢?其实跟上面的 if 一样,when 语句也可以作为表达式为变量赋值,代码如下:
val i: Int = 1
val message = when(i) {
1 -> "一"
2 -> "二"
else -> "其它" // 如果去掉这行,会报错
}
注意:
Kotlin 有 while 和 do-while 循环,它们的语法和 Java 中相应的循环没有什么区别,一般用于重复执行某些代码。
var i = 0
while (i <= 2) { // 如果 i 值小于等于 2 则输出并自增 1
println(i)
i++
}
var j = 0
do {
println(j)
j++
} while (j <= 2)
而对于 for 语句,Kotlin 不是常规的先初始化变量,然后在循环的每一步更新它的值,并在值满足某个限制条件时退出循环。而是使用了区间的概念,区间本质上就是两个值之间的间隔,这两个值通常是数字:一个起始值,一个结束值。使用 … 运算符来表示区间:
val oneToTen = 1..10 // 表示 [1, 10] 左闭右闭
val aToG = 'A'..'G' // 表示 [A, G] 左闭右闭
注意:Kotlin 的区间是包含的或者闭合的,意味着第二个值始终是区间的一部分。
接下来,使用 for 语句来对上面的闭区间进行迭代:
for (i in oneToTen) { // 正序输出 1..10 的值
println(i)
}
这里不仅可以正序迭代输出,还可以逆序迭代输出:
for (i in 10 downTo 0 step 2) { // 逆序迭代,从 10 到 0,迭代步长为 2
println(i) // 输出 10 8 6 4 2 0
}
注意:逆序区间我们不能使用 6…0 来定义,如果用这样的方式来定义的话,代码将无法正常运行。
还有,针对区间可以使用 in 运算符来检查一个值是否在区间中,或者它的逆运算 !in 来检查这个值是否不在区间中。
val aToG = 'A'..'G' // 代表 [A, G]
println('B' in aToG) // ‘B’ 在区间内,输出 true
println('H' !in aToG) // ‘H’ 不在区间内,输出 true
在 Kotlin 中所有类都有一个共同的超类 Any,这对于没有超类型声明的类是默认超类,Any 类:它除了 equals() 、 hashCode() 和 toString() 外没有任何成员。
Kotlin 中定义类有些地方不同于 Java,通过一段代码及注释来详解:
/*
关键字 类名 可见性修饰符 主构造方法关键字声明
↓ ↓ ↓ ↓ */
class Person private constructor(userName: String) {
private var mUserName: String
private var mAge: Int
private var mAddress: String = ""
init { // init 代码块在主构造器被调用时调用
println("init Person")
// 可以直接使用主构造方法中定义的参数
mUserName = userName
// 也可以用于给属性赋初值
mAge = 0
}
// 次要构造方法,非必须的,如果有,那么可以有多个
constructor(userName: String, age: Int) : this(userName) { // 直接调用主构造方法
mAge = age
}
// 次要构造方法,间接调用主构造方法,在同一个类中代理另一个构造函数使用 this 关键字
constructor(userName: String, age: Int, address: String) : this(userName, age) {
mAddress = address
}
}
// 实例化对象时,直接调用构造方法即可,由于声明时加了 private 访问修饰符,在外部实例化会报错
var people = Person("Chris")
// 空实现类可以不需要花括号 {}
class Man
Kotlin 的可见性修饰符与 Java 类似,但是默认的可见性修饰符不一样,如果省略修饰符:Java 默认包私有,Kotlin 默认声明是 public 的。
| 修饰符 | 类成员 | 顶层声明 |
|---|---|---|
| public (默认) | 所有地方可见 | 所有地方可见 |
| internal | 模块中可见 | 模块中可见 |
| protected | 子类中可见 | – |
| private | 类中可见 | 文件中可见 |
internal 只在模块内部可见。一个模块就是一组一起编译的 Kotlin 文件,这可能是一个 intellij IDEA 模块,一个 Eclipse 项目,或者一组使用调用 ant 任务进行编译的文件。
此外,Kotlin 还提供一种简化的构造方法定义形式,在定义构造参数的同时就对属性进行初始化 (可以是 var 或 val)。
/*
关键字 类名 访问修饰符 主构造方法中对属性进行初始化
↓ ↓ ↓ ↓ */
class Person (private val username: String, private val age:Int) {
......
}
类的成员变量或属性直接在主构造方法中定义,代替了构造参数。它们和构造参数的区别在于,作用域不同。在主构造方法定义的属性,其在整个类的作用域内有效,而构造参数仅在 init 代码块中有效,或者在定义属性初始化值时有效。
枚举类最基本的用法是实现一个类型安全的枚举,枚举常量用逗号分隔,每个枚举常量都是一个对象。
/*
软关键字 关键字 类名
↓ ↓ ↓ */
enum class Color {
RED, BLACK, BLUE, GREEN, WHITE
}
每一个枚举都是枚举类的实例,它们可以被初始化,即创建每个枚举常量时可以指定其属性值:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF); // 必须加分号
fun rgb() = { ... } // 枚举类定义一个方法
}
注意:如果要在枚举类中定义任何方法,要使用分号 ; 将枚举常量列表和方法定义分隔开。