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



近期必读热门文章




目录
相关文章
|
6月前
|
Kotlin
Kotlin之Hello,World
Kotlin之Hello,World
189 1
|
XML JavaScript 前端开发
如何优雅地使用 Stack Overflow?
如何优雅地使用 Stack Overflow?
512 0
|
7月前
|
存储 安全 Java
《Kotlin核心编程》笔记:val 和 var & 字符串
《Kotlin核心编程》笔记:val 和 var & 字符串
112 0
|
C语言 C++ 容器
C++中stack的用法(超详细,入门必看)
⭐一、stack的简介 stack的中文译为堆栈,堆栈一种数据结构。C语言中堆栈的定义及初始化以及一些相关操作实现起来较为繁琐,而C++的stack让这些都变得简便易实现。因为C++中有许多关于stack的方法函数。 堆栈(stack)最大的特点就是先进后出(后进先出)。就是说先放入stack容器的元素一定要先等比它后进入的元素出去后它才能出去。呃这样说可能有点绕哈哈,举个生活中的例子吧。 某一天,天气炎热,你买了一个冰淇淋甜筒,而这个冰淇淋甜筒在制作过程时冰淇淋是不是先进入到甜筒的底部然后到最上面呢,但是你在吃的过程中要从最上面吃起到最后才能吃到甜筒底部的冰淇淋,但底部的冰淇淋是先进入甜筒
895 0
|
Java Kotlin
Kotlin内联函数inline、noinline、crossinline
如果一个函数接收另一个函数作为参数,或返回类型是一个函数类型,那么该函数被称为是高阶函数
158 0
|
缓存 数据处理 Kotlin
kotlin flow操作符详解
kotlin flow操作符详解
1092 0
kotlin flow操作符详解
|
IDE 安全 Java
Kotlin Inline classes,你了解吗?
kotlin 1.5 中的 Inline classes 终于进入稳定版。在提高代码的可读性、易用性的同时,不会造成性能的损失,值得大家学习和使用
124 0
Kotlin Inline classes,你了解吗?
|
JSON Java Android开发
用kotlin打印出漂亮的android日志
用kotlin打印出漂亮的android日志
255 0
用kotlin打印出漂亮的android日志
|
JSON Java Maven
用kotlin打印出漂亮的android日志(二)
用kotlin打印出漂亮的android日志(二)
456 0
用kotlin打印出漂亮的android日志(二)
|
Java API Apache
Stack Overflow 最火的一段代码竟然有 Bug...
于是,我看到了下面这个问题:怎样将字节数输出成人类可读的格式?也就是说,怎样将123,456,789字节输出成123.5MB?
Stack Overflow 最火的一段代码竟然有 Bug...