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)
1601 1
|
运维 监控 Linux
Centos开启snmp服务
Centos开启snmp服务
314 1
|
人工智能 监控 开发者
详解大模型应用可观测全链路
阿里云可观测解决方案从几个方面来尝试帮助使用 QwQ、Deepseek 的 LLM 应用开发者来满足领域化的可观测述求。
2600 157
详解大模型应用可观测全链路
YOLOv11改进策略【损失函数篇】| 通过辅助边界框计算IoU提升检测效果(Inner_GIoU、Inner_DIoU、Inner_CIoU、Inner_EIoU、Inner_SIoU)
YOLOv11改进策略【损失函数篇】| 通过辅助边界框计算IoU提升检测效果(Inner_GIoU、Inner_DIoU、Inner_CIoU、Inner_EIoU、Inner_SIoU)
1434 4
YOLOv11改进策略【损失函数篇】| 通过辅助边界框计算IoU提升检测效果(Inner_GIoU、Inner_DIoU、Inner_CIoU、Inner_EIoU、Inner_SIoU)
|
机器学习/深度学习 数据采集 TensorFlow
使用Python实现智能食品消费者行为分析的深度学习模型
使用Python实现智能食品消费者行为分析的深度学习模型
363 4
|
机器学习/深度学习 自然语言处理 并行计算
DeepSpeed分布式训练框架深度学习指南
【11月更文挑战第6天】随着深度学习模型规模的日益增大,训练这些模型所需的计算资源和时间成本也随之增加。传统的单机训练方式已难以应对大规模模型的训练需求。
2028 3
|
存储 Prometheus 监控
AutoMQ 开源可观测性方案:夜莺 Flashcat
在现代企业中,随着数据处理需求的不断增长,AutoMQ [1] 作为一种高效、低成本的流处理系统,逐渐成为企业实时数据处理的关键组件。然而,随着集群规模的扩大和业务复杂性的增加,确保 AutoMQ 集群的稳定性、高可用性和性能优化变得尤为重要。因此,集成一个强大而全面的监控系统对于维护 AutoMQ 集群的健康运行至关重要。夜莺监控系统以其高效的数据采集、灵活的告警管理和丰富的可视化能力,成为企业监控AutoMQ 集群的理想选择。通过使用夜莺监控系统,企业可以实时掌握 AutoMQ 集群的运行状态,及时发现和解决潜在问题,优化系统性能,确保业务的连续性和稳定性。
477 2
AutoMQ 开源可观测性方案:夜莺 Flashcat
|
机器学习/深度学习 人工智能 自然语言处理
“魔搭”来了!一文深度解读达摩院推出的AI模型社区
一文详解ModelScope魔搭社区,模型即服务开启AI开发使用新范式
10666 12
“魔搭”来了!一文深度解读达摩院推出的AI模型社区
|
jenkins 持续交付 开发工具
|
数据采集 人工智能 文字识别
高能力全透明双语大语言模型MAP-Neo完全开源,开放所有细节!
近年来,大型语言模型 (LLMs) 在各种任务中取得了前所未有的性能提升。然而,由于商业利益,最强大的模型(如 GPT、Gemini 和Claude)只能通过API访问,并未公开训练细节。