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 的原始类型。
在数字类型上,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
不论 Java 还是 Kotlin 中,类可以将其理解为对某种事物的抽象模型。
在 Java 中,Object 类是所有类的父类,即 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。
在 Kotlin 中,其所有类都有一个共同的超类 Any,这对于没有超类型声明的类是默认超类,Any 类:它除了 equals() 、 hashCode() 和 toString() 外没有任何成员。
Kotlin 中定义类有些地方不同于 Java,在 Kotlin 中类是由 class 声明,其类声明有三部分:类名、类头 (指定类型参数、主构造函数)、类体 (花括号),其中类头和类体都是可选。下面通过一段代码及注释来详解:
/*
关键字 类名 可见性修饰符 主构造方法关键字声明
↓ ↓ ↓ ↓ */
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 代码块中有效,或者在定义属性初始化值时有效。
最后,对于类中的属性,Kotlin 编译器会根据实际情况,自动为其生成 getter 和 setter。
注意:Kotlin 的属性要求必须明确提供初值,否则要么修改为 optional,要么使用 lateinit 关键字延迟初始化。
lateinit 关键字:
- 该修饰符只能用于在类体中 (不是在主构造函数中) 声明的 var 属性。
- 并且仅当该属性没有自定义 getter 或 setter 时。
- 该属性必须是非空类型,并且不能是原生类型。
枚举类最基本的用法是实现一个类型安全的枚举,枚举常量用逗号分隔,每个枚举常量都是一个对象。
/*
软关键字 关键字 类名
↓ ↓ ↓ */
enum class Color {
RED, BLACK, BLUE, GREEN, WHITE
}
每一个枚举都是枚举类的实例,它们可以被初始化,即创建每个枚举常量时可以指定其属性值:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF); // 必须加分号
fun rgb() = { ... } // 枚举类定义一个方法
}
注意:如果要在枚举类中定义任何方法,要使用分号 ; 将枚举常量列表和方法定义分隔开。
// Expr 接口没有声明任何方法,它只是一个标记接口
// 用来给不同种类的表达式提供一个公共的类型
interface Expr
// 声明类的时候,使用一个冒号(:)+接口名称,来标记这个类实现了这个接口
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
fun eval(e: Expr): Int {
if (e is Num) {
// 使用 as 关键字来表示到特定类型的显示转换
val num = e as Num //显示地转换成类型 Num 是多余的
return num.value
}
if (e is Sum) {
//变量 e 被智能地转换为类型 Sum,因此可以直接使用 e.left 和 e.right
return eval(e.left) + eval(e.right)
}
return 0
}
Java 中,通过 instanceof 关键字来判断一个对象是否是某种类型,在使用 instanceof 关键字判断之后,还需显式地加上类型转换。如果最初的变量会使用超过一次,常常选择把类型转换的结果存储在另一个单独的变量中。
而在 Kotlin 中,使用 is 检查来判断一个对象是否是某种类型。不同的是,在 Kotlin 中,编译器已经帮我们执行了类型转换,不用再手动添加类型转换,我们把这种行为称为智能转换。
注意:智能转换只在变量经过 is 检查且之后不再发生变化的情况下有效。当你对一个类的属性进行智能转换时,这个属性必须是一个 val 属性,而且不能有自定义的访问器。否则,每次对属性的访问是否都能返回同样的值将无从验证。
异常是在程序运行时可能发生的不必要的问题,并突然终止程序。异常处理是一个过程,使用它可以防止程序出现可能破坏代码的异常,有两种类型的异常:
注意:在 Kotlin 中,并不区分受检查异常和运行时异常,所有异常都是运行时异常,即便是原本在 Java 中的受检查异常,在 Kotlin 中也是运行时异常,如:IOException 在 Java 中是受检查异常,在 Kotlin 中是运行时异常。
Kotlin 的异常有 3 种:Exception、Error 和 Throwable。它们的继承关系如下:
Throwable:异常都直接或间接的继承于该类,在 Throwable 类中有几个非常重要的属性和函数:
Error:程序无法恢复的严重错误,只能让程序中止。例如:Java 虚拟机内部错误、内存溢出和资源耗尽等严重情况。
Exception:程序可以恢复的异常,属于可控范围内的。例如:除零异常、空指针访问、网络连接中断和读取不存在文件等。
处理 Kotlin 中的异常与 Java 相同,使用 try,catch 和 finally 块来处理代码中的异常。
fun main(args: Array<String>) {
try {
var num = 10 / 0 // 被除数不可以为 0 因此会抛出异常
// 使用 throw 关键字抛出异常,异常执行前的语句已执行
// 但异常后的语句未执行,因为控制流已转移到catch块
throw Exception("Something went wrong here")
println(num)
} catch (e: ArithmeticException) {
// 捕获算术异常并打印输出
println("Arithmetic Exception")
} catch (e: Exception) {
// 捕获其它异常并打印输出
println(e.message)
} finally {
// 无论是否发生异常,始终会执行 finally 块
println("It will print in any case.")
}
}
在 Kotlin 标准库中封装的异常类型,不可能会预见所有的可能碰见的异常情况,此时自己定义异常,来表示程序中可能出现的特定问题。
而如果要自定义异常,就必须继承现有的异常类,一般都继承其异常情况相似的类,建立异常类型最简单的方法就是使用编辑器产生默认的构造方法,这样简单而有效。
class CustomException : Exception {
// 无参构造
constructor() {}
// 带参构造
constructor(msg: String) : super(msg) {}
}
fun throwCustomExFun(param: String?) {
if (param == null) {
// 使用 throw 抛出自定义异常
throw CustomException("param is null")
}
}
注意:在 Kotlin 中不要求显式地指定函数可能抛出的异常,并且可以处理也可以不处理异常。
Kotlin 和 Java 的语言很相似,但又有一些改进和优化之处。本文介绍了一些 Kotlin 的基础知识: