《Android最常讨论的关于Kotlin的面试问题》我到底可以拆出多少知识点

话说不要过度解读,但是学习嘛,很多东西是需要解读的,如果说单纯的快速的学习使用黑箱模式其实是最好的,我不需要知道他是什么意思,只需要在需要的时候正确的把他整处理就行。但是卷到如今的今天,黑箱模式已经不行了,你得知道为什么,那就卷吧。

作为一个使用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啊

相关推荐
阿巴斯甜11 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker12 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952713 小时前
Andorid Google 登录接入文档
android
黄林晴14 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android