简介
创作历史
Kotlin(科特林)是由JetBrains公司开发的静态编程语言,推出于2011年7月,支持编译为Java字节码、JavaScript及二进制代码,适用于Android、嵌入式设备等多平台开发。该语言于2016年2月15日发布首个稳定版本v1.0,采用Apache 2开源协议,2017年被Google列为Android官方支持开发语言。
设计目标
创建一种兼容Java的语言
让它比Java更安全,能够静态检测常见的陷阱。如:引用空指针
让它比Java更简洁,通过支持variable type inference,higher-order functions (closures),extension functions,mixins and first-class delegation等实现。
让它比最成熟的竞争对手Scala语言更加简单。
变量和函数
编程语言共有的特性那就是变量和函数了,因此可以说变量和函数是编程语言之本,这一节就先来来学习Kotlin中变量和函数的用法。
变量
先来学习变量。在 Kotlin 中定义变量的方式和 Java 区别很大,在 Java 中如果想要定义一个变
量,需要在变量前面声明这个变量的类型,比如说 int a 表示 a 是一个整型变量, String b 表
示 b 是一个字符串变量。而 Kotlin 中定义一个变量,只允许在变量前声明两种关键字: val 和
var 。
val ( value 的简写)用来声明一个不可变的变量,这种变量在初始赋值之后就再也不能重新赋
值,对应 Java 中的 final 变量。
var ( variable 的简写)用来声明一个可变的变量,这种变量在初始赋值之后仍然可以再被重新
赋值,对应 Java 中的非 final 变量。
如果你有 Java 编程经验的话,可能会在这里产生疑惑,仅仅使用 val 或者 var 来声明一个变量,
那么编译器怎么能知道这个变量是什么类型呢?这也是 Kotlin 比较有特色的一点,它拥有出色的
类型推导机制。
举个例子,我们可以 在 main() 函数中编写如下代码:
Kotlin
fun main() {
val a = 10
println("a = " + a)
}
注意, Kotlin 每一行代码的结尾是不用加分号的,如果写惯了 Java 的话,在这里得先熟悉一
下。
在上述代码中,我们使用 val 关键字定义了一个变量 a ,并将它赋值为 10 ,这里 a 就会被自动推
导成整型变量。因为既然你要把一个整数赋值给 a ,那么 a 就只能是整型变量,而如果你要把一
个字符串赋值给 a 的话,那么 a 就会被自动推导成字符串变量,这就是 Kotlin 的类型推导机制。
现在我们运行一下 main() 函数,执行结果如图 所示,正是我们所预期的。

但是 Kotlin 的类型推导机制并不总是可以正常工作的,比如说如果我们对一个变量延迟赋值的
话, Kotlin 就无法自动推导它的类型了。这时候就需要显式地声明变量类型才行, Kotlin 提供了
对这一功能的支持,语法如下所示:
Kotlin
val a: Int = 10
可以看到,我们显式地声明了变量 a 为 Int 类型,此时 Kotlin 就不会再尝试进行类型推导了。如
果现在你尝试将一个字符串赋值给 a ,那么编译器就会抛出类型不匹配的异常。
如果你学过 Java 并且足够细心的话,你可能发现了 Kotlin 中 Int 的首字母是大写的,而 Java 中
int 的首字母是小写的。不要小看这一个字母大小写的差距,这表示 Kotlin 完全抛弃了 Java 中的
基本数据类型,全部使用了对象数据类型。在 Java 中 int 是关键字,而在 Kotlin 中 Int 变成了一
个类,它拥有自己的方法和继承结构。表 中列出了 Java 中的每一个基本数据类型在 Kotlin 中
对应的对象数据类型。

