前言
这是前段时间学习Kotlin官方参考(中 / 英)时所做的笔记,相比官方参考,这些笔记:
- 为了方便根据笔记去查阅官方参考,笔记的整体结构与官方参考基本保持一致
- 对官方参考中一些零散分布的知识点进行了整合
- 对官方参考中个别讲的比较笼统的知识点做了详细展开
- 将官方参考中一些比较晦涩的知识点,使用个人认为更容易理解的方式进行了阐述
- 笔记并未100%覆盖官方参考中的内容
这份笔记基本涵盖了Kotlin的大部分知识点,可以作为 Kotlin快速入门 或是 官方参考的导读 来使用。
另外,因为个人也正处于学习Kotlin的过程中,因此笔记中的理解和论述难免会有错误,希望读者朋友不吝指正,大家共同学习进步。
1开始
1.1 基本语法
1.1.1 定义包
package foo.bar
//kotlin中语句结尾不需要写分号
var aaa = 1
fun baz() {
}
class Goo {
}
……
包的声明应处于源文件顶部。不同于Java,Kotlin中包名可以不用与源文件所在的目录结构对应,也就是说源文件可以随便移动而不用修改其包名。
Kotlin源文件的结构与C++类似,即变量、函数、类可以同级(如上面代码所示),而Java中,变量和函数都在类中。
源文件所有内容都包含在声明的包内。所以上例中 baz() 的全名是 foo.bar.baz 、Goo 的全名是foo.bar.Goo。如果没有指明包,该文件的内容属于无名字的默认包。
1.1.2 访问权限修饰符(又称为可见性修饰符)
Kotlin中变量、函数、类有三种位置,分别是:
- 顶层位置:即在源文件中顶层
- 类中
- 局部位置:即在代码块中或函数中
Kotlin中有四种可见性修饰符,分别为public(默认)、internal、protected、private。可以修饰顶层位置和类中的变量、函数、类,不能修饰局部位置的变量、函数、类。
顶层位置的可见性修饰符:
- public:随处可见,默认。
- internal:本模块内可见。
- protected:不可用于顶层位置的变量、函数、类。
- private:本文件内可见。
类中的可见性修饰符:
- public:能见到类的任何人都可见。
- internal:在本模块内,能见到类的任何人都可见。
- protected:本类和子类中可见。
- private:本类中可见。
关于可见性的应用,举一个简单直观的例子:
/*
*文件File1.kt
*/
package cn.szx.kotlindemo
//默认可见性为public
fun function1(){
print("function1 in File1")
}
//---------------------------------------------------------------------------------------
/*
*文件File2.kt
*/
package cn.szx.kotlindemo
fun function2(){
//File2中可以访问File1中定义的函数function1()
//并且因为File2和File1在同一个包内,因此访问function1()时不用加包名前缀
function1()
}
//编译报错,不能在同一个包内再定义一个function1()了
//因为此处的function1()和File1中的function1(),其全名都是cn.szx.kotlindemo.function1()
//系统无法区分二者
//fun function1(){
//
//}
//function1(a:Int)与File1中的function1()形参列表不同,可以区分,因此可以定义
fun function1(a:Int){
print("function1 in File2"+a)
}
//---------------------------------------------------------------------------------------
/*
*文件File3.kt
*/
package cn.szx.kotlindemo2
//import cn.szx.kotlindemo.function1
fun function3(){
//File3和File1不在同一个包内,因此访问function1()时需要加包名前缀
cn.szx.kotlindemo.function1()
//使用“import cn.szx.kotlindemo.function1”将function1导入后,则可以省略前缀
//“import cn.szx.kotlindemo.function1”代表将cn.szx.kotlindemo包内所有名为function1的函数都导入了
//function1()
}
//允许定义function1()。与File1中的function1()的包名前缀不同,因此系统可以区分
fun function1(){
print("function1 in File3")
}
1.1.3 定义函数
基本规则:使用fun关键字来定义函数,形参的定义格式为“形参名:形参类型”。
//带有两个 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) {
println("sum of $a and $b is ${a + b}")
}
1.1.4 定义变量
注意:abstract只能修饰成员位置(即类中)的变量和函数,不能修饰顶层位置和局部位置的变量和函数。
定义变量的基本格式:
var a:Int = 1//可变变量
val b:Int = 2//不可变变量
var c = 3//根据等号右边的值自动推断c的类型
var d:Int//定义时不进行初始化,则Int不能省略
var定义的是可变变量,即一般的变量,val定义的是不可变变量,即符号常量,类似于Java中的final变量或者C++中的const变量。
此外,处于不同位置的变量,其初始化要求是不同的:
- 顶层变量必须进行初始化。(除非其没有幕后字段,详见《03类和对象1.md》->“属性”)
- 类中的变量要么进行初始化(可以直接后接等号进行初始化,也可以在构造函数或初始化块(init块)中初始化),要么声明为abstract。(除非其没有幕后字段,详见《03类和对象1.md》->“属性”)
- 局部变量可以不初始化。
package cn.szx.kotlindemo
var a: Int = 1 //顶层变量,必须进行初始化
private class Test{
//abstract var a:Int //类中的变量,声明为抽象
var b: Int = 2 //类中的变量,初始化
}
private fun baz() {
var a: Int //局部变量,可以不初始化
}
1.1.5 字符串
字符串用String类型表示。字符串是不可变的。字符串的元素——字符可以使用索引运算符访问: s[i] 。可以用for循环迭代字符串:
for (c in str) {
println(c)
}
Kotlin有两种类型的字符串字面值: 转义字符串和原生字符串。
转义字符串支持转义字符,很像Java中的字符串:val s = "Hello, world!\n"
原生字符串使用三个引号( “”” )分界符括起来,不支持转义,任何字符都会被原样展示(包括换行、空格、tab等):
val text = """
for (c in "foo")
print(c)
"""
字符串模板
字符串可以包含模板表达式,使用时会用模板表达式的值来替换掉模板表达式。模板表达式以美元符( $ )开头,由一个简单的名字构成,如:
val i = 10
val s = "i = $i" // s为"i = 10"
或者用花括号扩起来的任意表达式:
val s = "abc"
val str = "$s.length is ${s.length}" // str为"abc.length is 3"
转义字符串和原生字符串都支持模板表达式,那如果我们想要的就是$怎么办呢——使用${'$'}
1.1.6 条件表达式
Java中的很多语句,在kotlin中都既是语句,也是表达式(即本身可以代表一个值,可以放在等号右边)。例如if表达式:
//if的分支可以是代码块,最后的表达式的值作为该块的值
val max = if (a > b) {
print("Choose a")
a
} else {
print("Choose b")
b
}
//也可以像下面这样,类似于Java中的?:表达式
val max = if (a > b) a else b
1.1.7 空安全
1.1.7.1 非空类型与可空类型
默认情况下,所有类型都是非空的,即我们告诉编译系统,该类型变量的值不可能是null,因此我们也不能将null赋值给该类型的变量:
var a: String = null //非空类型取值不能为null,编译报错
var b: String = "hello"
val l = b.length //因为b是非空类型的,所以b.length必然是安全的,不可能报出NPE(Null Pointer Exception)
在类型关键字后加上?
,那么就得到了一个可空类型,如String?
:
var b: String? = null //可空类型,取值可以为空
val l = b.length //编译报错,因为b是可空类型,这样调用是不安全的
那么我们想要获得可空的b的length属性该怎么办呢?有两种方法:
(1) 先进行空检测
val l = if (b != null) b.length else -1
//也支持更复杂(更智能的检测)
if (b != null && b.length > 0) {
print("String of length ${b.length}")
} else {
print("Empty string")
}
(2) 使用安全调用操作符“?.”
val l = b?.length
如果b非空,就返回b.length,否则返回null,这个表达式的类型是Int?。
安全调用操作符在链式调用中很有用,例如:
bob?.department?.head?.name
只要任何一个环节为null,那么该链式调用就会返回null。
如果要只对非空值执行某个操作,可以将安全调用操作符与let一起使用:
val listWithNulls: List<String?> = listOf("A", null)
for (item in listWithNulls) {
item?.let { println(it) } //输出A并忽略null
}
1.1.7.2 Elvis操作符
表达式1 ?: 表达式2
含义:如果表达式1不是null,那么就返回表达式1的值,否则就返回表达式2的值。注意,当且仅当表达式1的值为null时,才会对表达式2进行求值。
throw和return在Kotlin中也都是表达式,所以它们也可以用在elvis操作符右侧。这可能会非常方便:
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
……
}
1.1.7.3 !!操作符
这是为NPE爱好者准备的:
var a: String? = null //a可空
val l = a!!.length //如果引用a为null,则抛出NPE,如果不为null,则返回a.length
1.1.7.4 过滤集合中的非空元素
如果你有一个可空类型元素的集合,并且想要过滤得到非空元素,你可以使用filterNotNull来实现:
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
1.1.8 类型检测与转换
1.1.8.1 类型检测与智能类型转换
类型检测使用is或!is操作符。在多数情况下,不需要在Kotlin中使用显式转换操作符,因为编译器会跟踪不可变值的is检查,并在需要时自动进行类型转换:
if (obj is String) {
print(obj.length) //obj被自动转换为String
}
if (obj !is String) {
print("Not a String")
}else {
print(obj.length) //obj被自动转换为String
}
if (x !is String) return
print(x.length) //x被转换为字符串,可见编译器是十分智能的
//||右侧的x被自动转换为字符串
if (x !is String || x.length == 0) return
//&&右侧的x被自动转换为字符串
if (x is String && x.length > 0) {
print(x.length)
}
但是需要注意,当编译器不能保证变量在检查和使用之间不可改变时,智能转换不能用(详见官方参考)。
1.1.8.2 类型转换操作符as
val b:String = a as String
当无法转换时会抛出类型转换异常。
1.1.8.3 安全的类型转换操作符as?
val b:String? = a as? String
当无法转换时不会抛出类型转换异常,而是返回null。
1.1.9 For循环
Java中有普通的for循环和foreach循环(也称为高级for循环),但kotlin中不支持普通的for循环,即你不能像这样使用for循环:for(var i=0;i<100;i++){}
。
kotlin中的for循环都是高级for循环。for循环可以对任何提供迭代器(iterator)的对象进行遍历,语法如下:
for(item in collection){
print(item)
}
例如:
val strs = listOf("1", "2", "3")//List集合
val ints = arrayOf(1, 2, 3)//数组
for (item in strs) {
print(item)
}
for (item in ints) {
print(item)
}
/*
* 也可以通过索引来访问
*/
for (index in strs.indices) {
print(strs[index])
}
for (index in ints.indices) {
print(ints[index])
}
1.1.10 while循环
while循环和do…while循环与Java中的用法一样
val items = listOf("apple", "banana", "kiwi")
var index = 0
while (index < items.size) {
println("item at $index is ${items[index]}")
index++
}
1.1.11 when
when即可以作为语句,也可以作为表达式,它的作用类似于C++和Java中的switch语句,不过使用起来更加灵活。
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> {
print("x is neither 1 nor 2")
}
}
//0,1两个分支执行一样的操作
when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}
//我们可以用任意表达式(而不只是常量)作为分支条件
when (x) {
parseInt(s) -> print("s encodes x")//当x等于parseInt(s)时,执行此分支
else -> print("s does not encode x")
}
//我们也可以检测一个值在(in)或者不在(!in)某个区间或者集合中
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")
}
//另一种可能性是检测某个值是(is)或者不是(!is)某个特定类型的值
val hasPrefix = when(x) {
is String -> x.startsWith("prefix")//会将x智能转换为String类型
else -> false
}
//when也可以用来取代if-else if链。如果不提供参数,所有的分支条件都是简单的布尔表达式,而当一个分支的条件为真时则执行该分支
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}
1.1.12 使用区间(range)
使用..操作符来定义一个区间,如1..10
。任何可比较的类型都可以定义为一个区间。区间常与in和!in配合使用。
if (i in 1..10) { //检测i是否在区间[1,10]之内
println(i)
}
整型区间(IntRange、LongRange、CharRange )有一个额外的特性:它们可以迭代。
前面讲for循环时提到过,kotlin中的for循环类似于Java中的高级for循环,那么如果我们非要使用Java中普通for循环那样的for循环呢,答案就是使用区间:当我们使用for循环来迭代区间时,编译器会将其转换为类似于Java中的普通for循环。
for (i in 1..4) print(i) //输出“1234”
等价于Java中的:
for(int i=1;i<=4;i++){...}
使用downTo操作符来定义一个逆向的区间:for (i in 4 downTo 1) print(i)
,输出“4321”
指定迭代的步长:
for (i in 1..4 step 2) print(i) // 输出“13”
for (i in 4 downTo 1 step 2) print(i) // 输出“42”
使用util操作符定义一个不含尾元素的区间:
for (i in 1 until 10) { // i in [1, 10),排除了10
println(i)
}
1.1.13 使用集合
对集合进行迭代:
for (item in items) {
println(item)
}
使用in运算符来判断集合内是否包含某实例:
when {
"orange" in items -> println("juicy")
"apple" in items -> println("apple is fine too")
}
使用lambda表达式来过滤(filter)和映射(map)集合:
fruits
.filter { it.startsWith("a") }
.sortedBy { it }
.map { it.toUpperCase() }
.forEach { println(it) }
1.2 习惯用法
详见官方参考。建议暂时搁置,最后再看。
1.3 编码规范
详见官方参考。建议暂时搁置,最后再看。