简述: 上接上篇文章,咱们深刻分析了Kotlin1.3版本中的Contract契约的内容,那么这篇文章将会继续把Kotlin1.3新特性研究完毕。这篇文章还有个很是重要的点就是inline class 内联类。关于内联类的知识除了这篇文章会有介绍,后面立刻会翻译几篇有关Kotlin中的内联类相关内容。只有一个目的完全搞定Kotlin中的内联类。那咱们一块儿来看下本次提纲:java
关于inline内联相信你们都不陌生吧,在实际开发中咱们常常会使用Kotlin中的inline内联函数。那你们还记得inline内联做用吗? 这里再次复习下inline内联的做用:python
inline内联函数主要有两大做用:数组
用于lambda表达式调用,下降Function系列对象实例建立的内存开销,从而提升性能。声明成内联函数的话,而是在调用的时把调用的方法给替换掉,能够下降很大的性能开销。bash
另外一个内联函数做用就是它能是泛型函数类型实参进行实化,在运行时能拿到类型实参的信息。app
经过复习了inline函数的两大做用,实际上内联类存在乎义和inline函数第一个做用有点像。有时候业务场景须要针对某种类型建立包装器类。 可是,使用包装器类避免不了去实例化这个包装器类,可是这样会带来额外的建立对象的堆分配,它会引入运行时开销。 此外,若是包装类型是基础类型的,性能损失是很糟糕的,由于基础类型一般在运行时大大优化,而它们的包装器没有获得任何特殊处理。框架
那究竟是什么场景会须要内联类呢? 实际上,在开发中有时候就基本数据类型外加变量名是没法彻底表达某个字段的含义,甚至还有可能形成歧义,这种场景就特别适合内联类包装器。是否是很抽象,那么一块儿来看个例子。jvm
public fun <T> Iterable<T>.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}
复制代码
咱们仔细分析下joinToString函数,它有不少个函数参数,其中让人感到歧义就是前面三个: separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = ""
.有的人就会说不会有歧义啊,定义得很清楚很明白了,separator,prefix,postfix形参名明显都有本身的含义啊。仅仅从joinToString这个函数的角度来看确实比较清晰。但是你有没有想过外层调用者困惑呢。对于外部调用者前面三个参数都是CharSequence类型,对于他们而言除非去看函数声明而后才知道每一个参数表明什么意思,外面调用者很容易把三个参数调用顺序弄混了。就像下面这样。maven
fun main(args: Array<String>) {
val numberList = listOf(1, 3, 5, 7, 9, 11, 13)
println(numberList.joinToStr("<",",",">"))
//这段代码在调用者看来就仅仅是传入三个字符串,给人看起来很迷惑,根本就不知道每一个字符串实参到底表明是什么意思。
//这里代码是很脆弱的,在不看函数声明时,字符串要么很容易被打乱顺序,要么没人敢改这里的代码了。
}
fun <T> Iterable<T>.joinToStr(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = ""): String{
return this.joinToString(separator, prefix, postfix)
}
复制代码
上面那种问题,为何咱们平时感觉不到呢? 这是由于IDE帮你作了不少工做,可是试想下若是你的代码离开IDE就忽然感受很丑陋. 就看上面那段代码假如没有给你joinToStr函数声明定义,是否是对传入三个参数一脸懵逼啊。针对上述实际上有三种解决办法:ide
第一种: IDE高亮提示函数
第二种: Kotlin中命名参数
关于Kotlin中命名参数解决问题方式和IDE提示解决思路是同样的。关于Kotlin中命名参数不了解的能够参考我以前这篇文章浅谈Kotlin语法篇之如何让函数更好地调用(三)
fun main(args: Array<String>) {
val numberList = listOf(1, 3, 5, 7, 9, 11, 13)
println(numberList.joinToStr(prefix = "<", separator = ",", postfix = ">"))
}
复制代码
第三种: 使用包装器类解决方案
fun main(args: Array<String>) {
val numberList = listOf(1, 3, 5, 7, 9, 11, 13)
println(numberList.joinToStr(Speparator(","), Prefix("<"), Postfix(">")))
}
class Speparator(val separator: CharSequence)
class Prefix(val prefix: CharSequence)
class Postfix(val postfix: CharSequence)
fun <T> Iterable<T>.joinToStr( separator: Speparator, prefix: Prefix, postfix: Postfix ): String {
return this.joinToString(separator.separator, prefix.prefix, postfix.postfix)
}
复制代码
看到这里是否是不少人以为这样实现有问题,虽然它能很好解决咱们上述类型不明确的问题。可是却引入一个更大问题,须要额外建立Speparator、Prefix、Postfix实例对象,带来不少的内存开销。从投入产出比来看,估计没人这么玩吧。这是由于inline class没出来以前,可是若是inline class能把性能开销下降到和直接使用String同样的性能开销,你还认为它是不好的方案吗? 请接着往下看
第四种: Kotlin中inline class终极解决方案
针对上述问题,Kotlin提出inline class解决方案,就是从源头上解决问题。一块儿来看看:
fun main(args: Array<String>) {
val numberList = listOf(1, 3, 5, 7, 9, 11, 13)
println(numberList.joinToStr(Speparator(","), Prefix("<"), Postfix(">")))
}
//相比上一个方案,仅仅是多加了inline关键字
inline class Speparator(val separator: CharSequence)
inline class Prefix(val prefix: CharSequence)
inline class Postfix(val postfix: CharSequence)
fun <T> Iterable<T>.joinToStr( separator: Speparator, prefix: Prefix, postfix: Postfix ): String {
return this.joinToString(separator.separator, prefix.prefix, postfix.postfix)
}
复制代码
经过使用inline class来改造这个案例,会发现刚刚上述那个问题就被完全解决了,外部调用者不会再一脸懵逼了,一看就很明确,是该传入Speparator、Prefix、Postfix对象。性能开销问题的彻底不用担忧了,它和直接使用String的性能几乎是同样的,这样一来是否是以为这种解决方案还不错呢。至于它是如何作到的,请接着往下看。这就是为何须要inline class场景了。
inline class 为了解决包装器类带来额外性能开销问题的一种特殊类。
基本结构很简单就是在普通class前面加上inline关键字
由于kotlin中的inline class仍是处于Experimental中,因此你要使用它须要作一些额外的配置。首先你的Kotlin Plugin升级到1.3版以上,而后配置gradle,这里给出IntelliJ IDEA和AndroidStudio尝鲜gradle配置:
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions {
freeCompilerArgs = ["-XXLanguage:+InlineClasses"]
}
}
复制代码
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs = ['-XXLanguage:+InlineClasses']
}
}
复制代码
<configuration>
<args>
<arg>-XXLanguage:+InlineClasses</arg>
</args>
</configuration>
复制代码
估计不少人都没使用过typealias吧,若是还不了解typealias的话请参考我以前的这篇文章:[译]有关Kotlin类型别名(typealias)你须要知道的一切. 其实上述那个问题还能够用typealias来改写,可是你会发现是有点缺陷的,并无达到想要效果。能够给你们看下:
typealias Speparator = CharSequence
typealias Prefix = CharSequence
typealias Postfix = CharSequence
fun main(args: Array<String>) {
val numberList = listOf(1, 3, 5, 7, 9, 11, 13)
println(numberList.joinToStr(",", "<", ">"))
}
fun <T> Iterable<T>.joinToStr( separator: Speparator, prefix: Prefix, postfix: Postfix ): String {
return this.joinToString(separator, prefix, postfix)
}
复制代码
关于inline class和typealias有很大相同点不一样点,相同点在于: 他们二者看起来貌似都引入一种新类型,而且二者都将在运行时表现为基础类型。不一样点在于: typealias仅仅是给基础类型取了一个别名而已,而inline class是基础类型一个包装器类。换句话说inline class才是真正引入了一个新的类型,而typealias则没有。
是否是仍是有点抽象啊,来个例子你就明白了
typealias Token = String
inline class TokenWrapper(val value: String)
fun main(args: Array<String>) {
val token: Token = "r3huae03zdhreol38fdjhkdfd8df"//能够看出这里Token名称彻底是当作String类型来用了,至关于给String取了一个有意义的名字
val tokenWrapper = TokenWrapper("r3huae03zdhreol38fdjhkdfd8df")//而inline class则是把String类型的值包裹起来,至关于String的一个包装器类。
println("token is $token")
println("token value is ${tokenWrapper.value}")//这里tokenWrapper并不能像token同样当作String来使用,而是须要打开包装器取里面value值
}
复制代码
经过反编译分析,就能清楚明白为何inline class不会存在建立对象性能开销。实际上inline class在运行时表现和直接使用基础类型的效果是同样的。
就拿上述例子反编译分析一下:
TokenWrapper类
public final class TokenWrapper {
@NotNull
private final String value;
@NotNull
public final String getValue() {//这个方法就不用说了吧,val自动生成的get方法
return this.value;
}
// $FF: synthetic method
private TokenWrapper(@NotNull String value) {//构造器私有化
Intrinsics.checkParameterIsNotNull(value, "value");
super();
this.value = value;
}
@NotNull
public static String constructor_impl/* $FF was: constructor-impl*/(@NotNull String value) {
Intrinsics.checkParameterIsNotNull(value, "value");
return value;
}
// $FF: synthetic method
@NotNull
public static final TokenWrapper box_impl/* $FF was: box-impl*/(@NotNull String v) {//box-impl装箱操做
Intrinsics.checkParameterIsNotNull(v, "v");
return new TokenWrapper(v);
}
@NotNull
public static String toString_impl/* $FF was: toString-impl*/(String var0) {//toString方法实现
return "TokenWrapper(value=" + var0 + ")";
}
public static int hashCode_impl/* $FF was: hashCode-impl*/(String var0) {//hashCode方法实现
return var0 != null ? var0.hashCode() : 0;
}
public static boolean equals_impl/* $FF was: equals-impl*/(String var0, @Nullable Object var1) {//equals方法实现
if (var1 instanceof TokenWrapper) {
String var2 = ((TokenWrapper)var1).unbox-impl();
if (Intrinsics.areEqual(var0, var2)) {
return true;
}
}
return false;
}
public static final boolean equals_impl0/* $FF was: equals-impl0*/(@NotNull String p1, @NotNull String p2) {
Intrinsics.checkParameterIsNotNull(p1, "p1");
Intrinsics.checkParameterIsNotNull(p2, "p2");
throw null;
}
// $FF: synthetic method
@NotNull
public final String unbox_impl/* $FF was: unbox-impl*/() {//拆箱操做
return this.value;
}
public String toString() {
return toString-impl(this.value);//委托给对应静态方法实现
}
public int hashCode() {
return hashCode-impl(this.value);
}
public boolean equals(Object var1) {
return equals-impl(this.value, var1);
}
}
复制代码
能够看到TokenWrapper类中反编译后的源码重写了Any中toString、equal、hashCode三个方法。而后这三个方法又委托到给外部定义对应的静态方法来实现。unbox_impl和box_impl两个函数实际上就是拆箱和装箱的操做
main函数
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String token = "r3huae03zdhreol38fdjhkdfd8df";//能够看到typealias定义的Token名字已经消失的无影无踪,只剩下String基础类型。
String tokenWrapper = TokenWrapper.constructor-impl("r3huae03zdhreol38fdjhkdfd8df");//TokenWrapper类痕迹依然存在
String var3 = "token is " + token;
System.out.println(var3);
var3 = "token value is " + tokenWrapper;
System.out.println(var3);
}
复制代码
分析以下: 能够先从main函数入手,重点看这行:
String tokenWrapper = TokenWrapper.constructor-impl("r3huae03zdhreol38fdjhkdfd8df");
复制代码
而后再跳到TokenWrapper中constructor-impl方法
@NotNull
public static String constructor_impl/* $FF was: constructor-impl*/(@NotNull String value) {
Intrinsics.checkParameterIsNotNull(value, "value");
return value;//这里仅仅是接收一个value值,作了一个参数检查,最后就直接把这个value又返回出去了。
}
复制代码
因此main函数中的val tokenWrapper = TokenWrapper("r3huae03zdhreol38fdjhkdfd8df")
在运行时至关于val tokenWrapper: String = "r3huae03zdhreol38fdjhkdfd8df"
. 因此性能问题就不用担忧了。
Kotlin1.3新特性对when表达式作一个写法上的优化,为何这么说呢?仅仅就是写法上的优化,实际上什么都没作,一块儿来研究下。不知道你们在使用when表达式有没有这样感觉(反正我是有过这样的感觉): 在when表达式做用域内,老天啊请赐我一个像lambda表达式中的同样it实例对象指代吧。---来自众多Kotlin开发者心声。一块儿看下这个例子:
@JvmStatic
private fun fillIntentArguments(intent: Intent, params: Array<out Pair<String, Any?>>) {
params.forEach {
val value = it.second//因为没有像lamba那样的it指代只能在when表达式最外层定义一个局部变量value,以便于在when表达式体内使用value.
when (value) {
null -> intent.putExtra(it.first, null as Serializable?)
is Int -> intent.putExtra(it.first, value)//能够看到这里,若是value能像lambda表达式中it指代该多好,能够没有
is Long -> intent.putExtra(it.first, value)
is CharSequence -> intent.putExtra(it.first, value)
is String -> intent.putExtra(it.first, value)
is Float -> intent.putExtra(it.first, value)
is Double -> intent.putExtra(it.first, value)
...
}
return@forEach
}
}
复制代码
能够看到上面的1.3版本以前源码案例实现,本就一个when表达式的实现因为在表达式内部须要使用传入值,可是呢表达式做用域内又不能像lambda表达式内部那样快乐使用it,因此被活生生拆成两行代码实现,是否是很郁闷。关于这个问题,官方已经注意到了,能够看到Kotlin团队的大佬们对开发者的问题处理仍是蛮积极的,立刻就优化这个问题。
官方究竟是怎么优化的呢? 那么有的人就说了是否是像lambda表达式同样赐予咱们一个it指代呢。官方的回答是: NO. 一块儿再来看1.3版本的实现:
private fun fillIntentArguments(intent: Intent, params: Array<out Pair<String, Any?>>) {
params.forEach {
when (val value = it.second) {//看到没有,官方说你不是想要一个when表达式实现吗,那行把value缩进来了. 这样在when表达式内部快乐使用value了
null -> intent.putExtra(it.first, null as Serializable?)
is Int -> intent.putExtra(it.first, value)
is Long -> intent.putExtra(it.first, value)
is CharSequence -> intent.putExtra(it.first, value)
is String -> intent.putExtra(it.first, value)
is Float -> intent.putExtra(it.first, value)
is Double -> intent.putExtra(it.first, value)
...
}
return@forEach
}
}
复制代码
fun main(args: Array<String>) {
val value = getValue()
when (value) {
is Int -> "This is Int Type, value is $value".apply(::println)
is String -> "This is String Type, value is $value".apply(::println)
is Double -> "This is Double Type, value is $value".apply(::println)
is Float -> "This is Float Type, value is $value".apply(::println)
else -> "unknown type".apply(::println)
}
}
fun getValue(): Any {
return 100F
}
复制代码
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Object value = getValue();
String var3;
if (value instanceof Integer) {
var3 = "This is Int Type, value is " + value;
System.out.println(var3);
} else if (value instanceof String) {
var3 = "This is String Type, value is " + value;
System.out.println(var3);
} else if (value instanceof Double) {
var3 = "This is Double Type, value is " + value;
System.out.println(var3);
} else if (value instanceof Float) {
var3 = "This is Float Type, value is " + value;
System.out.println(var3);
} else {
var3 = "unknown type";
System.out.println(var3);
}
}
@NotNull
public static final Object getValue() {
return 100.0F;
}
复制代码
fun main(args: Array<String>) {
when (val value = getValue()) {//when表达式条件直接是一个表达式,并用value保存了返回值
is Int -> "This is Int Type, value is $value".apply(::println)
is String -> "This is String Type, value is $value".apply(::println)
is Double -> "This is Double Type, value is $value".apply(::println)
is Float -> "This is Float Type, value is $value".apply(::println)
else -> "unknown type".apply(::println)
}
}
fun getValue(): Any {
return 100F
}
复制代码
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Object value = getValue();
String var2;
if (value instanceof Integer) {
var2 = "This is Int Type, value is " + value;
System.out.println(var2);
} else if (value instanceof String) {
var2 = "This is String Type, value is " + value;
System.out.println(var2);
} else if (value instanceof Double) {
var2 = "This is Double Type, value is " + value;
System.out.println(var2);
} else if (value instanceof Float) {
var2 = "This is Float Type, value is " + value;
System.out.println(var2);
} else {
var2 = "unknown type";
System.out.println(var2);
}
}
@NotNull
public static final Object getValue() {
return 100.0F;
}
复制代码
经过对比二者实现方式反编译的代码你会发现没有任何变化,因此这就是我说为何实际上没作什么操做。
还记得开发者日大会上官方布道师Hali在讲Kotlin 1.3新特性的时候,第一个例子就是讲无参数main函数,在他认为这是一件很兴奋的事。下面给出官方一张动图一块儿兴奋一下:
不知道你们在开发中有没有被其余动态语言开发的人吐槽过。好比最简单的在程序中打印一行内容的时候,静态语言就比较繁琐用Java举例先得定义一个类,而后再定义main函数,函数中还得传入数组参数。人家python一行print代码就解决了。其实Kotlin以前版本相对Java仍是比较简单至少不须要定义类了,可是Kotlin 1.3就直接把main函数中的参数干掉了(注意: 这里指的是带参数和不带参数共存,并非彻底把带参main函数给替换掉了)。
能够你们有没有思考过无参main函数是怎么实现的呢? 不妨咱们一块儿来探索一波,来了你就懂了,很简单。 来个Hello Kotlin的例子哈。
fun main(){
println("Hello Kotlin")
}
复制代码
将上述代码反编译成Java代码以下
public final class NewMainKt {
public static final void main() {//外部定义无参的main函数
String var0 = "Hello Kotlin";
System.out.println(var0);
}
// $FF: synthetic method
public static void main(String[] var0) {//自动生成一个带参数的main函数
main();//而后再去调用一个无参的main函数
}
}
复制代码
看完反编译后的Java代码是否是一眼就清楚,所谓的无参main函数,实际上就是个障眼法。默认生成一个带参数的main函数继续做为执行的入口,只不过在这带参数的main函数中再去调用外部无参main函数。
注意: 使用无参main函数有好处也有不妥的地方,好处显而易见的是使用很是简洁。可是也就间接丧失了main函数执行入口配置参数功能。因此官方并无把带参数main函数去掉,而是共存。两种main函数都是有各自使用场景的。
咱们天然而然知道在类的伴生对象是彻底支持@JvmStatic,@JvmField注解。首先呢,关于@JvmStatic,@JvmField注解我想有必要说明下它们的做用。
他们做用主要是为了在Kotlin伴生对象中定义的一个函数或属性,可以在Java中像调用静态函数和静态属性那样类名.函数名/属性名方式调用,让Java开发者彻底没法感知这是一个来自Kotlin伴生对象中的函数或属性。若是不加注解那么在Java中调用方式就是类名.Companion.函数名/属性名。你让一个Java开发者知道Companion存在,只会让他一脸懵逼。
这就意味着1.3接口中伴生对象中函数和属性能够向类中同样快乐地使用@JvmStatic,@JvmField注解了。 一块儿来看个使用例子:
//在Kotlin接口中定义
interface Foo {
companion object {
@JvmField
val answer: Int = 42
@JvmStatic
fun sayHello() {
println("Hello, world!")
}
}
}
//在Java代码中调用
class TestFoo {
public static void main(String[] args){
System.out.println("Foo test: " + Foo.answer + " say: " + Foo.sayHello());
}
}
复制代码
不知道你们是否还记得我以前几篇文章深刻研究过Lambda表达式整个运行原理,其中就详细讲了关于Function系列的接口。由于咱们知道Lambda表达式最后会编译成一个class类,这个类会去继承Kotlin中Lambda的抽象类(在kotlin.jvm.internal包中)而且实现一个Function0...FunctionN(在kotlin.jvm.functions包中)的接口(这个N是根据lambda表达式传入参数的个数决定的,目前接口N的取值为 0 <= N <= 22,也就是lambda表达式中函数传入的参数最多也只能是22个),这个Lambda抽象类是实现了FunctionBase接口,该接口中有两个方法一个是getArity()获取lambda参数的元数,toString()实际上就是打印出Lambda表达式类型字符串,获取Lambda表达式类型字符串是经过Java中Reflection类反射来实现的。FunctionBase接口继承了Function,Serializable接口。(具体详细内容请参考个人这篇文章: 浅谈Kotlin语法篇之lambda编译成字节码过程彻底解析(七))
由上面分析获得N取值范围是0 <= N <= 22,要是此时有个lambda表达式函数参数个数是23个也就是大于22个时候该怎么办?不就玩不转了吗? 虽然大于22个参数场景不多不多,可是这始终算是一个缺陷。因此此次Kotlin 1.3直接就完全抹平这个缺陷,增长了FunctionN接口支持传入的是可变长参数列表,也就是支持任意个数参数,这样扩展性就更强了。
//官方源码定义
interface FunctionN<out R> : Function<R>, FunctionBase<R> {
/** * Invokes the function with the specified arguments. * * Must **throw exception** if the length of passed [args] is not equal to the parameter count returned by [arity]. * * @param args arguments to the function */
operator fun invoke(vararg args: Any?): R//能够看到这里接收是一个vararg 可变长参数,支持任意个数的lambda表达式参数,不再用担忧超过22个参数该怎么办了。
/** * Returns the number of arguments that must be passed to this function. */
override val arity: Int
}
//使用例子伪代码
fun trueEnterpriseComesToKotlin(block: (Any, Any, ... /* 42 more */, Any) -> Any) {
block(Any(), Any(), ..., Any())
}
复制代码
在Kotlin 1.3中,注解类能够嵌套注解类、接口以及伴生对象.关于Kotlin中的注解和反射尚未详细深刻研究过,这个暂且放一放,等到研究注解时候,会再次探讨有关注解类嵌套的问题。
annotation class Foo {
enum class Direction { UP, DOWN, LEFT, RIGHT }
annotation class Bar
companion object {
fun foo(): Int = 42
val bar: Int = 42
}
}
复制代码
到这里Kotlin1.3新特性相关的内容就结束。下面将会继续深刻研究下Kotlin 1.3中的inline class(主要是以翻译国外优秀文章为主)。而后就是去深刻研究你们一直期待的协程和ktor框架,并把最终研究成果以文章的形式共享给你们。欢迎关注,会一直持续更新下去~~~
原创系列:
翻译系列:
实战系列:
欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~