《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啊

相关推荐
CYRUS STUDIO34 分钟前
ARM64汇编寻址、汇编指令、指令编码方式
android·汇编·arm开发·arm·arm64
weixin_449310841 小时前
高效集成:聚水潭采购数据同步到MySQL
android·数据库·mysql
Zender Han2 小时前
Flutter自定义矩形进度条实现详解
android·flutter·ios
白乐天_n3 小时前
adb:Android调试桥
android·adb
姑苏风7 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
数据猎手小k11 小时前
AndroidLab:一个系统化的Android代理框架,包含操作环境和可复现的基准测试,支持大型语言模型和多模态模型。
android·人工智能·机器学习·语言模型
你的小1011 小时前
JavaWeb项目-----博客系统
android
风和先行12 小时前
adb 命令查看设备存储占用情况
android·adb
AaVictory.13 小时前
Android 开发 Java中 list实现 按照时间格式 yyyy-MM-dd HH:mm 顺序
android·java·list
似霰14 小时前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder