Stack Overflow 上最热门的 10 个 Kotlin 问题

简介: 这是 Stack Overflow 上最热门的几个 Kotlin 问题,每个问题如果更深入的分析,都可以单独写一篇文章,后面我会针对这些问题,在进一步的分析。

image.png


hi 大家好,我是 DHL。公众号:ByteCode ,专注分享最新技术原创文章,涉及 Kotlin、Jetpack、算法动画、数据结构 、系统源码、 LeetCode / 剑指 Offer / 多线程 / 国内外大厂算法题 等等。



这是 Stack Overflow 上最热门的几个 Kotlin 问题,每个问题如果更深入的分析,都可以单独写一篇文章,后面我会针对这些问题,在进一步的分析。


通过这篇文章你将学习到以下内容:


  • Array<Int>IntArray 的区别,以及如何选择
  • IterableSequence 的区别,以及如何选择
  • 常用的 8 种 For 循环遍历的方法
  • 在 Kotlin 中如何使用 SAM 转换
  • 如何声明一个静态成员,Java 和 Koltin 进行互操作
  • 为什么 kotlin 中的智能转换不能用于可变属性,如何才能解决这个问题
  • 当重写 Java 函数时,如何决定参数的可空性
  • 如何在一个文件中使用多个具有相同名称的扩展函数和类


译文



Array 和 IntArray 的区别


Array


Array<T> 可以为任何 T 类型存储固定数量的元素。它和 Int 类型参数一起使用, 例如 Array<Int>,编译成 Java 代码,会生成  Integer[] 实例。我们可以通过 arrayOf 方法创建数组。


val arrayOfInts: Array<Int> = arrayOf(1, 2, 3, 4, 5)


IntArray


IntArray 可以让我们使用基础数据类型的数组,编译成 Java 代码,会生成 int[] (其它的基础类型的数组还有 ByteArray , CharArray 等等), 我们可以通过 intArrayOf 工厂方法创建数组。


val intArray: IntArray = intArrayOf(1, 2, 3, 4, 5)


什么时候使用 Array<Int> 或者 IntArray


默认使用 IntArray,因为它的性能更好,不需要对每个元素进行装箱。IntArray 进行初始化的时候,默认将每个索引的值初始化为 0,代码如下所示。


val intArray = IntArray(10)
val arrayOfInts = Array<Int>(5) { i -> i * 2 }


Array<Int> 的性能比较差,会对每个元素进行装箱,如果你需要创建包含 null 值的数组,Kotlin 也提供了 arrayOfNulls 方法,帮助我们进行创建。


val notActualPeople: Array<Person?> = arrayOfNulls<Person>(13)

Iterable 和 Sequence 的区别


Iterable


Iterable 对应 Java 的 java.lang.Iterable, Iterable 会立即处理输入的元素,并返回一个包含结果的新集合。我们来举一个简单的例子 返回年龄 > 21 前 5 个人的集合


val people: List<Person> = getPeople()
val allowedEntrance = people
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)


  • 首先通过 filter 函数检查每个人的年龄,将结果放入到一个新的结果集中
  • 通过 map 函数对上一步得到的结果进行名字映射,然后生成一个新的列表 list<String>
  • 通过 take 函数返回前 5 个元素,得到最终的结果集


Sequence


Sequence 是 Kotlin 中一个新的概念,用来表示一个延迟计算的集合。Sequence 只存储操作过程,并不处理任何元素,直到遇到终端操作符才开始处理元素,我们也可以通过 asSequence 扩展函数,将现有的集合转换为 Sequence ,代码如下所示。


val people: List<Person> = getPeople()
val allowedEntrance = people.asSequence()
  .filter { it.age >= 21 }
  .map { it.name }
  .take(5)
  .toList()


在这个例子中, toList() 表示终端操作符,filtermaptake 都是中间操作符,返回 Sequence 实例,当 Sequence 遇到中间操作符时,只是存储操作过程,并不参与计算,直到遇到 toList()


Sequence 的好处它不会生成中间结果集,直接对原始列表中的每一个人重复这个步骤,直到找到 5 个人,返回最终的结果集。


应该如何选择


如果数据量比较小,可以使用 Iterable。虽然会创建中间结果集,在数据不大的情况下,对性能的影响不会很严重。


如果处理的数据量比较大,Sequence 是最好的选择,因为不会创建中间结果集,内存开销更小。


常用的 8 种 For 循环遍历方法


我们经常会使用以下方法进行遍历。


for (i in 0..args.size - 1) {
  println(args[i])
}


但是 Array 有一个可读性更强的扩展属性 lastIndex


for (i in 0..args.lastIndex) {
  println(args[i])
}


但是实际上我们不需要知道最后一个索引,有一个更加简单的写法。


for (i in 0 until args.size) {
  println(args[i])
}


当然你也可以使用下标扩展属性 indices 得到它的范围。


for (i in args.indices) {
  println(args[i])
}


还有一个更加直接的写法,通过下面的方式直接迭代集合。


for (arg in args) {
  println(arg)
}


您也可以使用 forEach 函数,传递一个 lambda 表达式来处理每个元素。


args.forEach { arg ->
  println(arg)
}


它们生成的 Java 代码都非常的相似,在这些例子中,都增加一个索引变量,并在循环中通过索引获取元素。但是如果我们迭代的是 List,最后两个例子底层使用 Iterator,而其他的例子仍是通过索引获取元素。另外还有两个遍历的方法:


  • withIndex 函数,它返回一个 Iterable 对象,该对象可以被解构为当前索引和元素。


for ((index, arg) in args.withIndex()) {
    println("$index: $arg")
}


  • forEachIndexed 函数,它为每个索引和参数提供了一个 lambda 表达式。


args.forEachIndexed { index, arg ->
    println("$index: $arg")
}


如何使用 SAM 转换


可以通过 lambda 表达式实现 SAM 转换,从而使代码更简洁,可读性更强,我们来看一个例子。


在 Java 中定义一个 OnClickListener 接口,并声明一个 onClick 的方法。


public interface OnClickListener {
    void onClick(Button button);
}


我们给 Button 添加 OnClickListener 监听器,每次点击的时候都会被调用。


public class Button {
    public void setListener(OnClickListener listener) { ... }
}


在 Kotlin 中常见的写法,创建匿名类,实现 OnClickListener 接口。


button.setListener(object: OnClickListener {
    override fun onClick(button: Button) {
        println("Clicked!")
    }
})


如果我们使用 SAM 转换,将使代码更简洁,可读性更强。


button.setListener {
    fun onClick(button: Button) {
        println("Clicked!")
    }
}


如何声明一个静态成员,Java 和 Koltin 进行互操作


一个普通的类可以有静态成员和非静态成员,在 Kotlin 中我们常把静态成员放到 companion object 中。


class Foo {
    companion object {
        fun x() { ... }
    }
    fun y() { ... }
}


我们也可以用 object 来声明一个单例,替换 companion object


object Foo {
    fun x() { ... }
}


如果你不想总是通过类名去调用 Foo.x(), 而只是想使用 x(),可以声明为顶级函数。


fun x() { ... }


另外想在 Java 中调用 Kotlin 中静态方法,需要添加 @JvmStatic@JvmName 注解。用一张表格汇总一下,如何在 Java 中调用 Kotlin 中的代码。


Function declaration Kotlin usage Java usage
Companion object Foo. F () Foo. Companion. F ();
Companion object with @JvmStatic Foo. F () Foo. F ();
Object Foo. F () Foo. INSTANCE. F ();
Object with @JvmStatic Foo. F () Foo. F ();
Top level function f () UtilKt. F ();
Top level function with @JvmName f () Util. F ();


同样的规则也适用于变量,@JvmField 注解用于变量上,加上 const 关键字,编译时可以将常量值内联到调用处。


Variable declaration Kotlin usage Java usage
Companion object X. X X. Companion. GetX ();
Companion object with @JvmStatic X. X X. GetX ();
Companion object with @JvmField X. X X. X;
Companion object with const X. X X. X;
Object X. X X. INSTANCE. GetX ();
Object with @JvmStatic X. X X. GetX ();
Object with @JvmField X. X X. X
Object with const X. X X. X;
Top level variable X. X ConstKt. GetX ();
Top level variable with @JvmField X. X ConstKt. X;
Top level variable with const x ConstKt. X;
Top level variable with @JvmName x Const. GetX ();
Top level variable with @JvmName and @JvmField x Const. X;
Top level variable with @JvmName and const x Const. X;

为什么 kotlin 中的智能转换不能用于可变属性


我们先来看一段有问题的代码。


class Dog(var toy: Toy? = null) {
    fun play() {
        if (toy != null) {
            toy.chew()
        }
    }
}


上面的代码在编译时无法通过,异常信息如下所示。


Kotlin: Smart cast to 'Toy' is impossible, because 'toy' is a mutable property that could have been changed by this time


出现这个问题的原因在于,执行完 toy != null 之后和 toy.chew() 方法被调用之间,这个 Dog 的实例可能被另外一个线程修改,这可能会出现 NullPointerException 异常。


如何才能解决这个问题呢


只需要将变量设置为不可变的,即用 val 声明,那么上面的问题就不存在,默认情况将所有的变量都用 val 声明,除非有必要的时候,才将它们设置为 var


如果一定要声明为 var ,那么可以使用局部不可变的副本来解决这个问题。修改一下上面的代码,如下所示。


class Dog(var toy: Toy? = null) {
    fun play() {
        val _toy = toy
        if (_toy != null) {
            _toy.chew()
        }
    }
}


但是还有一个更简洁的写法。


class Dog(var toy: Toy? = null) {
    fun play() {
        toy?.length
    }
}


当重写 Java 函数时,如何决定参数的可空性


在 Java 中定义一个 OnClickListener 接口,并声明一个 onClick 的方法。


public interface OnClickListener {
    void onClick(Button button);
}


在 Kotlin 中实现这个接口,并通过 IDEA 自动生成 onClick 方法,将会得到下面的方法签名,onClick 方法参数默认为可空类型。


class KtListener: OnClickListener {
    override fun onClick(button: Button?): Unit {
        val name = button?.name ?: "Unknown button"
        println("Clicked $name")
    }
}


由于 Java 平台没有可空类型,而 Kotlin 中有,在这个例子中 Button 是否为空由我们来决定。默认情况下,对所有参数使用可空类型更安全,编译器会强制我们处理这些参数。


对于已知的永远不会空的参数,可以使用非空类型,空和非空都可以正常编译,但是如果将方法参数声明为非空,那么 Kotlin 编译器会自动注入一个空的检查,可能会抛出  IllegalArgumentException 异常,潜在的风险很大。当然使用非空参数,代码将会更加简洁。


class KtListener: OnClickListener {
    override fun onClick(button: Button): Unit {
        val name = button.name
        println("Clicked $name")
    }
}

如何在一个文件中使用多个具有相同名称的扩展函数和类


假设在不同的包中对 String 类实现了两个相同名字的扩展函数,如果是一个普通函数,你可以使用完全限定包名来调用它,但是扩展函数不行。所以我们可以在 import 语句中使用 as 关键字对其重命名,代码如下所示。


import com.example.code.indent as indent4
import com.example.square.indent as indent2
"hello world".indent4()


另外一个案例,想在同一个文件中使用来自不同包中两个具有相同名称的类(例如 java.util.Datejava.sql.Date ),并且您不希望通过完全限定包名来调用它们。我们也可以在 import 语句中使用 as 关键字对其重命名。


