可以在 JetBrains 官方博客中阅读有关 Kotlin 1.4 的更多信息。
JetBrains 官方博客
https://blog.jetbrains.com/zh-hans/kotlin/2020/08/kotlin-1-4-released-with-a-focus-on-quality-and-performance-zh/
Kotlin 1.4 中引入的新语言特性改善了编写 Kotlin 代码的 "人机工程学",例如:
Kotlin 接口的 SAM 转换
过去,只有 Java 编程语言中定义的功能接口 (即只有一个单一抽象方法 - SAM) 受益于 Kotlin 中的速记语法:
executor.execute { println("This is shorthand for passing in a Runnable") }现在,Kotlin 1.4 可以将 Kotlin 接口标记为功能接口,并通过添加 fun 关键字让它们以类似方式工作:
fun interface Transformer<T, U> { fun transform(x: T): U } val length = Transformer { x: String -> x.length }Kotlin 1.4 版本说明页面介绍了有关新语言特性的更多信息,如: 混合命名和位置参数、尾部逗号、可调用引用改进,以及在循环中的 when 内部使用 break和 continue。
版本说明
https://kotlinlang.org/docs/reference/whatsnew14.html#mixing-named-and-positional-arguments
显式 API 模式
另外一个新特性是显式 API 模式,面向 Kotlin 的库作者。
该模式强制执行 Kotlin 的某些语言属性,这些属性通常为可选,例如指定可见性修饰符,以及任何公共声明的显式键入,防止设计库的公共 API 时出错。官方文档说明了如何启用显式 API 模式并开始使用这些附加检查。
官方文档: 显式 API 模式
https://kotlinlang.org/docs/reference/whatsnew14.html#explicit-api-mode-for-library-authors
上述语言特性是 Kotlin 1.4 中密切贴合开发者的一部分改动,其他大部分工作是以提高 Kotlin 编译器的整体质量和性能为主。
现在,所有开发者都可以利用的优势之一是更强大的新类型推断算法 (默认启用),它可以在更多用例中自动推理类型,即使在复杂的场景下也支持智能转换,帮助开发者提高工作效率。
除了类型推断算法,Kotlin 1.4 还为 Kotlin/JVM 和 Kotlin/JS 带来了全新的编译器后端 (Alpha 稳定阶段,可选),用 IR 编译器生成代码并用于 Kotlin/Native 后端。
Jetpack Compose 需要 Kotlin/JVM IR 后端。Google 工程师正在与 JetBrains 合作,希望将其打造成默认的 JVM 编译器后端。
因此,即使您当前不使用 Jetpack Compose 进行开发,我们同样建议您尝试全新 alpha 版 Kotlin/JVM 后端,如果在使用中有任何问题和功能请求请提交到问题跟踪器。
问题跟踪器
http://kotl.in/issue
在 Gradle 构建脚本中指定额外编译器选项即可启用新的 JVM IR 后端:
kotlinOptions.useIR = true项目和 IDE 更新到 Kotlin 1.4 需要完成两个步骤。
首先,确保您使用最新版 Android Studio,以最大限度地提高性能优势以及与最新 Kotlin 插件的兼容性。当与 IDE 版本兼容的 Kotlin 1.4.0 插件可用时,Android Studio 将提示您,或者您也可以转到 Preferences | Plugins 手动触发更新。
Android Studio
https://developer.android.google.cn/studio
启用插件后,您可以更新 build.gradle 脚本中的 Kotlin Gradle 插件版本,将应用项目升级以使用 Kotlin 1.4。根据插件的管理方式,您必须在顶层项目的buildscript 块中更新版本:
buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0" } }或在模块级别的 build.gradle 文件的 plugins 块中更改版本号:
plugins { id 'org.jetbrains.kotlin.android' version '1.4.0' }请务必仔细阅读语言改动并更新项目代码,确保兼容最新版本。快来体验 Kotlin 1.4 吧!
------------------------------以下是技巧说明-------------------------
通过这篇文章你将学习到以下内容,文中会给出相应的答案
如何使用 plus 操作符对集合进行操作?
当获取 Map 值为空时,如何设置默认值?
require 或者 check 函数做什么用的?
如何区分 run, with, let, also and apply 以及如何使用?
如何巧妙的使用 in 和 when 关键字?
Kotlin 的单例有几种形式?
为什么 by lazy 声明的变量只能用 val?
在 Java 中算术运算符只能用于基本数据类型,+ 运算符可以与 String 值一起使用,但是不能在集合中使用,在 Kotlin 中可以应用在任何类型,我们来看一个例子,利用 plus (+) 和 minus (-) 对 Map 集合做运算,如下所示。
fun main() { val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3) // plus (+) println(numbersMap + Pair("four", 4)) // {one=1, two=2, three=3, four=4} println(numbersMap + Pair("one", 10)) // {one=10, two=2, three=3} println(numbersMap + Pair("five", 5) + Pair("one", 11)) // {one=11, two=2, three=3, five=5} // minus (-) println(numbersMap - "one") // {two=2, three=3} println(numbersMap - listOf("two", "four")) // {one=1, three=3} }其实这里用到了运算符重载,Kotlin 在 Maps.kt 文件里面,定义了一系列用关键字 operator 声明的 Map 的扩展函数。
用 operator 关键字声明 plus 函数,可以直接使用 + 号来做运算,使用 operator 修饰符声明 minus 函数,可以直接使用 - 号来做运算,其实我们也可以在自定义类里面实现 plus (+) 和 minus (-) 做运算。
data class Salary(var base: Int = 100){ override fun toString(): String = base.toString() } operator fun Salary.plus(other: Salary): Salary = Salary(base + other.base) operator fun Salary.minus(other: Salary): Salary = Salary(base - other.base) val s1 = Salary(10) val s2 = Salary(20) println(s1 + s2) // 30 println(s1 - s2) // -10在 Map 集合中,可以使用 withDefault 设置一个默认值,当键不在 Map 集合中,通过 getValue 返回默认值。
val map = mapOf( "java" to 1, "kotlin" to 2, "python" to 3 ).withDefault { "?" } println(map.getValue("java")) // 1 println(map.getValue("kotlin")) // 2 println(map.getValue("c++")) // ?源码实现也非常简单,当返回值为 null 时,返回设置的默认值。
internal inline fun <K, V> Map<K, V>.getOrElseNullable(key: K, defaultValue: () -> V): V { val value = get(key) if (value == null && !containsKey(key)) { return defaultValue() } else { @Suppress("UNCHECKED_CAST") return value as V } }但是这种写法和 plus 操作符在一起用,有一个 bug ,看一下下面这个例子。
val newMap = map + mapOf("python" to 3) println(newMap.getValue("c++")) // 调用 getValue 时抛出异常,异常信息:Key c++ is missing in the map.这段代码的意思就是,通过 plus(+) 操作符合并两个 map,返回一个新的 map, 但是忽略了默认值,所以看到上面的错误信息,我们在开发的时候需要注意这点。
那么我们如何在项目中使用呢,具体的用法可以查看我 GitHub 上的项目 DataBindingDialog.kt 当中的用法。
https://github.com/hidhl/JDataBinding/blob/master/jdatabinding/src/main/java/com/hi/dhl/jdatabinding/DataBindingDialog.kt
感谢大神 Elye 的这篇文章提供的思路 Mastering Kotlin standard functions。
https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84
run, with, let, also, apply 都是作用域函数,这些作用域函数如何使用,以及如何区分呢,我们将从以下三个方面来区分它们。
是否是扩展函数。
作用域函数的参数(this、it)。
作用域函数的返回值(调用本身、其他类型即最后一行)。
首先我们来看一下 with 和 T.run,这两个函数非常的相似,他们的区别在于 with 是个普通函数,T.run 是个扩展函数,来看一下下面的例子。
val name: String? = null with(name){ val subName = name!!.substring(1,2) } // 使用之前可以检查它的可空性 name?.run { val subName = name.substring(1,2) }?:throw IllegalArgumentException("name must not be null")在这个例子当中,name?.run 会更好一些,因为在使用之前可以检查它的可空性。
我们在来看一下 T.run 和 T.let,它们都是扩展函数,但是他们的参数不一样 T.run 的参数是 this, T.let 的参数是 it。
val name: String? = "hi-dhl.com" // 参数是 this,可以省略不写 name?.run { println("The length is ${this.length} this 是可以省略的 ${length}") } // 参数 it name?.let { println("The length is ${it.length}") } // 自定义参数名字 name?.let { str -> println("The length is ${str.length}") }在上面的例子中看似 T.run 会更好,因为 this 可以省略,调用更加的简洁,但是 T.let 允许我们自定义参数名字,使可读性更强,如果倾向可读性可以选择 T.let。
接下里我们来看一下 T.let 和 T.also 它们接受的参数都是 it, 但是它们的返回值是不同的 T.let 返回最后一行,T.also 返回调用本身。
var name = "hi-dhl" // 返回调用本身 name = name.also { val result = 1 * 1 "juejin" } println("name = ${name}") // name = hi-dhl // 返回的最后一行 name = name.let { val result = 1 * 1 "hi-dhl.com" } println("name = ${name}") // name = hi-dhl.com从上面的例子来看 T.also 似乎没有什么意义,细想一下其实是非常有意义的,在使用之前可以进行自我操作,结合其他的函数,功能会更强大。
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }当然 T.also 还可以做其他事情,比如利用 T.also 在使用之前可以进行自我操作特点,可以实现一行代码交换两个变量,在后面会有详细介绍
通过上面三个方面,大致了解函数的行为,接下来看一下 T.apply 函数,T.apply 函数是一个扩展函数,返回值是它本身,并且接受的参数是 this。
// 普通方法 fun createInstance(args: Bundle) : MyFragment { val fragment = MyFragment() fragment.arguments = args return fragment } // 改进方法 fun createInstance(args: Bundle) = MyFragment().apply { arguments = args } // 普通方法 fun createIntent(intentData: String, intentAction: String): Intent { val intent = Intent() intent.action = intentAction intent.data=Uri.parse(intentData) return intent } // 改进方法,链式调用 fun createIntent(intentData: String, intentAction: String) = Intent().apply { action = intentAction } .apply { data = Uri.parse(intentData) }以表格的形式汇总,更方便去理解
函数是否是扩展函数函数参数(this、it)返回值(调用本身、最后一行)with不是this最后一行T.run是this最后一行T.let是it最后一行T.also是it调用本身T.apply是this调用本身接下来演示的是使用 T.also 函数,实现一行代码交换两个变量?我们先来回顾一下 Java 的做法。
int a = 1; int b = 2; // Java - 中间变量 int temp = a; a = b; b = temp; System.out.println("a = "+a +" b = "+b); // a = 2 b = 1 // Java - 加减运算 a = a + b; b = a - b; a = a - b; System.out.println("a = " + a + " b = " + b); // a = 2 b = 1 // Java - 位运算 a = a ^ b; b = a ^ b; a = a ^ b; System.out.println("a = " + a + " b = " + b); // a = 2 b = 1 // Kotlin a = b.also { b = a } println("a = ${a} b = ${b}") // a = 2 b = 1来一起分析 T.also 是如何做到的,其实这里用到了 T.also 函数的两个特点。
调用 T.also 函数返回的是调用者本身。
在使用之前可以进行自我操作。
也就是说 b.also { b = a } 会先将 a 的值 (1) 赋值给 b,此时 b 的值为 1,然后将 b 原始的值
(2)赋值给 a,此时 a 的值为 2,实现交换两个变量的目的。
使用 in 和 when 关键字结合正则表达式,验证用户的输入,这是一个很酷的技巧。
// 使用扩展函数重写 contains 操作符 operator fun Regex.contains(text: CharSequence) : Boolean { return this.containsMatchIn(text) } // 结合着 in 和 when 一起使用 when (input) { in Regex("[0–9]") -> println("contains a number") in Regex("[a-zA-Z]") -> println("contains a letter") }in 关键字其实是 contains 操作符的简写,它不是一个接口,也不是一个类型,仅仅是一个操作符,也就是说任意一个类只要重写了 contains 操作符,都可以使用 in 关键字,如果我们想要在自定义类型中检查一个值是否在列表中,只需要重写 contains() 方法即可,Collections 集合也重写了 contains 操作符。
val input = "kotlin" when (input) { in listOf("java", "kotlin") -> println("found ${input}") in setOf("python", "c++") -> println("found ${input}") else -> println(" not found ${input}") }我汇总了一下目前 Kotlin 单例总共有三种写法:
使用 Object 实现单例。
使用 by lazy 实现单例。
可接受参数的单例(来自大神 Christophe Beyls)。
代码:
object WorkSingletonKotlin 当中 Object 关键字就是一个单例,比 Java 的一坨代码看起来舒服了很多,来看一下编译后的 Java 文件。
public final class WorkSingleton { public static final WorkSingleton INSTANCE; static { WorkSingleton var0 = new WorkSingleton(); INSTANCE = var0; } }通过 static 代码块实现的单例,优点:饿汉式且是线程安全的,缺点:类加载时就初始化,浪费内存。
利用伴生对象 和 by lazy 也可以实现单例,代码如下所示。
class WorkSingleton private constructor() { companion object { // 方式一 val INSTANCE1 by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { WorkSingleton() } // 方式二 默认就是 LazyThreadSafetyMode.SYNCHRONIZED,可以省略不写,如下所示 val INSTANCE2 by lazy { WorkSingleton() } } } lazy 的延迟模式有三种:上面代码所示 mode = LazyThreadSafetyMode.SYNCHRONIZED,lazy 默认的模式,可以省掉,这个模式的意思是:如果有多个线程访问,只有一条线程可以去初始化 lazy 对象。
当 mode = LazyThreadSafetyMode.PUBLICATION 表达的意思是:对于还没有被初始化的 lazy 对象,可以被不同的线程调用,如果 lazy 对象初始化完成,其他的线程使用的是初始化完成的值。
mode = LazyThreadSafetyMode.NONE 表达的意思是:只能在单线程下使用,不能在多线程下使用,不会有锁的限制,也就是说它不会有任何线程安全的保证以及相关的开销。
通过上面三种模式,这就可以理解为什么 by lazy 声明的变量只能用 val,因为初始化完成之后它的值是不会变的。
但是有的时候,希望在单例实例化的时候传递参数,例如:
Singleton.getInstance(context).doSome()上面这两种形式都不能满足,来看看大神 Christophe Beyls 在这篇文章给出的方法 Kotlin singletons with argument 代码如下。
class WorkSingleton private constructor(context: Context) { init { // Init using context argument } companion object : SingletonHolder<WorkSingleton, Context>(::WorkSingleton) } open class SingletonHolder<out T : Any, in A>(creator: (A) -> T) { private var creator: ((A) -> T)? = creator @Volatile private var instance: T? = null fun getInstance(arg: A): T { val i = instance if (i != null) { return i } return synchronized(this) { val i2 = instance if (i2 != null) { i2 } else { val created = creator!!(arg) instance = created creator = null created } } } }有没有感觉这和 Java 中双重校验锁的机制很像,在 SingletonHolder 类中如果已经初始化了直接返回,如果没有初始化进入 synchronized 代码块创建对象,利用了 Kotlin 伴生对象提供的非常强大功能,它能够像其他任何对象一样从基类继承,从而实现了与静态继承相当的功能。所以我们将 SingletonHolder 作为单例类伴随对象的基类,在单例类上重用并公开 getInstance()函数。
参数传递给 SingletonHolder 构造函数的 creator,creator 是一个 lambda 表达式,将 WorkSingleton 传递给 SingletonHolder 类构造函数。
并且不限制传入参数的类型,凡是需要传递参数的单例模式,只需将单例类的伴随对象继承于 SingletonHolder,然后传入当前的单例类和参数类型即可,例如:
class FileSingleton private constructor(path: String) { companion object : SingletonHolder<FileSingleton, String>(::FileSingleton) }到这里就结束了,Kotlin 的强大不止于此,后面还会分享更多的技巧,在 Kotlin 的道路上还有很多实用的技巧等着我们一起来探索。
例如利用 Kotlin 的 inline、reified、DSL 等等语法, 结合着 DataBinding、LiveData 等等可以设计出更加简洁并利于维护的代码,更多技巧可以查看我 GitHub 上的项目 JDataBinding。
关注我获取更多知识或者投稿