但我们可能会产生疑惑:既然 val 关键字有这么多的束缚,为什么还要用这个关键字呢?干脆全
部用 var 关键字不就好了。其实 Kotlin 之所以这样设计,是为了解决 Java 中 final 关键字没有被
合理使用的问题。
在 Java 中,除非主动在变量前声明了 final 关键字,否则这个变量就是可变的。然而这并不
是一件好事,当项目变得越来越复杂,参与开发的人越来越多时,你永远不知道一个可变的变
量会在什么时候被谁给修改了,即使它原本不应该被修改,这就经常会导致出现一些很难排查
的问题。因此,一个好的编程习惯是,除非一个变量明确允许被修改,否则都应该给它加上
final 关键字。
但是,不是每个人都能养成这种良好的编程习惯。我们大多数 Java 程序员没有主动在
变量前加上 final 关键字的意识,仅仅因为 Java 对此是不强制的。因此, Kotlin 在设计的时候就
采用了和 Java 完全不同的方式,提供了 val 和 var 这两个关键字,必须由开发者主动声明该变量
是可变的还是不可变的。
那么我们应该什么时候使用 val ,什么时候使用 var 呢?这里有一个小诀窍,就是永远优
先使用 val 来声明一个变量,而当 val 没有办法满足你的需求时再使用 var 。这样设计出来的程
序会更加健壮,也更加符合高质量的编码规范。
函数
函数是用来运行代码的载体,你可以在一个函数里编写很多行代码,当运行这个函数时,函数
中的所有代码会全部运行。像我们前面使用过的 main() 函数就是一个函数,只不过它比较特
殊,是程序的入口函数,即程序一旦运行,就是从 main() 函数开始执行的。
但是只有一个 main() 函数的程序显然是很初级的,和其他编程语言一样, Kotlin 也允许我们自
由地定义函数,语法规则如下:
Kotlin
fun methodName(param1: Int, param2: Int): Int {
return 0
}
下面来解释一下上述的语法规则,首先 fun ( function 的简写)是定义函数的关键字,无论
定义什么函数,都一定要使用 fun 来声明。
紧跟在 fun后面的是函数名,能表达这个函数的作用是什么。
函数名后面紧跟着一对括号,里面可以声明该函数接收什么参数,参数的数量可以是任意多
个,例如上述示例就表示该函数接收两个 Int 类型的参数。参数的声明格式是 " 参数名 : 参数类
型 " ,其中参数名也是可以随便定义的,这一点和函数名类似。如果不想接收任何参数,那么写
一对空括号就可以了。
参数括号后面的那部分是可选的,用于声明该函数会返回什么类型的数据,上述示例就表示该
函数会返回一个 Int 类型的数据。如果你的函数不需要返回任何数据,这部分可以直接不写。
最后两个大括号之间的内容就是函数体了,我们可以在这里编写一个函数的具体逻辑。由于上
述示例中声明了该函数会返回一个 Int 类型的数据,因此在函数体中我们简单地返回了一个 0 。
这就是 Kotlin中最基本也是最常用的函数用法,我们再来学习一个 Kotlin 函数的语法糖,这个语法糖在以后的开发中会起到相当重要的作用。
当一个函数中只有一行代码时, Kotlin 允许我们不必编写函数体,可以直接将唯一的一行代码写
在函数定义的尾部,中间用等号连接即可。例如:
Kotlin
fun largerNumber(num1: Int, num2: Int): Int = max(num1, num2)
使用这种语法, return 关键字也可以省略了,等号足以表达返回值的意思。另外,由于Kotlin出色的类型推导机制吗,在这里它也可以发挥重要的作用。由于 max() 函数返回的是一个 Int 值,而我们在 largerNumber() 函数的尾部又使用等号连接了 max() 函数,因此Kotlin 以推导出largerNumber() 函数返回的必然也是一个 Int 值,这样就不用再显式地声明返回值类型了,代码可以进一步简化成如下形式:
Kotlin
fun largerNumber(num1: Int, num2: Int) = max(num1, num2)
程序的逻辑控制
程序的执行语句主要分为 3 种:顺序语句、条件语句和循环语句。顺序语句很好理解,就是代码
一行一行地往下执行就可以了,但是这种 执行方式在很多情况下并不能满足我们的 编程需求,这时就需要引入条件语句和循环语句了,下面我们逐个进行介绍。
if条件语句
Kotlin 中的条件语句主要有两种实现方式: if 和 when 。
首先学习 if , Kotlin 中的 if 语句和 Java 中的 if 语句几乎没有任何区别,因此这里就简单举个
例子。
还是以上一节中的 largerNumber() 函数为例,之前我们借助了 Kotlin 内置的 max() 函数来实
现返回两个参数中的较大值,但其实这是没有必要的,因为使用 if 判断同样可以轻松地实现这
个功能。将 largerNumber() 函数的实现改成如下写法:
Kotlin
fun largerNumber(num1: Int, num2: Int): Int {
var value = 0
if (num1 > num2) {
value = num1
} else {
value = num2
}
return value
}
Kotlin 中的 if 语句相比于 Java 有一个额外的功能,它是可以有返回值的,返回值就是 if 语句每
一个条件中最后一行代码的返回值。因此,上述代码就可以简化成如下形式:
Kotlin
fun largerNumber(num1: Int, num2: Int): Int {
val value = if (num1 > num2) {
num1
} else {
num2
}
return value
}
when条件语句
接下来我们开始学习 when 。 Kotlin 中的 when 语句有点类似于 Java 中的 switch 语句,但它又远
比 switch 语句强大得多。
如果熟悉 Java 的话,我们会发现 ava 中的 switch 语句并不怎么好用。首先, switch 只能传入
整型或短于整型的变量作为条件, JDK 1.7之后增加了对字符串变量的支持。其次, switch 中
的每个 case 条件都要在最后主动加上一个 break ,否则执行完当前 case 之后会依次执行下面的
case 。
而 Kotlin 中的 when 语句不仅解决了上述痛点,还增加了许多更为强大的新特性,有时候它比 if
语句还要简单好用,现在我们就来学习一下。
现在编写一个查询考试成绩的功能,输入一个学生的姓名,返回该学生考试的分数。
Kotlin
fun getScore(name: String) = when (name) {
"Tom" -> 86
"Jim" -> 77
"Jack" -> 95
"Lily" -> 100
else -> 0
}
when 语句和 if 语句一样,也是可以有返回值的,因此我们仍然可以使用单行代码函数的语法糖。
when 语句允许传入一个任意类型的参数,然后可以在 when 的结构体中定义一系列的条件,格式
是:
Kotlin
匹配值 -> { 执行逻辑 }
当执行逻辑只有一行代码时, { } 可以省略。
除了精确匹配之外, when 语句还允许进行类型匹配。什么是类型匹配呢?这里再举个例子。
定义一个 checkNumber() 函数,如下所示:
Kotlin
fun checkNumber(num: Number) {
when (num) {
is Int -> println("number is Int")
is Double -> println("number is Double")
else -> println("number not support")
}
}
上述代码中, is 关键字就是类型匹配的核心,它相当于 Java 中的 instanceof 关键字。由于
checkNumber() 函数接收一个 Number 类型的参数,这是 Kotlin 内置的一个抽象类,像 Int、
Long、Float、Double 等与数字相关的类都是它的子类,所以这里就可以使用类型匹配来判断传入的参数到底属于什么类型。
循环语句
熟悉 Java 的人应该都知道, Java 中主要有两种循环语句: while 循环和 for 循环。而 Kotlin 也提
供了 while 循环和 for 循环,其中 while 循环不管是在语法还是使用技巧上都和 Java 中的while循环没有任何区别。
下面我们开始学习 Kotlin 中的 for 循环。
Kotlin 在 for 循环方面做了很大幅度的修改, Java 中最常用的 for-i 循环在 Kotlin 中直接被舍弃了,而Java 中另一种 for-each 循环则被 Kotlin 进行了大幅度的加强,变成了 for-in 循环,所以我们只需要学习 for-in 循环的用法就可以了。
在开始学习 for-in 循环之前,还得先普及一个区间的概念,因为这也是 Java 中没有的东西。我们可以使用如下Kotlin 代码来表示一个区间:
Kotlin
val range = 0..10
上述代码表示创建了一个 0 到10 的区间,并且两端都是闭区间,这意味着 0 到 10 这两个端点都是包含在区间中的,用数学的方式表达出来就是0, 10 。
其中, .. 是创建两端闭区间的关键字,在 .. 的两边指定区间的左右端点就可以创建一个区间
了。
有了区间之后,我们就可以通过 for-in 循环来遍历这个区间,比如在 main() 函数中编写如下
代码:
Kotlin
fun main() {
for (i in 0..10) {
println(i)
}
}
但是在很多情况下,双端闭区间却不如单端闭区间好用。比如数组的下标都是从0 开始的,一个长度为 10 的数组,它的下标区间范围是 0 到 9 ,因此左闭右开的区间在程序设计当中更加常用。Kotlin 中可以使用 until 关键字来创建一个左闭右开的区间,如下所示:
Kotlin
val range = 0 until 10
上述代码表示创建了一个 0 到 10 的左闭右开区间,它的数学表达方式是 [0, 10) 。修改 main() 函
数中的代码,使用 until 替代 .. 关键字,你就会发现最后一行 10 不会再打印出来了。
默认情况下, for-in 循环每次执行循环时会在区间范围内递增 1 ,相当于 Java for-i 循环中 **i++**的效果,而如果你想跳过其中的一些元素,可以使用 step 关键字:
Kotlin
fun main() {
for (i in 0 until 10 step 2) {
println(i)
}
}
上述代码表示在遍历 [0, 10) 这个区间的时候,每次执行循环都会在区间范围内递增 2 ,相当于for-i循环中 i = i + 2 的效果。
不过,前面我们所学习的 .. 和 until 关键字都要求区间的左端必须小于等于区间的右端,也就是这两种关键字创建的都是一个升序的区间。如果想创建一个降序的区间,可以使用downTo关键字,用法如下:
Kotlin
fun main() {
for (i in 10 downTo 1) {
println(i)
}
}
这就创建了一个10, 1 的降序区间 。
面向对象编程
和很多现代高级语言一样, Kotlin 也是面向对象的。
类与对象
我们创建一个Person 类**,**会生成如下所示的代码:
Kotlin
class Person {
}
这是一个空的类实现,可以看到, Kotlin 中也是使用 class 关键字来声明一个类的,这一点和 Java 一致。现在我们可以在这个类中加入字段和函数来丰富它的功能。
Kotlin
class Person {
var name = ""
var age = 0
fun eat() {
println(name + " is eating. He is " + age + " years old.")
}
}
Person 类已经定义好了,接下来我们看一下如何对这个类进行实例化,代码如下所示:
Kotlin
val p = Person()
Kotlin 中实例化一个类的方式和 Java 是基本类似的,只是去掉了 new 关键字而已。之所以这么设计,是因为当调用了某个类的构造函数时,意图只可能是对这个类进行实例化,因此即使没有new 关键字,也能清晰表达出你的意图。 Kotlin 本着最简化的设计原则,将诸如 new 、行尾分号这种不必要的语法结构都取消了。
下面我们可以在 main() 函数中对 p 对象进行一些操作:
Kotlin
fun main() {
val p = Person()
p.name = "Jack"
p.age = 19
p.eat()
}
这都是跟Java 差不多的。
继承与构造函数
我们现在创建一个 Student 类继承于 Person类,
Kotlin
class Student {
var sno = ""
var grade = 0
}
现在 Student 和 Person 这两个类之间是没有任何继承关系的,想要让 Student 类继承 Person
类,我们得做两件事才行。
第一件事,使 Person 类可以被继承。可能很多人会觉得奇怪,尤其是有 Java 编程经验的人。一个类本身不就是可以被继承的吗?为什么还要使Person 类可以被继承呢?这就是 Kotlin 不同的地方,在Kotlin 中任何一个非抽象类默认都是不可以被继承的,相当于 Java 中给类声明了 final关键字。之所以这么设计,其实和 val 关键字的原因是差不多的,因为类和变量一样,最好都是不可变的,而一个类允许被继承的话,它无法预知子类会如何实现,因此可能就会存在一些未知的风险。
很明显, Kotlin 在设计的时候遵循了这条编程规范,默认所有非抽象类都是不可以被继承的。之所以这里一直在说非抽象类,是因为抽象类本身是无法创建实例的,一定要由子类去继承它才能创建实例,因此抽象类必须可以被继承才行,要不然就没有意义了。由于Kotlin中的抽象类和Java 中并无区别,这里就不再多讲了。
既然现在 Person 类是无法被继承的,我们得让它可以被继承才行,方法也很简单,在 Person类的前面加上open 关键字就可以了,如下所示:
Kotlin
open class Person {
...
}
加上 open 关键字之后,我们就是在主动告诉 Kotlin 编译器, Person 这个类是专门为继承而设计的,这样Person 类就允许被继承了。
第二件事,要让 Student 类继承 Person 类。在 Java 中继承的关键字是 extends ,而在 Kotlin中变成了一个冒号,写法如下:
Kotlin
class Student : Person() {
var sno = ""
var grade = 0
}
继承的写法如果只是替换一下关键字倒也挺简单的,但是为什么 Person 类的后面要加上一对括
号呢? 它涉及主构造函数、次构造函数等方面的知识,这里尽量尝试用最简单易懂的讲述来让你理解这对括号的意义和作用,同时顺便学习一下Kotlin 中的主构造函数和次构造函数。
任何一个面向对象的编程语言都会有构造函数的概念, Kotlin 中也有,但是 Kotlin 将构造函数分成了两种:主构造函数和次构造函数。
主构造函数将会是你最常用的构造函数,每个类默认都会有一个不带参数的主构造函数,当然也可以显式地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面即可。比如下面这种写法:
Kotlin
class Student(val sno: String, val grade: Int) : Person() {
}
这里我们将学号和年级这两个字段都放到了主构造函数当中,这就表明在对 Student 类进行实例化的时候,必须传入构造函数中要求的所有参数。
到这里为止和那对括号又有什么关系呢?这就涉及了 Java 继承特性中的一个规定,子类中的构造函数必须调用父类中的构造函数,这个规定在Kotlin 中也要遵守。
那么回头看一下Student类,现在我们声明了一个主构造函数,根据继承特性的规定,子类的构造函数必须调用父类的构造函数,可是主构造函数并没有函数体,我们怎样去调用父类的构造函数呢?
Kotlin 采用了一种简单但是可能不太好理解的设计方式:括号。子类的主构造函数调用父类中的哪个构造函数,在继承的时候通过括号来指定。因此再来看一遍这段代码,应该就能理解了。
Kotlin
class Student(val sno: String, val grade: Int) : Person() {
}
在这里, Person 类后面的一对空括号表示 Student 类的主构造函数在初始化的时候会调用Person类的无参数构造函数,即使在无参数的情况下,这对括号也不能省略。
学到这里,我们就将 Kotlin的主构造函数基本掌握了,但是, Kotlin 在括号这个问题上的复杂度并不仅限于此,因为我们还没涉及Kotlin构造函数中的另一个组成部分 ------ 次构造函数。
任何一个类只能有一个主构造函数,但是可以有多个次构造函数。次构造函数也可以用于实例化一个类,这一点和主构造函数没有什么不同,只不过它是有函数体的。Kotlin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。这里通过一个具体的例子就能简单阐明,代码如下:
Kotlin
class Student(val sno: String, val grade: Int, name: String, age: Int) :
Person(name, age) {
constructor(name: String, age: Int) : this("", 0, name, age) {
}
constructor() : this("", 0) {
}
}
次构造函数是通过 constructor 关键字来定义的,这里我们定义了两个次构造函数:第一个次构造函数接收name 和 age 参数,然后它又通过 this 关键字调用了主构造函数,并将 sno 和grade这两个参数赋值成初始值;第二个次构造函数不接收任何参数,它通过 this 关键字调用了我们刚才定义的第一个次构造函数,并将name 和 age 参数也赋值成初始值,由于第二个次构造函数间接调用了主构造函数,因此这仍然是合法的。