大家好,之前有聊过kotlin1.5、1.6版本提供的一些新特性,下面是总结的相关系列文章:
kotlin密封sealed class/interface的迭代之旅
优化@BuilderInference注解,Kotlin高版本下了这些“毒手”!
@JvmDefaultWithCompatibility优化小技巧,了解一下~
接下来就带大家了解kotlin1.7版本提供了哪些特性!
一. 内联类支持借助内联属性实现类委托
之前有写过一篇文章带大家了解内联类是啥:value class 用的爽不爽,进来瞧一瞧吧。这里再简单对内联类做个介绍:
- 像内联函数都带有一个内联定义,内联类主要是用来包装属性的,实际上对属性读写访问时,并不会真正创建内联类,这样就帮助我们减少了类创建带来的开销;
- 其次,内联类想要真的不被创建,前提是不能发生装箱(简单理解为:被当作其他类进行使用),比如参与泛型类型传递等,一旦发生装箱内联类就会被真的创建,这样就违背了内联类本身被官方设计的初衷;
我们看一个内联类实现类委托代理的例子:
interface Bar {
fun foo() = "foo"
}
@JvmInline
value class BarWrapper(val bar: Bar): Bar by object: Bar {}
其中内联类BarWrapper的Bar接口实现代理给了object: Bar {},这样写是没啥问题的。但是如果你将内联类BarWrapper的Bar接口实现代理给了构造属性bar:
@JvmInline
value class BarWrapper(val bar: Bar): Bar by bar
上面的写法就会报错了,内联类不支持这种特性。
由于类接口委托给构造属性实现,是类委托常用的一种开发技巧,内联类不支持这种特性肯定是非常不方便了,于是官方在kotlin1.7.0支持了这个特性,上面的代码使用起来就不会报错:
二. 类型参数支持下划线运算符_
简而言之,这个下划线运算符_就是为了帮助我们简化泛型类型推断,先写一段测试代码:
interface Action<T> {
fun execute(): T
}
class SimpleAction : Action<String> {
override fun execute(): String = "SimpleAction"
}
class ComplexAction : Action<Int> {
override fun execute(): Int = "ComplexAction".length
}
fun <T, S: Action<T>> update() {
}
定义了Action接口以及两个接口实现类SimpleAction、ComplexAction,以及测试方法update(),可以看到,这个测试方法定义了两个泛型T和S,其中S的泛型类型还得通过T决定。其实只要我们声明了S的泛型类型,T的泛型类型自然也就被决定了。但是在代码中,我们还是得分别声明S和T的泛型类型:
fun main() {
update<String, SimpleAction>()
}
其实当我们指定了S泛型的具体类型为SimpleAction,T的泛型类型肯定是String了,但是我们还是得显示声明。
等到了kotlin1.7.0,官方就为类型参数引入了下划线操作符_,这样编译器会根据上下文自动帮助我们推断泛型类型:
fun main() {
update<_, SimpleAction>()
}
其实,我们经常使用的函数类型参数也引入了下划线操作符,当我们不使用函数参数时,可以使用_替代:
fun main() {
update { _, age ->
"$age"
}
}
fun update(bar: (content: String, age: Int) -> String) {
}
三. 绝不可为null的泛型类型支持
这个特性其实在kotlin1.6就支持了,只不过属于测试特性,到了1.7就稳定了下来,可用于生产环境。
相信大家肯定知道,一般我们声明的泛型类型参数,是可以传入null的:
fun test200() {
val res = elvisLike(null, 855)
}
fun <T> elvisLike(x: T, y: T): T = x ?: y
如果我们想要支持传入的泛型类型禁止为null,可以这样写:
这样当我们传入null就会报错。
但如果我想要实现elvisLike方法x参数是可以传null的,y禁止传null,该怎么实现呢,一般可以通过下面这样实现:
但是上面这样写起来显得稍微复杂,又是强制指定T的上界为Any代表不为null,这样会影响所有使用T声明的参数类型不可传null,所以还得单独指定x参数的类型为T?表示可null,外部可以传入null值。
kotlin1.7.0提供了 & Any语法可以强制指定泛型类型参数不可传为null:
fun test200() {
val res = elvisLike(null, 855)
}
fun <T> elvisLike(x: T, y: T & Any): T & Any = x ?: y
当我们尝试给y参数传null,就会报错:
相当的方便好用,我们可以反编译成java代码看下实现原理:
就是给参数y增加了参数是否为null校验。
四. 稳定的函数式接口构造引用
这个特性其实也是1.6提供的,只不过1.7才变成稳定版。
这个特性很简单,我们直接上代码:
fun interface Printer {
fun print(): String
}
fun main() {
val c = ::Printer
val printer: Printer = c.invoke {
"aaa"
}
}
通过::Printer我们就拿到了Printer接口的构造函数实例,调用其invoke方法就能创建一个实现Printer接口的对象。
如果要使用这个特性,需要增加一个compiler option: -XXLanguage:+KotlinFunInterfaceConstructorReference 。
五. 移除JVM的目标版本1.6
也就是说当kotlin插件升级到1.7.0及以上,jvm target需要指定为1.8,而不是1.6.
六. 深递归DeepRecursiveFunction支持
大家可能都碰到过,写一个递归函数处理一些逻辑计算的问题,但如果递归的层数过深,就会存在StackOverflowError的风险,但递归的层数多深才能造成这种风险,很难给一个确切的答案,对于常见的可预见的有限的递归深度我们可能可以进行一个测试衡量效果,但总归太麻烦,所以kotlin提供了DeepRecursiveFunction递归类支持。
接下来我们写一个遍历二叉树深度的例子:
class Tree(val left: Tree?, val right: Tree?)
fun listTree(tree: Tree?): Int {
if (tree == null) return 0
val left = listTree(tree.left)
val right = listTree(tree.right)
return max(left, right) + 1
}
但二叉树可能很深,递归遍历是存在一定风险的,比如二叉树深度上千次,比较极端的情况可能就会触发StackOverflowError。
所以kotlin1.4就提供了DeepRecursiveFunction递归类尽可能避免上面的风险,到了kotlin1.7这个API才变得稳定下来。
接下来我们使用DeepRecursiveFunction改造下上面代码:
fun testDeepRecursiveFunction(tree: Tree?) {
val drf = DeepRecursiveFunction<Tree?, Int> { t ->
if (t == null) 0 else maxOf(
callRecursive(t.left),
callRecursive(t.right)
) + 1
}
val depth = drf.invoke(tree)
}
其中,callRecursive()也是搭配DeepRecursiveFunction使用的官方方法。
根据官方文档的说法,使用了DeepRecursiveFunction,即使上面的二叉树发生了100000递归遍历,也不会发生StackOverflowError,可以说是相当的牛逼了 。
同时官方推荐:
如果你的递归次数超过额1000次,就应该考虑使用DeepRecursiveFunction了。
七. 总结
其实我上面介绍的特性大部分属于kotlin语言方面提供的通用新特性,包括针对特定的kotlin/jvm新增的特性。但是对于官方标准库的特性并没有做深入的了解,所以文章中就只提到了一个DeepRecursiveFunction,其他的标准库特性感兴趣的可以看下下面的参考链接。
八. 参考链接
本文正在参加「金石计划」