为数不多的人知道的 Kotlin 技巧以及 原理解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的人开始使用 Kotlin。

image.png


Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的人开始使用 Kotlin。


结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁,但是呢简洁的背后是有代价的,使用不当对性能可能会有损耗,这块往往很容易被我们忽略,这就需要我们去研究 kotlin 语法糖背后的魔法,当我们在开发的时候,选择合适的语法糖,尽量避免这些错误,关于 Kotlin 性能损失那些事,可以看一下我另外两篇文章。



这两篇文章都分析了 Kotlin 使用不当对性能的影响,不仅如此 Kotlin 当中还有很多让人傻傻分不清楚的语法糖例如 run, with, let, also, apply 等等,这篇文章将介绍一种简单的方法来区分它们以及如何选择使用。


通过这篇文章你将学习到以下内容,文中会给出相应的答案


  • 如何使用 plus 操作符对集合进行操作?
  • 当获取 Map 值为空时,如何设置默认值?
  • require 或者 check 函数做什么用的?
  • 如何区分 run, with, let, also and apply 以及如何使用?
  • 如何巧妙的使用 in 和 when 关键字?
  • Kotlin 的单例有几种形式?
  • 为什么 by lazy 声明的变量只能用 val?


plus 操作符



在 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 集合的默认值



在 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, 但是忽略了默认值,所以看到上面的错误信息,我们在开发的时候需要注意这点。


使用 require 或者 check 函数作为条件检查



// 传统的做法
val age = -1;
if (age <= 0) {
    throw IllegalArgumentException("age must  not be negative")
}
// 使用 require 去检查
require(age > 0) { "age must be negative" }
// 使用 checkNotNull 检查
val name: String? = null
checkNotNull(name){
    "name must not be null"
}


那么我们如何在项目中使用呢,具体的用法可以查看我 GitHub 上的项目 DataBindingDialog.kt 当中的用法。


如何区分和使用 run, with, let, also, apply



感谢大神 Elye 的这篇文章提供的思路 Mastering Kotlin standard functions


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 会更好一些,因为在使用之前可以检查它的可空性。


作用域函数的参数(this、it)


我们在来看一下 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 函数,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 函数交换两个变量


接下来演示的是使用 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 关键字



使用 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 的单例三种写法



我汇总了一下目前 Kotlin 单例总共有三种写法:


  • 使用 Object 实现单例。
  • 使用 by lazy 实现单例。
  • 可接受参数的单例(来自大神 Christophe Beyls)。


使用 Object 实现单例


代码:


object WorkSingleton


Kotlin 当中 Object 关键字就是一个单例,比 Java 的一坨代码看起来舒服了很多,来看一下编译后的 Java 文件。


public final class WorkSingleton {
   public static final WorkSingleton INSTANCE;
   static {
      WorkSingleton var0 = new WorkSingleton();
      INSTANCE = var0;
   }
}


通过 static 代码块实现的单例,优点:饿汉式且是线程安全的,缺点:类加载时就初始化,浪费内存。


使用 by lazy 实现单例


利用伴生对象 和 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


参考链接




结语



关注公众号:ByteCode,查看一系列 Android 系统源码、逆向分析、算法、译文、Kotlin、Jetpack 源码相关的文章,如果这篇文章对你有帮助,请帮我点个 star,感谢!!!,欢迎一起来学习,在技术的道路上一起前进。


最后推荐我一直在更新维护的项目和网站:


  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看:AndroidX-Jetpack-Practice
  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析


image.png


  • 最新 Android 10 源码分析系列文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,仓库持续更新,欢迎前去查看 Android10-Source-Analysis
  • 整理和翻译一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的解读,仓库持续更新,欢迎前去查看 Technical-Article-Translation
  • 「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址,欢迎前去查看 为互联网人而设计导航网站


历史文章



目录
相关文章
|
17天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
35 3
|
1月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
5天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
16 1
|
10天前
|
数据采集 存储 编解码
一份简明的 Base64 原理解析
Base64 编码器的原理,其实很简单,花一点点时间学会它,你就又消除了一个知识盲点。
37 3
|
29天前
|
前端开发 Java 应用服务中间件
21张图解析Tomcat运行原理与架构全貌
【10月更文挑战第2天】本文通过21张图详细解析了Tomcat的运行原理与架构。Tomcat作为Java Web开发中最流行的Web服务器之一,其架构设计精妙。文章首先介绍了Tomcat的基本组件:Connector(连接器)负责网络通信,Container(容器)处理业务逻辑。连接器内部包括EndPoint、Processor和Adapter等组件,分别处理通信、协议解析和请求封装。容器采用多级结构(Engine、Host、Context、Wrapper),并通过Mapper组件进行请求路由。文章还探讨了Tomcat的生命周期管理、启动与停止机制,并通过源码分析展示了请求处理流程。
|
26天前
|
开发框架 缓存 前端开发
electron-builder 解析:你了解其背后的构建原理吗?
本文首发于微信公众号“前端徐徐”,详细解析了 electron-builder 的工作原理。electron-builder 是一个专为整合前端项目与 Electron 应用的打包工具,负责管理依赖、生成配置文件及多平台构建。文章介绍了前端项目的构建流程、配置信息收集、依赖处理、asar 打包、附加资源准备、Electron 打包、代码签名、资源压缩、卸载程序生成、安装程序生成及最终安装包输出等环节。通过剖析 electron-builder 的原理,帮助开发者更好地理解和掌握跨端桌面应用的构建流程。
58 2
|
1月前
|
搜索推荐 Shell
解析排序算法:十大排序方法的工作原理与性能比较
解析排序算法:十大排序方法的工作原理与性能比较
46 9
|
7天前
|
供应链 安全 分布式数据库
探索区块链技术:从原理到应用的全面解析
【10月更文挑战第22天】 本文旨在深入浅出地探讨区块链技术,一种近年来引起广泛关注的分布式账本技术。我们将从区块链的基本概念入手,逐步深入到其工作原理、关键技术特点以及在金融、供应链管理等多个领域的实际应用案例。通过这篇文章,读者不仅能够理解区块链技术的核心价值和潜力,还能获得关于如何评估和选择适合自己需求的区块链解决方案的实用建议。
26 0

推荐镜像

更多