1 前言
在开始语法学习Kotlin之前,先说说本系列文章内容的大概的定位,本系统文章只是针对Android开发者快速上手Kotlin语言,大多时候在语法的介绍时会跟Java语言作比较,一些基本上跟Java差别不大的地方可能并不会介绍到,所以如果你并不想花太多时间去阅读枯燥无味的官方文档又希望快速上手的话,那恭喜你是来对地方了。同时并建议,如果你是没有Java语言基础的同学,对不起,这篇文章可能并不适合你哦。
2 Kotlin背景
在2017年Google I/O大会后,Kotlin语言几乎是一夜暴红。在当时大会上Google宣布其 Android Studio IDE 中支持 Kotlin,Kotlin 与 Java 并存,并且成为 Android 开发的一级语言,当时很多Android开发者也是一脸懵逼,用Java用得好好的干嘛又搞出一个新的语言来呢?当然这也是一个原因的,因为在过去几年里,Oracle对Google一直有一桩侵权使用Java专利的官司,这也难怪Google要为自己准备一条后路。其实这也是很正常的,旁边Apple不也是搞了一门新的Swift想办法摆脱Object C吗。又在两年后2019年Google I/O再把Kotlin语言定为Android应用程序开发的首选开发语言。果然在拼爹的时代能有个强大的干爹当靠山走的路还是比别人顺风顺水一点的,我们可以从http://pypl.github.io/PYPL.html上看到Kotlin语言近两年来其热度迅速攀升。
我们来看看Kotlin语言究竟是什么,能在芸芸众语言中脱颖而出受到Google的垂爱肯定也是有它的特殊之处的。Kotlin语言的开发者来自捷克的JetBrains公司,Kotlin是一门可运行在Java虚拟机、Android、浏览器上的静态语言,它与Java有着100%的兼容和具备诸多Java不支持的特性,它的设计意图本来也就是兼容Java语言并且比Java更安全、更简洁。
来看看Kotlin的发展历史:
2010年 立项,JetBrains 着手开发 Kotlin
2011.6 对外公开
2012.2 在 Apache 2 许可证下开源了 Kotlin 的源码
2013.8 支持Android Studio
2014.6 全新的开源web站点和域名
2016.2 发布了 Kotlin 1.0 版
2017.5 Google I/O大会宣布为官方一级语言
2019.5 Google I/O大会定为首选开发语言
3 开发环境
工欲善其事,必先利其器。要快速上手Kotlin语言,第一步当然是先搭建其开发环境。其实也是很简单的事,直接前往JetBrains的官网下载地址:https://www.jetbrains.com/idea/download/index.html下载IntelliJ IDEA就可以了。其中有免费的社区版和收费的旗舰版本,作为入门级别的我们,社区版完全够用了,因为当你安装完后打开它,你会惊然地发现IntelliJ IDEA除了明显的图标之外,其界面跟我们熟悉的Android Studio几乎是一模一样的,原来Android Studio就是IntelliJ Idea社区版的一个分支,所以它们俩使用起来体验不会有太大差别。Android Studio不能创建Kotlin工程,但可以支持打开。
正所谓千里之行始于HelloWorld,环境搭建好的,现在就让我们开始创建工程吧。我们在New Project中选择【Kotlin】-【JVM|IDEA】,然后按Next选择保存工程路径,便可以简单地创建一个工程了。
跟大多数语言一样,程序的入口就是main函数,所以我们先手动创建一个.kt的文件,然后敲入main方法,并打印一行不需要分号结束的Hello World!(因为Kotlin语言每行代码的结束一般不需要分号),最后点击小绿三角按钮运行程序,不出意外的情况下,你便会看到下方窗口中显示出运行结果来,如下图。
另外在开发过程中,希望查看Kotlin的编码或跟Java语法的对应关系,可以在【Tools】-【Kotlin】-【Show Kotlin Bytecode】中再点击【Decompile】查看,如:
4 基础语法快速掌握
4.1 数据类型
Kotlin 中我们常使用的类型跟Java基本一样,包括: Byte、Short、Int、Long、Float、Double、Char、String。跟Java区别于,Java中区分装箱类型和基本类型(像int和Integer、long和Long),而在Kotlin中不区分装箱类型和基本类型,它内部会根据实际情况自动转换,开发者无需关心是否需要使用大写还是小写,统一首字母大写。
补充和注意:
长整型的数值
在Java中,长整型的赋值后面是数字+大写或小写的L,而Kotlin中,语言设计者认为小写l容易造成跟数字1混淆,所以在Kotlin中只允许使用大写L。如:
var a:Long = 123L
禁止隐式转换
在Java中,可以将一个int值直接赋值给long作隐式转换,而在Kotlin中,语言设计者认为,类型转换是危险的,真的需要就要明确进行。如:
var i = 1
var a:Long = i.toLong()
无符号类型
在Java中,不存在无符号类型,而在Kotlin中跟C一样,是可以有无符号类型的,如:UByte、UShort、UInt、ULong等,其数值需要在数值后加u,便是无符号类型,它的取值范围就是普通数值类型正数部分。如:
var a:ULong = 123u
字符串判断两个等号和三个等号
在Java中,使用==来判断两个字符串是判断其引用值,而的Kotlin中,==和String.equals等价,是值判断,===才是引用判断。
字符串模板
在Java中,连接两个字符串一般使用+,而在Kotlin中,同样可以使用+,但是还可以使用字符串模板。如:
var a = "World"
var b = "Hello ${a}"
字符串三个引号情况
在Java中没有三个引用的语法,在Kotlin中,使用三个引号引用的字符串,可其进行换行和缩进,一般还配合trimIndent方法一同使用,表示裁剪字符串内容前面的空格(前导空格)。如:
var a = """
<html>
<body>
...
</body>
</html>
""".trimIndent()
字符串最终结果是:
<html>
<body>
...
</body>
</html>
或者还可以配合 trimMargin 方法一同使用,表示移除字符串每行前面的空格。如:
var a = """
<html>
|<body>
|...
|</body>
</html>
""".trimMargin() // 无参默认边界字符是"|"
var b = """
<html>
%<body>
%...
%</body>
</html>
""".trimMargin("%") // 参数"%"表示边界字符
字符串最终结果是:
<html>
<body>
...
</body>
</html>
4.2 变量和常量的声明
在Kotlin中,当使用到变量和常量的声明时,一般使用关键字“var”来声明变量,使用“val”来声明“常量”,后面可带数据类型或省略由编译器来推断。
说明:为什么上面那句话中“常量”需要加双引号?因为用val声明的“常量”在Kotlin中一般叫作只读变量,或者也可以叫作运行时常量。它并非真正意义上的常量。它们的区别在于:像Java的常量是编译期常量,编译机在编译时已经知道它是常量,并会把它的引用全部替换成它的值,而val声明的变量虽然是不可变,但它实质上还是一个变量,所以在运行时的时候,还是可以使用反射将它的值进行改变的。如果你一定要使用像Java中加final声明的常量的话,就要在其前面加const。它们的使用如:
var name1:String = "子云心" // 变量
var name2 = "子云心" // 不写类型即编辑器自动推断类型
val name3 = "子云心" // 只读变量,或者叫运行时常量
const val name4 = "子云心" // 常量(编译期常量),等价于Java中的 static final String name4 = "子云心"
注意:使用const声明常量只适配全局范围、只能修饰基本类型、必须立即用字面量初始化。所以我们在一般情况下想使用常量使用val即可。
4.3 数组、区间和集合
4.3.1 数组
数组的使用可以使用Array类型声明。如:
var array1: Array<String> = arrayOf("Hello", "World")
var array2: Array<Char> = arrayOf('H','e','l','l','o')
var array3: Array<Int> = arrayOf(1,2,3)
设计者为了避免装箱和折箱,所以又为基本类型定义了一套。如:
var array2: CharArray = charArrayOf('H','e','l','l','o')
var array3: IntArray = intArrayOf(1,2,3)
上述array3变量又可以写以这样,IntArray类的构造方法接收数组的大小,it的值为数组下标:
var array3 = IntArray(3) { it + 1 }
补充:
joinToString方法
使用joinToString方法可让数组所有元素连接成一个字符串,使用如:
println(array2.joinToString()) // 输出结果:H, e, l, l, o
println(array2.joinToString("")) // 输出结果:Hello
判断元素是否存在
判断某个元素是否存在于数组中,已经不需要像Java那样在循环中进行每个判断的判断,可以直接使用contains方法或关键字in、!in便可。如:
var result1: Boolean = 'w' !in array2
var result2: Boolean = 1 in array3
var result3: Boolean = array3.contains(1)
循环中使用indices关键字
数组循环中使用indice属性可以像Java中的for的i++用法,如:
for (i in array2.indices) {
println(i)
}
循环中使用withIndex关键字
数组循环中使用withIndex属性可以同时输出下标和值,如:
for ((i, e) in array2.withIndex()) {
println("${i} ${e} ")
}
4.3.2 区间
区间是一个数学上的概念,表示一段范围,在Kotlin中一般情况下使用两个点..、until、downTo等来表示。使用如:
var a = 0..100 // [0,100]
var b = 0 until 100 // [0,100),不包括100
var c = 0 downTo 100 // 倒序
var d = c.reversed() // 创建一个基于原来倒序的区间
var e = 'a'..'z' // 也适用于字母
补充:
判断元素是否存在
判断是否存在于区间内也是使用contains方法或使用关键字in。如:
var result1: Boolean = 1 in a
var result2: Boolean = 'A' !in e
var result3: Boolean = e.contains('a')
步长
步长表示区间内的间隔。如:
var e = 0..10 step 2 // 值是:0 2 4 6 8 10
4.3.3 集合
Java中的集合有:List<T>、Map<K,V>和Set<T>,而在Kotlin中将它们细分为可变集合和不可变集合。其中不可变集合依然是List<T>、Map<K,V>和Set<T>,而它们对应的可变集合是MutableList<T>、MutableMap<K,V>和MutableSet<T>。其使用如下:
val list1: List<Int> = listOf(1, 2, 3) // 不能添加和删除元素
val list2: MutableList<Int> = mutableListOf(1, 2, 3) // 可以添加和删除元素
val list3 = ArrayList<Int>() // 可以添加和删除的空List
val map1: Map<Int, String> = mapOf(1 to "一", 2 to "二") // to 理解为K-V
val map2: MutableMap<Int, String> = mutableMapOf(1 to "一", 2 to "二")
val set1: Set<Int> = setOf(1, 2, 3)
val set2: MutableSet<Int> = mutableSetOf(1, 2, 3)
补充:
添加和删除元素
添加和删除元素可以如果是List或Set的话,使用add和remove方法,又或者直接使用+=和-=运算符,如果是Map的话,使用push方法或可像数组的形式进行添加。如:
list2.add(4)
list2.remove(3)
list2 += 5
list2 -= 2
map2.put(3, "三")
map2[4] = "四"
读取和修改元素
读取和修改元素跟数组类型的访问方式相同。如:
val a = list2[0]
list2[0] = 9
map2[3] = "四"
Pari键值对
上面在Map的创建中传入的就是pari对象,它通过to关键字连接key和value值。使用如:
val pair1 = 1 to "一" // 创建
var pair2 = Pair(2, "二") // 不同的创建方式
val first = pair1.first // 获得第一个元素
val second = pair1.second // 获得第二个元素
val(x, y) = pair1 // 解构
Triple 三个元素版本的Pair
val triple = Triple(1, "一", "壹")
val first = triple.first
val second = triple.second
val third = triple.third
val (x, y, z) = triple
有时候一个函数需要返回多个参数,那完全可以使用Pari或Triple来实现
4.4 空类型安全和智能类型转换
4.4.1 空类型安全
在Kotlin中,任意类型都有可空和不可空两种情况,在默认情况下定义的变量是不能为空的,若需要为空的情况,需要在类型后面加一个问号”?”,所以在Kotlin开发中减少了很多空指针崩溃的恶梦。我们来看看可空和不可空的定义:
val notNull:String = null // 代码报错,编译不通过
val nullable:String? = null // 正确的代码
val length1:Int = notNull.length // 正确的代码
val length2:Int = nullable.length // 代码报错,因为可能为空,不能直接获取长度
val length3:Int = nullable!!.length // 正确的代码,表示认定一定不为空
val length4:Int? = nullable?.length // 正确的代码,表示若为空,则返回空
val length5:Int = nullable?.length?:0 // 正确的代码,表示若为空,则返回0
注意:
关键符号!!、?.、?:
!! 表示我已明确清楚类型一定不为空,强制转成非空类型
?. 表示若不为空就返回后面属性,为空就返回空
?: 表示若不为空就返回本身,为空就返回后面语句(助记:像Java中的 a != null ? a : 0,但为了代码不冗余,把 “判空” 和 “本身” 省略,变成 a ? : 0)
不可空类型是可空类型的子类
我们通过里氏替换原则可以发现不可空和可空类型它们的关系是父类和子类的关系,如代码:
var x: String = "Hello"
var y: String? = "World"
x = y // 代码报错
y = x // 代码正确,因为String是String?的子类
平台类型
在Kotlin就一定不会出现空指针异常了吗?非也,当使用Kotlin去调用一段Java的代码类时还是会存在空指针的可能。如:
// Java code
public class JavaClass {
public String getA() {
return null;
}
}
// Kotlin code
var javaClass = JavaClass()
var a: String = javaClass.getA() // 发生异常
var b: String? = javaClass.getA()
上述代码中,javaClass的getA方法实质上返回的类型就叫平台类型,在IDE中会提示其类型是String!(实质上是不存在类型加感叹号出现的),因为它可能为空也可能不为空,所以程序在运行时才会发生空指针异常。
4.4.2 智能类型转换
在Kotlin中,类型的强转换可以使用as和as?关键字。如:
open class Parent()
class Son() : Parent()
class Boy()
val son: Son = parent as Son // 转换成功
val boy1:Boy = parent as Boy // 转换失败,程序抛出异常
val boy2:Boy? = parent as? Boy // 转换失改,返回null
编译器推断类型。如:
val parent: Parent = Son()
parent.sonFun() // 编译报错,因为parent中不存在sonFun方法
if (parent is Son) {
parent.sonFun() // 正确代码,因为已经进行过判断了,编译器已经推断了类型
}
parent.sonFun() // 编译报错,因为离开了智能转换的范围
4.5 条件、循环、异常捕获
4.5.1 条件
在Java中,条件语句就是if else、switch case以及三元运算符 ?a:b,下面就来看看Kotlin中对应的使用。
if else还是一样的if else
var a:Int = 1
var b:String
// 跟Java一样,没特别
if (a == 1) {
b = "true"
} else {
b = "false"
}
// 表达式方式,像函数返回值一样(一行时大括号省略了),也就是Java中的三元运算符
b = if (a == 1) "true" else "false"
使用when代替switch
when(a) {
0-> b = "一"
1-> b = "二"
else -> b ="未知"
}
// 表达式,像函数返回值一样
b = when(a) {
0-> "一"
1-> "二"
else -> "未知"
}
// 表达式,when后不跟括号和对象,条件直接写在每个分支中
var x: String? = "123"
b = when {
x is String -> x.length.toString()
x.isNullOrEmpty() -> "为空"
else -> "未知"
}
4.5.2 循环
for循环
for循环配合in关键字,使用如下:
var array: Array<Char> = arrayOf('H', 'e', 'l', 'l', 'o')
for(str in array) {
println(str)
}
for ((i, v) in array.withIndex()) {
println("${i} , ${v} ")
}
for (item in array.withIndex()) { // item为每一项的下标和值
println("${item.index}, ${item.value} ")
}
for (i in array.indices) { // i为每一项的下标
println(i)
}
forEach
var array: Array<Char> = arrayOf('H', 'e', 'l', 'l', 'o')
array.forEach({ it -> println(it) })
while循环
跟Java的使用一样,不列举。
跳了多层循环
在循环处声明一个名称用@连接,在break处指定跳出位置。如:
var array1: Array<Char> = arrayOf('H', 'e', 'l', 'l', 'o')
var array2: Array<Char> = arrayOf('W', 'o', 'r', 'l', 'd')
Outter@for(i in array1) {
Inner1@for(j in array2) {
break@Outter
}
}
给任意类实现Iterator
实现Iterator,也就是说可支持循环,需要实现next 和hasNext。如:
class A(val iterator:Iterator<Int>) {
operator fun next():Int{
return iterator.next()
}
operator fun hasNext():Boolean{
return iterator.hasNext()
}
}
class AList() {
private val list = ArrayList<Int>()
fun add(int:Int) {
list.add(int)
}
fun remove(int:Int) {
list.remove(int)
}
operator fun iterator():A {
return A(list.iterator())
}
}
// 调用示例
val list = AList()
list.add(1)
list.add(3)
list.add(5)
// for循环形式
for (i in list) {
println(i)
}
// while循环形式
val i = list.iterator()
while (i.hasNext()) {
println(i.next())
}
4.5.3 异常捕获
Kotlin中异常捕获跟Java中的语法一样,这里提一下其表达式的用法。如:
val result = try {
1
} catch (e:Exception) {
e.printStackTrace()
0
}
4.6 函数、具名参数、变长参数、默认参数和Lambda表达式
4.6.1 函数
我们先来说说方法和函数的区别,在Java中只有方法没有函数,而在C++中又一般叫函数不叫方法,这是为什么呢?其实很好理解,类里的funcation一般叫方法,而函数可以放在类的外边。所以也有说,方法其实是函数的一种特殊类型。Kotlin中对函数的语法如:
fun 方法名(参数1:参数类型, 参数N:参数类型):返回类型 {
函数体
}
其中,如果返回类型是Unit的话,可以省略不写。
函数的类型和引用
函数的引用,使用两个分号::。如:
class A() {
fun fun1(p1:String, p2:Long):Int {
return 1
}
}
// 调用
val f:(String, Long)->Int = a::fun1
val result = f("Hello", 50L)
匿名函数
函数可以不命名,但要赋值给一个变量。将函数看作一个代码块,或者说是一个类型,直接赋值给一个变量。如:
val result = fun(x:Int):Long {
return x.toLong()
}
4.6.2 具名参数
明确告诉编译器哪个参数给哪个变量,参数的顺序也可以变换。如:
fun add(p1:String, p2:String):String {
return p1 + p2
}
// 调用
add(p2 = "World", p1 = "Hello")
4.6.3 变长参数
变长参数使用关键字vararg来表示,使用起来就是可以接收多个不确定数量的同类型参数或者传入一个数组,如果传入的是一个数组,需要在其前面加个*,表示把数组展开成一个个元素,目前只支持变长参数的展开数组,并非C++的指针。如:
fun add(vararg ints: Int) {
ints.forEach {
println(it)
}
}
// 调用
add(1, 2, 3, 4, 5)
var list: IntArray = intArrayOf(1, 2, 3)
add(*list)
注意:
- 变长参数还不支持list,只支持array。
- 结合具名参数时,变长参数可以放在函数参数列表的任何位置。
4.6.4 默认参数
默认参数的出现大大减少了使用方法重载的麻烦。如:
fun add(int:Int, string:String = "Hello") {
println(int)
println(string)
}
// 调用
add(100)
add(100, "子云心")
注意:
结合具名参数时,默认参数也是可以放在函数参数列表的任何位置。
兼容Java的注解
因为Java中是不存在默认参数的,所以如果你写的Kotlin代码也想提供给Java开发者调用的话,最好可以加上@JvmOverloads注解。如:
class A () {
@JvmOverloads
fun a(p:Int = 0) {
println(p)
}
}
// Java中的调用也可以这样
new A().a();
4.6.5 Lambda表达式
Lambda表达式其实就是匿名函数比较有表示力的一种写法,Lambad表达式返回值是大括号里的最后一行。写法如下:
{[参数列表] –> [函数体,最后一行是返回值]}
比如:
()->Unit // 无参,返回值为Unit
(Int)->Int // 传入整型,返回一个整型
(String, (String)->String)->Boolean // 传入字符串、Lambda表式式,返回Boolean
示例:
// 匿名函数
val sum = fun(a: Int, b: Int): Int {
return a + b
}
// Lambda表达式
val sum = { a: Int, b: Int -> a + b }
// 调用
sum(2, 3)
sum.invoke(2, 3)
简化过程:
var array: IntArray = intArrayOf(1, 2, 3)
// for循环的形式
for (item in array) {
println(item)
}
// forEach循环形式
array.forEach({ it -> println(it) })
//如果一个函数最后一个参数是Lambda表达式,可以将它移到括号外
array.forEach() { println(it) }
// 如果小括号没有参数,可以省略
array.forEach { println(it) }
// 入参、返回值与形参一致的函数可以用函数引用方式作为实参传入
array.forEach(::println)
注意:
如果在Lambda中执行return,是直接对外边的函数执行,因为Lambda是表过式,并非函数。如果一定要return表达式,可以这样写:
fun AA() {
var array: IntArray = intArrayOf(1, 2, 3)
array.forEach ForEach@ {
if (it == 2) return@ForEach
print(it)
}
println("AA End")
}
fun BB() {
var array: IntArray = intArrayOf(1, 2, 3)
array.forEach {
if (it == 2) return
print(it)
}
println("BB End")
}
// 调用
AA() // 输出:13 AA End
BB() // 输出:1
4.7运算符重载、扩展成员、中缀表达式
4.7.1 运算符重载
运算符其实本身就是一个函数,所以可对其进行重载,只需要在函数前使用operator关键字。
class A(var a: String) {
operator fun plus(other:A):A {
var result = (a.toInt() + other.a.toInt()).toString()
return A(result)
}
override fun toString(): String {
return a
}
}
// 调用
var a1 = A("2")
var a2 = A("3")
println(a1 + a2)
说明:
- 示例中,将两个A对象进行用加号相加,其实“+”对应就是plus方法。
- Kotlin不支持自定义运算符,但可以使用中缀表达式做到类似的效果。
- 更多详细运算符对应的函数名,可以参考官方文档:https://www.kotlincn.net/docs/reference/operator-overloading.html。
4.7.2 中缀表达式
中缀表达式,使用infix关键字进行声明,它的使用看起来像是自定义运算符似的。如:
class Book{
infix fun on(any:Any):Boolean{
return false
}
}
class Desk {
}
// 调用
var result1 = Book() on Desk()
var result2 = Book().on(Desk())
说明:
- 这里写义了一个中缀表达式,也就是on方法,它在使用起来就跟表达式很相似了。
- 中缀表达式一般在DSL中使用,平时不建议使用,因为会使代码可读性降低。
4.7.3 扩展成员
在Java中是不支持扩展方法我,我们往往需要写一个工具方法就是通过静态方法来实现,而在Kotlin的扩展方法的定义要加.。如:
fun String.multiply(int:Int):String { // 扩展方法
var stringBuilder = StringBuffer()
for (i in 0 until int) {
stringBuilder.append(this)
}
return stringBuilder.toString()
}
operator fun String.times(int:Int):String { // 扩展方法+运算符重载,times对应的运算符是*
var stringBuilder = StringBuffer()
for (i in 0 until int) {
stringBuilder.append(this)
}
return stringBuilder.toString()
}
// 调用
println("abc".multiply(2)) // 输出结果:abcabc
println("abc" * 2) // 输出结果:abcabc
补充:
除了方法可扩展外,属性也是可以进行扩展的
val String.a:String // 扩展属性
get() = "abc"
// 调用
println("123".a) // 输出结果:abc