话说不要过度解读,但是学习嘛,很多东西是需要解读的,如果说单纯的快速的学习使用黑箱模式其实是最好的,我不需要知道他是什么意思,只需要在需要的时候正确的把他整处理就行。但是卷到如今的今天,黑箱模式已经不行了,你得知道为什么,那就卷吧。
作为一个使用Kotlin断断续续的搬砖的Android,今年上半年我还蛮喜欢JAVA的,主要是感觉用Kotlin写JAVA好像很生硬,但是这个砖全是Kotlin写到,搬砖2周左右,仿佛打通了任督二脉,基于方法的编程真的香,限制的只有想象力而不是API,于是也关注了一些Kotlin的公众号,同时也翻翻书啥的,但是更多的时候还是在搬砖。
最近公众号推了一篇《Android 最常讨论的关于 Kotlin 的面试问题》 洋洋洒洒的应该有几千字吧,讲的都是Kotlin很基础的东西,当我快速的浏览完关闭的时候,我脑子里面对于哪些细碎的知识点还没有回顾完,以至于我搬砖的时候,那些知识点还在干扰着我搬砖,这是一个很有意思的本能现象,因为我对于这些知识点并不是熟悉到可以瞬间回顾的程度,大脑潜意识里认为这很重要,于是就帮忙回顾一遍,这蛮费心力的,同时也导致搬砖的注意力不集中容易写出bug。这和考试的时候大脑老是唱歌一个道理,当你把考试大脑唱的歌能够非常熟悉的唱出来的之后,考试就不至于唱那首歌了,只有熟悉的身心环境又才会触发那首歌了,至于会不会唱其他的,拖欠的重要事情太多了,估计下次会是其他事情。嗯,这也是这个笔记系列的重要原因吧,尝试把知识点细化到某个层面的最小吧,那么就开整。
正文
我还是按照自己的思路把文章的知识点换一下顺序吧,感觉从最简单的开始是一个好方向。
kotlin init块的目的是什么?他和构造函数有什么不同
原文结论:
kotlin中的init块用于初始化属性,并执行在创建内的实例时所需要运行的代码,他是主构造函数的一部分,在对象初始化阶段调用,与构造函数的区别在于,如果有多个构造函数,无论调用那个构造函数,init 块总会被执行,它整合了公共的初始化代码。
这就是kotlin编译器的牛逼的地方了,我们写的所有Kotlin Android代码,他都会编译成 class的字节码,虽然不会生成.java文件,但是可以理解成生成了.class 文件。但是JAVA 里面是没有init 这个概念的,而且init在kotlin 的概念里面并不是JAVA的static{},那么答案就是kotlin 把init 里面的代码编译到构造函数里面去了,毕竟JAVA class一定是有构造函数了的,哪怕你没有写。我们先来看代码及其现象:
javascript
class Man : Person {
constructor(name: String, age: Int) : super(name = name) {
println("Man 2 个参数 $age")
}
constructor(name: String, code: String,age: Int) : super(name = name,code) {
println("Man 3 个参数 $age")
}
init {
println("Man init ")
}
}
open class Person constructor( name: String, code:String){
var msg:String="msg 的默认值"
constructor(name: String):this(name,"code") {
println("一个参数的构造函数")
}
init {
println("Person $name")
println("Person $msg")
msg="init"
println("Person $msg")
}
}
上面代码很简单。定义了一个Person 类,然后又一个两个参数的构造函数,一个一个一个参数的次构造函数。Man继承了Person类,然后定义了两个构造函数。
我们先来看第一种最基础的情况。直接创建主构造函数的对象 Person("man","new code") 打印如下:
csharp
Person man
Person msg 的默认值
Person init
这个可以看出几个东西:
- init 被执行了。
- init 里面构造函数的的变量赋值是成功的。
- 类的成员变量在init里面是默认值
再来看第二种情况:创建次构造函数对象Person("man") 打印如下:
csharp
Person man
Person msg 的默认值
Person init
一个参数的构造函数
可以看出,次构造函数的打印在init 执行之后。 那么最后一种情况,子类创建对象 Man("man",5) 和 Man("man","code",5) 打印如下:
csharp
Person man
Person msg 的默认值
Person init
一个参数的构造函数
Man init
Man 2 个参数 5
------
Person man
Person msg 的默认值
Person init
Man init
Man 3 个参数 5
可以看到,他其实先执行的父类的init 函数。然后再执行Man对象的构造函数,那么就开始知识点汇总。
知识点汇总
结合上的结论我们提取出以下几个知识点:
- 主构造函数对象赋值最快
- 然后是对象的变量的默认值
- 然后是init
- 然后是次构造函数
- 子类的init
- 子类的构造函数
当然了这个总结并不完善,并没涉及到所有情况,有片面之意。那么我们来看一下两个Kotlin class 生成的字节码转化为JAVA class的样子:
less
public class Person {
@NotNull
private String msg;
@NotNull
public final String getMsg() {
return this.msg;
}
public final void setMsg(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.msg = var1;
}
public Person(@NotNull String name, @NotNull String code) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(code, "code");
super();
this.msg = "msg 的默认值";
String var3 = "Person " + name;
System.out.println(var3);
var3 = "Person " + this.msg;
System.out.println(var3);
this.msg = "init";
var3 = "Person " + this.msg;
System.out.println(var3);
}
public Person(@NotNull String name) {
Intrinsics.checkNotNullParameter(name, "name");
this(name, "code");
String var2 = "一个参数的构造函数";
System.out.println(var2);
}
}
public final class Man extends Person {
public Man(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super(name);
String var3 = "Man init ";
System.out.println(var3);
var3 = "Man 2 个参数 " + age;
System.out.println(var3);
}
public Man(@NotNull String name, @NotNull String code, int age) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(code, "code");
super(name, code);
String var4 = "Man init ";
System.out.println(var4);
var4 = "Man 3 个参数 " + age;
System.out.println(var4);
}
}
可以看到,这个生成的class。我们并没有对其的完全总结,比如说 空校验、final class 、注解等等。这已经很细了,这些细枝末节的多看几次就记住了。那么就来看原文的下一条。
伴生对象的概念,以及他们如何在Kotlin中使用
原文结论:
在Kotlin中伴生对象是一个特殊的对象,他与声明他的类相关联,他类似于其他语言中的静态成员,但是具有更多的功能,伴生对象在类的所有实例中共享,其成员可以直接使用类名访问。
可以看到伴生对象是一个Kotlin 语言的一个概念,在JAVA中并没有,还是那个思路,我看你字节码转成JAVA是什么样的就行,我们先来模拟一下一个网络图片加载,通过简单的配置了一个默认图片。
kotlin
class ImageLoader{
companion object Config{
fun getDefaultImage():String ="这是一个默认图片"
}
fun load(path:String){
getDefaultImage()
}
}
JAVA 对象调用 配置 ImageLoader.Config.getDefaultImage() ,kotlin 调用 ImageLoader.getDefaultImage()。至于为啥要先说明JAVA 调用方式,因为我们可以通过实现功能去猜测到他实现方式,很可能是ImageLoader 持有了一个进行的Config 对象。OK,那我们直接看反编译过来的class:
java
public final class ImageLoader {
@NotNull
public static final Config Config = new Config((DefaultConstructorMarker)null);
public final void load(@NotNull String path) {
Intrinsics.checkNotNullParameter(path, "path");
Config.getDefaultImage();
}
public static final class Config {
@NotNull
public final String getDefaultImage() {
return "这是一个默认图片";
}
private Config() {
}
// $FF: synthetic method
public Config(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
为了节约blog字数空间,我把@Metadata 相关代码删除了,可以通过上面的内容看到,Kotlin把 config 编译成了一个JAVA静态的内部类,同时在ImageLoader 里面通过 static final 定义了一个config 对象。而结论上说的更多功能可能是代码上没有使用,所以反编译出来的class没有体现,而也验证了类似于静态对象这种逻辑。而我们也只是写了一个简单class的伴生对象,比如说 data class ,object class 具体的相关内容还需要自己去尝试一下。这里就不贴和详细描述了。object 不支持设置伴生对象,data class 生成的和正常class 类似,只是多了一些data class 的特性。
那么 伴生对象在什么情况下可以被使用呢,感觉在Android上使用的特别少,他的特性是伴生对象在所有类的实例中共享,这个玩意是静态对象,干上去了就不会回收了,需要使用的情景本身就不多,可以实现类似效果的有单例、继承关系持有、数据共享可以用viewModel等等。最近经常看到有用伴生对象提供一个对象获取或或者做fragment的不同入参的配置的,没有体会到这种写法的优势,明明可以只有一个对象,为啥要需要搞一个静态对象,而且在组件化中,fragment的创建根本就碰不到类直接创建对象的机会,有点懵逼。
解释Kotlin中的扩展函数的概念,inline,解释Kotlin 中let、apply、run、also和with 作用域函数的之间的区别,智能转换的概念
原文结论:
kotlin 中的扩展函数允许我们再不修改其源码的情况下,为类添加新功能,他们是向外部库或标准类添加实用函数的强大工具。
inline 是一个高阶函数的修饰符,他建议编译器在调用站点执行函数的代码的内联,当您将函数标记为 inline 时,该函数的字节码将直接复杂到调用的代码中,某些时候可以提高性能。
- let 用于对非空对象执行一段代码,并返回该代码块中最后一个表达式的结果,他通常用于空安全检查和链接操作,当然结合when 进行逻辑分发也是可以的。
- apply 用于通过调用多个方法来配置对象,并返回对象本身,通常是用于对象配置属性
- run 用于在对象上执行一个段代码,功能类似与了let,但如果没有指定返回值,则返回块内最后一个表达式的结果或整个对象。
- alse 用于对对象执行附加操作,并返回原始对象,它通常用于记录日志或者不允许改变原有对象的情况。
- with 用于调用一个对象的多个函数,没有返回值。感觉和apply 类似,但是apply有返回值,with的写法和apply 这种写到对象后面可以链式调度的还是在功能上有区别的。
Kotlin 中的智能转换是编译器根据特定条件执行的自动类型转换。当编译器可以保证某个变量属于某个代码块中的特定类型时,它会自动将该变量强制转换为该类型,从而允许您使用其属性和函数,而无需显式类型检查或强制转换。
听起来很黑科技的感觉。那么还是来试试他是如何在JAVA中实现的。
kotlin
fun main() {
"扩展函数".println()
}
fun String.println(){
println(this)
}
这代码很简单,就是对于String 定义了一个打印log的函数。同时使用了inline 定义的 内联函数。
arduino
public final class MainKt {
public static final void main() {
println("扩展函数");
}
public static void main(String[] var0) {
main();
}
public static final void println(@NotNull String $this$println) {
Intrinsics.checkNotNullParameter($this$println, "$this$println");
System.out.println($this$println);
}
}
虽然特别复杂的看不出来,但是可以大致的看出来,他其实是把我们定义的扩展函数定义了成了一个静态的函数,然后把扩展函数的实现整合到静态函数里面,这里就同时验证了两个理论的实现。
- 扩展函数是定义静态函数,然后把扩展的函数的实现在静态函数中实现。
- 内联函数是把内联函数的代码复制到调用的代码中。
那么我们对扩展函数添加 inline 关键字呢?
kotlin
fun main() {
val isPalindrome= "abc".isPalindrome()
println(isPalindrome)
}
inline fun String.isPalindrome(): Boolean {
val cleanString = this.toLowerCase().replace(Regex("[^a-zA-Z0-9]"), "")
return cleanString == cleanString.reversed()
}
反编译:
ini
public final class MainKt {
public static final void main() {
String $this$isPalindrome$iv = "abc";
int $i$f$isPalindrome = false;
String var10000 = $this$isPalindrome$iv.toLowerCase();
Intrinsics.checkNotNullExpressionValue(var10000, "this as java.lang.String).toLowerCase()");
CharSequence var3 = (CharSequence)var10000;
Regex var4 = new Regex("[^a-zA-Z0-9]");
String var5 = "";
String cleanString$iv = var4.replace(var3, var5);
boolean isPalindrome = Intrinsics.areEqual(cleanString$iv, StringsKt.reversed((CharSequence)cleanString$iv).toString());
System.out.println(isPalindrome);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final boolean isPalindrome(@NotNull String $this$isPalindrome) {
int $i$f$isPalindrome = 0;
Intrinsics.checkNotNullParameter($this$isPalindrome, "$this$isPalindrome");
String var10000 = $this$isPalindrome.toLowerCase();
Intrinsics.checkNotNullExpressionValue(var10000, "this as java.lang.String).toLowerCase()");
CharSequence var3 = (CharSequence)var10000;
Regex var4 = new Regex("[^a-zA-Z0-9]");
String var5 = "";
String cleanString = var4.replace(var3, var5);
return Intrinsics.areEqual(cleanString, StringsKt.reversed((CharSequence)cleanString).toString());
}
}
可以看到,会生成两个函数,先生成扩展函数的静态函数,然后把静态函数拷贝生成一个inline 拷贝的函数。嗯,结果扩展函数生成的静态函数就没有地方调用了,所以不建议对于扩展函数标记inline。
我们可以看到,let apply run also 和with 其实都是 inline 定义的高阶函数。那么也就没有多少角度可以细化知识点了。
在编码阶段智能转换的概念感觉和高阶函数中的let、run的使用相契合,同时也契合when语句,我们并不需要写返回类型,编译器可以自己基于代码逻辑进行判断。在编译阶段也是有很多的好处的,因为kotlin是直接生成字节码指令,Java编译器的很多优化比如说包装类型与基础类型的的调优等,kotlin 需要自己实现一份,同时新增了很多自己的优化思路。
今天先告一段落
其实这篇还有一些知识点还没有拆出来的,剩下的都是和协程相关的,因为有些组件代码需要在Android 中调用,同时我对协程相关有些知识点掌握的不是太牢固,需要先写demo 才可以拆分成细碎的知识点。sorry啊