import java.util.Date as UtilDate
import java.sql.Date as SqlDate


现在我们就可以在这个类中,使用通过  as 关键字声明的别名来引用这些类。


译者



全文到这里就结束了,这篇文章每个问题,都是一个知识点,后面我会针对每个问题,单独写一篇文章,进行更加深入的分析。


如果有帮助点个赞就是对我最大的鼓励


欢迎关注公众号:ByteCode,持续分享最新的技术


最后推荐长期更新和维护的项目:


  • 个人博客,将所有文章进行分类,欢迎前去查看 hi-dhl.com
  • KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit
  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目以及相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看 AndroidX-Jetpack-Practice
  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析


image.png



近期必读热门文章




目录
相关文章
|
自然语言处理 前端开发 JavaScript
【第52期】一文读懂React国际化 (i18n)
【第52期】一文读懂React国际化 (i18n)
1171 1
|
11月前
|
SQL 存储 关系型数据库
如何巧用索引优化SQL语句性能?
本文从索引角度探讨了如何优化MySQL中的SQL语句性能。首先介绍了如何通过查看执行时间和执行计划定位慢SQL,并详细解析了EXPLAIN命令的各个字段含义。接着讲解了索引优化的关键点,包括聚簇索引、索引覆盖、联合索引及最左前缀原则等。最后,通过具体示例展示了索引如何提升查询速度,并提供了三层B+树的存储容量计算方法。通过这些技巧,可以帮助开发者有效提升数据库查询效率。
1007 2
|
10月前
|
机器学习/深度学习 自然语言处理 并行计算
DeepSpeed分布式训练框架深度学习指南
【11月更文挑战第6天】随着深度学习模型规模的日益增大,训练这些模型所需的计算资源和时间成本也随之增加。传统的单机训练方式已难以应对大规模模型的训练需求。
1278 3
|
监控 Serverless Shell
函数计算操作报错合集之 显示"Function timed out after 30 seconds (maxMemoryUsage: 73.38MB)" ,该如何解决
在使用函数计算服务(如阿里云函数计算)时,用户可能会遇到多种错误场景。以下是一些常见的操作报错及其可能的原因和解决方法,包括但不限于:1. 函数部署失败、2. 函数执行超时、3. 资源不足错误、4. 权限与访问错误、5. 依赖问题、6. 网络配置错误、7. 触发器配置错误、8. 日志与监控问题。
295 2
|
自然语言处理 数据可视化 算法
NLP中的嵌入和距离度量
本文将深入研究嵌入、矢量数据库和各种距离度量的概念,并提供示例和演示代码。
351 3
|
Java 测试技术 C#
几个好用的自动化测试工具总结
【6月更文挑战第4天】几个好用的自动化测试工具总结
889 0
|
存储 缓存 iOS开发
基于iOS的高效图片缓存策略实现
【4月更文挑战第9天】在移动应用开发中,图片资源的加载与缓存是影响用户体验的重要因素之一。特别是对于iOS平台,合理设计图片缓存策略不仅能够提升用户浏览图片时的流畅度,还能有效降低应用程序的内存压力。本文将介绍一种针对iOS环境优化的图片缓存技术,该技术通过多级缓存机制和内存管理策略,实现了图片快速加载与低内存消耗的目标。我们将从系统架构、关键技术细节以及性能评估等方面展开讨论,为开发者提供一套实用的图片缓存解决方案。
207 0
|
存储 数据安全/隐私保护 云计算
带你了解文件系统架构的演变:从传统到分布式
带你了解文件系统架构的演变:从传统到分布式
720 0
|
Java Spring
Gateway工作原理
Gateway工作原理
|
消息中间件 负载均衡 Kafka
Kafka - 3.x 分区分配策略及再平衡不完全指北
Kafka - 3.x 分区分配策略及再平衡不完全指北
569 0