作为一个Android菜鸡,很汗颜到现在还没学Kotlin,更汗颜神级教科书《第一行代码》竟然还没看过。借这个机会,我终于要“一雪前耻”。也很推荐有一定基础的Android开发同学可以通过《第一行代码》第3版中每一章的末尾去学习Kotlin。这篇文章简单总结一下我个人认为的,有Java基础的前提下,需要关注的Kotlin语法。
基本语法
变量修饰符
val、var可推导字段类型。const用于修饰编译时已知值的常量,但只能放于顶层或者object(匿名类、伴生类、单例类)中。
函数修饰符
不修饰默认是public级别。 跟java不同,protected只对当前类与子类可见,对同一包路径下的类不是可见。 没有default,多了internal,对同模块下的类可见。
类修饰符
data class,数据类,用data声明后,会根据主构造函数中的参数自动生成hashCode()、equals()、toString()方法:
data class Cellphone(val brand: String, val price: Double)
object class,单例类,不用再写各种饿汉懒汉双重锁了,一个object就搞定:
object Singleton {
fun test(){}
}
调用方式类似Java中的静态方法:Singleton.test()
构造函数
分主构造函数与次构造函数 与Java一样,写主构造函数时同样要调用父类的构造函数,所以这一行的作用就跟Java里的super()是一样的
class Student(val sno: String, var grade: Int, name: String, age: Int): Person(name, age)
注意构造函数中的参数。其中sno与grade被声明成val与var,name与age没有。那么Student中的函数是调用不到name与age的,但可以在构造函数、init函数以及定义成员变量时使用。
次构造函数必须主动(直接或间接地)调用主构造函数:
// 直接
constructor(name: String, age: Int) : this("", 0, name, age) {
}
// 间接,先调用的上一个次构造函数,再调用的主构造函数
constructor() : this("", 0) {
}
目前理解下来,次构造函数一方面是实现了多态,一方面是做到将构造函数中的参数赋初始值。但实际上使用的会很少,因为主构造函数支持设置默认值,并且调用构造函数时也支持用键值对的方式构造:
class Student(val sno: String = "123", val grade: Int = 3, name: String, age: Int): Person(name, age){}
Student(name = "xiaozhi", age = 18) // 这样构造时,sno与grade就使用了默认值
静态方法调用
kotlin没有java中的静态方法,如果想像Java中那样调的话,一个是用object将类修饰为单例,就可以调它下面的所有方法了。但若不想让所有方法都能这样调,只能借助伴生类来实现:
class Student {
companion object {
fun eatLunch() {
}
}
}
Student.eatLaunch()
但这种方式只是调用方式跟静态方法一样,还不是真正的静态方法,而且Java代码就没法调用Student.eatLaunch()了。这个时候我们可以将eatLaunch()加上JvmStatic注解,就真正成为静态方法了。
companion object {
@JvmStatic
fun eatLunch() {
}
}
另外,顶层方法也是静态方法,即直接写在文件中的不放在class下的方法:
fun main() {
}
顶层方法在任何kotlin代码中都能直接调用,不用写类名。在Java代码中,调用的方式是文件名.main()。假设该文件名为LK,Kotlin会自动创建一个LK的Java类,而main函数就是其中的静态方法。
延迟初始化
因为kotlin默认参数与变量不为空,所以要声明一个空的成员变量只能这样声明:
private var str: String? = null
而且在使用时还必须str?.这样调用,很麻烦。所以有lateinit关键字可以帮我们延迟初始化:
private lateinit var str: String
我们只用保证调用前str被赋值了就可以了,若没赋值会抛出UninitializedPropertyAccessException异常。另外可以用::str.isInitialized判断是否已经完成了初始化。
内部类与嵌套类
kotlin中没有静态内部类,只有内部类与嵌套类。内部类用inner修饰,嵌套类则没有inner。
内部类与嵌套类的区别是,内部类会持有外部类的一个对象引用,所以内部类可以访问外部类的方法与属性,嵌套类则不行。
class OutClass {
private val name = "name"
fun outFunc() {
}
inner class InnerClass {
private val sex = "sex"
fun hello() {
print(name)
print(this@OutClass.name)
print(this.sex)
outFunc()
}
}
class NestClass {
fun hello() {
}
}
}
如上所示,InnerClass是内部类,NestClass是嵌套类。在InnerClass#hello()方法中,可以访问到OutClass的name以及outFunc()方法。当内部类中有外部类的同名属性时,不指定this的情况下访问的是内部类自身的属性。指定this@外部类类名.属性时,访问的则是外部类的属性。NestClass无法访问到外部类中的属性与方法。
集合
kotlin支持listOf、mutableListOf、setOf、mutableSetOf、mapOf、mutableMapOf创建集合,并用for in 遍历集合。
map的访问与读取建议用map[key]=value的方式。
常用的函数式API
list.maxBy ({ str: String -> str.length })
list.maxBy (){ str: String -> str.length }
list.maxBy { str: String -> str.length }
list.maxBy { str -> str.length }
val maxIt = list.maxBy { it.length }
最后一行由上面几行简化而来,本质上是一个lambda表达式,得到最大长度的元素。 类似的还有
// 将每个元素做映射操作后得到新的集合
val newList = list.map { it.toUpperCase() }
// 过滤之后再做映射
list.filter { it.length > 5 }.map { it.toUpperCase() }
// 是否任意一个元素满足某个条件
val any = list.any { it.length > 5 }
// 是否所有元素满足某个条件
val all = list.all { it.length > 5 }
语法糖
判空
kotlin默认所有参数和变量都不为空,所以调用study方法时传null,编译器是会报红的
fun doStudy(study: Study) {
study.doHomework()
study.readBooks()
}
但如果给Study加一个?,就代表这个参数可以为空了。可以为空代表着这个函数可能会发生NPE,所以内部逻辑仍然需要我们进行处理,否则study.doHomework()也会报红。
fun doStudy(study: Study?) {
study?.doHomework() // 为空时什么也不做
study?.readBooks()
}
结合let函数可以更加优雅,不需要每次使用参数的时候都写?
fun doStudy(study: Study?) {
study?.let {
it.doHomework()
it.readBooks()
}
}
还有?:的用法。左边不为空返回左边的值,为空返回右边的值。
fun getTextLength(text: String?) = text?.length ?: 0 // 若text为空,则text?.length返回空,函数返回0。否则返回text.length
标准函数
除了上面提到的let函数用于辅助判空外,还有常见的with,run,apply。
with常用于频繁使用某个参数/变量时,将该参数/变量作为上下文,相当于放进去了一个this,从而减少代码上的直接使用:
with(Student()) {
read()
readBooks()
doHomework()
}
run函数的作用于with是一样的,只是用法不同,同样最后一行作为返回值:
Student().run {
read()
readBooks()
doHomework()
}
apply方法作用也是类似,不同的一点是它不是返回最后一行作为返回值,而是返回调用对象本身作为返回值,常见的用法是:
val intent = Intent().apply {
putExtra("param1", "data1")
putExtra("param2", "data2")
}
get()与set()
kotlin class会自动给成员变量赋予get与set方法,且调用时也提供了语法糖:
open class Person(name: String, age: Int) {
var sex = 0
}
// 调用
person.sex = 1
sex变量自动生成了get与set方法。当我们想改默认生成的方法时,需要在变量下方重写get与set:
open class Person(name: String, age: Int) {
var sex = 0
get() = field
set(value) {
field = value
}
}
这就是默认生成的逻辑。其中field就代指sex这个成员变量。注意在get和set中不能显式调用到sex或者是this.sex,因为这样就相当于无限递归调用了。
函数
扩展函数
扩展函数即可以给任何一个类扩展任何函数,扩展方式定义格式为fun ClassName.MethodName()的方法:
fun String.lettersCount(): Int {
var count = 0
for (char in this) {
if (char.isLetter()) {
count++
}
}
return count;
}
如上就定义了String类的扩展函数,建议都写在一个文件中,以顶层函数的形式存在,调用方式:"abc".lettersCount()。扩展函数中自动拥有该类实例的上下文,所以就不用传入String实例了,用的最多的地方就是工具类。
高阶函数
高阶函数即一个将函数作为参数的函数,如:
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int{
val result = operation(num1, num2)
return result
}
其中operation就是一个函数参数。调用方式:
num1AndNum2(1, 2) {
n1, n2 -> n1 + n2
}
或者先声明一个plus函数,再将plus函数传入:
fun plus(num1: Int, num2: Int): Int {
return num1+num2
}
num1AndNum2(1,2, ::plus)
当我们给函数参数前面加上ClassName.时,就赋予了函数参数的运行上下文:
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
其中() -> Unit前面有StringBuilder.,即运行时的this就是StringBuilder实例,这样调用:
StringBuilder().build {
append("2")
append("3")
}
内联函数
还是拿高阶函数举例:
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int{
val result = operation(num1, num2)
return result
}
// 调用
var a = num1AndNum2(1, 2) {
n1, n2 -> n1 + n2
}
在Java中是没有函数参数这个类型的,Kotlin在编译过程中,是将函数参数转换成了匿名类去实现。而每个匿名类都要占用内存,因此需要内联函数解决这个问题,即给函数声明inline:
inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int{
val result = operation(num1, num2)
return result
}
字面意思看,就是将调用高阶函数的地方,变成了内联的。调用时的逻辑在编译时会直接转换为:
var a = 1+2,就没有高阶函数的运行时开销了。
另外,正因为内联函数在编译时会进行转换,所以它同非内联函数还有一个明显的区别:内联函数在lambda表达式中是可以return的。
fun main() {
var a = num1AndNum2(1, 2) {
n1, n2 -> n1 + n2
return@num1AndNum2 0 // 若num1AndNum2是非内联函数,只能以return num1AndNum2的方式局部返回
return // 若num1AndNum2是内联函数,这样return就是直接返回main函数
}
}
noinline
当高阶函数中有多个函数参数,且这几个函数参数有些是需要内联,有些是不需要内联的,就需要借助noinline关键字了:
inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int, noinline operation2: (Int, Int) -> Int): Int{
val result = operation(num1, num2)
return result
}
crossinline
前面说了,内联函数是可以return的,那么这种情况下的内联函数就会报红:
inline fun errorInlineFunc(crossinline funcArgs: () -> Int): Int{
Runnable {
funcArgs()
}
return 1
}
之所以会报红,是因为我们在Runnable的lambda表达式中使用了函数参数,而这个函数参数是可以return的。那么在Runnable匿名类中如果return,是无法return到外层调用函数的。
解决这个问题的方式就是给函数参数加crossinline关键字。crossinline关键字更像是一个约束,一旦声明,那么这个内联函数的函数参数就不可以使用return返回给外层调用函数了,但仍然可以进行局部返回:
errorInlineFunc { return } // 报错
errorInlineFunc { return@errorInlineFunc 0} // 允许局部返回
协程
协程构建
我们可以使用GlobalScope.launch创建一个协程:
fun main() {
GlobalScope.launch {
println("codes run in coroutine scope")
}
}
但GlobalScope.launch创建的协程,是会随着外部线程的结束而结束的。这里并不会打印出来这行代码,是因为协程还没来得及执行,外面线程就结束了。若外面线程挂起1000ms,就可以打印出来了:
fun main() {
GlobalScope.launch {
println("codes run in coroutine scope")
}
Thread.sleep(1000)
}
但我们如果用delay函数挂起协程,当然也就不会再打印了:
fun main() {
GlobalScope.launch {
delay(1000)
println("codes run in coroutine scope")
}
Thread.sleep(1000)
}
这时我们可以使用runBlocking去创建协程,顾名思义runBlocking创建的协程是会挂起外部的线程的:
fun main() {
runBlocking {
println("codes run in coroutine scope")
}
}
这样就会打印了。同时,我们可以在协程中创建N个子协程,因为创建协程的花销要比创建线程小很多,所以创建10万个都没有问题。这里用launch函数去创建子协程:
fun main() {
runBlocking {
launch {
println("11")
delay(1000)
println("111")
}
launch {
println("22")
delay(1000)
println("222")
}
}
}
需要注意,所有的子协程会随着父协程的结束而结束,且launch函数只能在协程的作用域中使用。若放到main函数中是没法调用launch函数的。那我们假设要把某个launch抽成一个函数,那么函数的声明肯定是不在协程作用域中的,该怎么声明呢?这里用coroutineScope函数:
fun launchFunc() {
coroutineScope {
launch {
}
}
}
coroutineScope函数可以让当前函数处于调用时的协程作用域中。但上面的代码其实是会报错的,原因是coroutineScope函数本身也是一个挂起函数,它要么需要处于携程作用域中,要么需要写在挂起函数中。所以我们可以给launchFunc函数声明为挂起函数,这样就不会报错了:
suspend fun launchFunc() {
coroutineScope {
launch {
}
}
}
因为launchFunc函数已经声明为挂起函数了,我们也可以在其中使用delay函数。所以coroutineScope函数与runBlocking函数是有一点像的,都是创建了一个协程并挂起了外部协程/线程。区别是前者挂起的是外部协程,后者挂起的是外部线程。runBlocking若在主线程使用,就会直接阻塞主线程,因此要谨慎使用。
上面说了四种协程构造器GlobalScope.launch、runBlocking、launch、coroutineScope。GlobalScope.launch和runBlocking在任何地方都能调用,launch只能在协程作用域中调用,coroutineScope只能在协程作用域或者挂起函数中调用。
runBlocking因为会阻塞线程,所以不建议在实际项目中运用。GlobalScope.launch创建的是一个顶层协程,实际项目中用起来管理也比较麻烦:
val job = GlobalScope.launch {
delay(1000)
println("codes run in coroutine scope")
}
job.cancel()
每当页面回收时,都需要去调用job#cancel方法去取消每一个创建的Job。所以在创建协程时,比较实用的用法是这样的:
val job = Job()
CoroutineScope(job).launch {
launch {
}
launch {
}
}
job.cancel()
调用CoroutineScope(job)去创建一个CoroutineScope,然后再调用launch函数创建一个协程。在需要回收的时候,调用job.cancel()将所有子协程取消。
获取协程执行结果
在协程作用域下,我们可以使用async函数去新建一个子协程,并返回Deferred对象。Deferred对象执行await函数时,会挂起父协程,直到获取到子协程返回的结果:
val launchJob = CoroutineScope(job).launch {
val deferred1 = async {
1 + 1
}.await()
val deferred2 = async {
1 + 1
}.await()
println("result:$deferred1 and $deferred2")
}
上图中deferred1、deferred2、println都是串行的。可以这样优化一下:
val launchJob = CoroutineScope(job).launch {
val deferred1 = async {
1 + 1
}
val deferred2 = async {
2 + 2
}
println("result:${deferred1.await()} and ${deferred2.await()}")
}
优化后,deferred1、deferred2就是并行的了,然后再执行println。
另外还有withContext函数,它的作用类似于async+await,同样有挂起与获取返回结果的作用:
val withContext = withContext(Dispatchers.Default) {
3 + 3
}
println("result:$withContext}")
不同的是,withContext函数必须要指明线程参数,即Dispatchers。线程参数用来指定所创建的协程分属于哪个线程。当我们进行网络请求时,即使是处于主线程下的协程中执行,也一样会报错,所以此时就需要指明Dispatchers.IO,代表是由子线程创建的高并发协程。Dispatchers.DEFAULT对应由子线程创建的低并发协程。Dispatchers.MAIN对应由主线程创建的协程。
事实上,除了coroutineScope构造器外,所有的构造器都可以指定线程参数,只不过只有withContext需要强行指定罢了。
使用协程简化回调的写法
在Java中写回调时,通常是这样的方式:
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(response: String) {
// 得到服务器返回的具体内容
}
override fun onError(e: Exception) {
// 在这里对异常情况进行处理
}
})
但我们可以用suspendCoroutine包一下:
suspend fun request(address: String): String {
return suspendCoroutine { continuation -> {
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(response: String) {
continuation.resume(response)
// 得到服务器返回的具体内容
}
override fun onError(e: Exception) {
// 在这里对异常情况进行处理
continuation.resumeWithException(e)
}
})
} }
}
suspendCoroutine是一个挂起函数且只能在协程作用域中使用,它会挂起当前协程并在一个普通线程中执行逻辑。上面代码中,在普通线程中发起一个网络请求得到响应后,再调用continuation.resume(response)或者continuation.resumeWithException(e)返回。那么调用处就可以这样使用:
try {
val response = request("https://test.com")
} catch (e: Exception) {
}
这样就不用写回调了。类似的回调都可以用suspendCoroutine去包掉。
进阶语法
类型别名
typealias关键字允许我们给任意类型设置一个别名,常用于给函数类型设置别名,因为函数类型通常都比较长,写起来会比较麻烦:
private var callback: ((Boolean, List<String>) -> Unit)? = null
fun request(callback: (Boolean, List<String>) -> Unit, vararg permissions: String,) {
this.callback = callback
requestPermissions(permissions, 1)
}
如上代码我们可以用typealias进行优化:
typealias PermissionCallback = (Boolean, List<String>) -> Unit
class InvisibleFragment : Fragment() {
private var callback: PermissionCallback? = null
fun request(callback: PermissionCallback, vararg permissions: String) {
this.callback = callback
requestPermissions(permissions, 1)
}
}
可以看到,原先的函数类型成了PermissionCallback,使用起来方便了很多。
密封类
当我们用when时,编译器要求我们必须写else,否则就会报红,密封类就可以解决这个问题:
interface MyResult
class SuccessResult: MyResult
class FailedResult: MyResult
fun handleResult(result: MyResult) = when(result) {
is SuccessResult -> {
print(11)
}
is FailedResult -> {
print(22)
}
else -> {
print(33)
}
}
密封类要求类及其子类必须定义在同一文件中的顶层位置:
sealed class MyResult
class SuccessResult: MyResult()
class FailedResult: MyResult()
这样就不用写else了:
fun handleResult(result: MyResult) = when(result) {
is SuccessResult -> {
print(11)
}
is FailedResult -> {
print(22)
}
}
而且,当增加一个MyResult的子类时,when那边如果没有添加相应的分支,编译器就会报红,保证了我们逻辑的完整性。
重载运算符
任何类都可以重载运算符,从而使得+、-、*、/等运算法具有意义。
class Money(val value: Int) {
operator fun plus(money: Money) : Money {
return Money(this.value + money.value)
}
operator fun plus(value: Int) : Money {
return Money(this.value + value)
}
}
operator重载了+号,因此可以这样使用:
val money = Money(1) + 33
val money1 = Money(1) + Money(2)
类似的-对应的函数名为minus,*对应times,/对应div。
委托
委托的关键字是by。
委托类
class MySet<T>(helperSet: HashSet<T>): Set<T> by helperSet {
override fun isEmpty(): Boolean {
return false
}
fun helloWorld() {
}
}
如上代码,MySet不必实现Set的所有接口,因为这些接口都委托给了helperSet去实现。当然,MySet可以拥有自己的方法并重写其中的方法。
委托属性
委托属性就是将该属性的get与set方法交由某个类去执行,基本语法结构如下:
var p by Delegate()
by是委托关键字,Delegate就是被委托类。Delegate必须按照要求重写getValue与setValue方法:
class Delegate {
var propValue: Any? = null
operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? { return propValue }
operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) { propValue = value }
}
常见的场景是by lazy,lazy是返回被委托类的一个方法,类似的我们可以自己实现一个被委托类:
fun <T> later(block: () -> T): Later<T> {
return Later(block)
}
class Later<T>(val block: () -> T) {
private var value: Any? = null
operator fun getValue(any: Any?, prop: KProperty<*>): T {
if (value == null) {
value = block()
}
return value as T
}
}
// 调用
val abc by later {
val abc = "123"
abc
}
当某处代码用到abc时,实际调用的是Later的getValue方法,会执行后面的labmda表达式初始化去初始化abc。
infix
infix用来将方法调用更语义化地表示出来,如"a" to "b"可以生成一个Pair,这个to就是一个infix方法。类似的我们可以有这样的定义:
// 跟to作用相同,生成一个Pair
infix fun <A, B> A.with(that: B) = Pair(this, that)
infix fun String.beginWith(str: String) : Boolean = startsWith(str)
infix fun <T> Collection<T>.has(ele: T) = contains(ele)
定义完之后就可以这样调用了:
val b = "abc" beginWith "a"
val b1 = listOf(1, 2, 3) has 1
另外infix有两个限制。一个是不能成为顶层函数,通常通过扩展函数的形式成为某个类的成员函数。二是必须且只能接收一个参数。
泛型
泛型的基本用法跟Java是差不多的,这里介绍比较常用的泛型特性,一个是泛型实化,一个是泛型协变/逆变。
泛型实化
Java中可以获取到MainActivity.class,但肯定不能获取到泛型T.class,Kotlin可以做到这一点。需要借助inline与reified来实现:
inline fun <reified T> getGenericType(obj: T) = T::class.java
getGenericType(1) 得到的会是java.lang.Integer
在实战中可以封装startActivity方法:
inline fun <reified T> startActivity(context: Context) {
val intent = Intent(context, T::class.java)
context.startActivity(intent)
}
startActivity<MainActivity>(this)
泛型协变与逆变
在Java中,List<Set>不是List<Collection>的子类,因为这样做会有类型转换问题:
private void handleList(List<Collection> collection) {
List<Set> set = (List<Set>)collection;
}
当往handleList方法传入List<List>时就会发生类型转换问题了。但在kotlin中却可以。只需要定义好泛型是out的,即对这个泛型,只能读,不能写,那么就能避免这个问题。kotlin中的List定义如下:
public interface List<out E> : Collection<E> {}
相反地,逆变可以让A继承于B,泛型<B>继承于泛型A,继承关系反了一下,用int去声明泛型就可以,如Comparable的声明:
interface Comparable<in T> { operator fun compareTo(other: T): Int }
总而言之,协变(out)与逆变(in),都是规范了泛型参数的输入或输出,防止代码出现类型转换异常。
Java与Kotlin代码间的互相转换
Java转Kotlin
两种方式。一种是将Java代码直接拷贝到任意一个Kotlin文件中。第二种方式是在AS中右键Java文件,选择Convert Java File to Kotlin File
Koltin转Java
Kotlin直接转Java,AS是不支持的,因为Kotlin应有很多语法糖AS识别不了。但我们可以点击Tools->Kotlin->show kotlin bytecode。查看Kotlin生成的字节码,再点击Decompile反编译,就能看到反编译后的Java代码。这有助于理解Kotlin语法糖背后的实现原理。
MVVM
书里还额外提到了MVVM架构,这里也记录一下分层模式。
- UI层
- ViewModel层:UI层调用,存放UI相关数据
- Repository层:仓库层,是缓存数据与网络数据的中转站,由ViewModel调用
- Network层:包装各种Service的接口,由仓库层调用
- DAO层:存放本地缓存数据与持久化数据,由仓库层调用
- Service:写网络请求并将响应封装为Model
《第一行代码》天气APP架构:
文章不足之处,还望大家多多海涵,多多指点,先行谢过!