金三银四必备,全面总结 Kotlin 面试知识点

简介: 金三银四必备,全面总结 Kotlin 面试知识点

前言


  • 在 Android 面试中很重视基础知识的考察,其中语言基础主要包括 Java、Kotlin、C/C++ 三种编程语言。在小彭面试的经验中,发现很多同学的 Kotlin 语言能力只是停留在一些非常入门的语法使用上;
  • 在这篇文章里,我将为你浓缩总结 Kotlin 中最常用的知识点和原理。希望通过这篇文章能够帮助你扫除支持盲区,对于一些语法背后的原理也有所涉猎。


1. 为什么要使用 Kotlin?


面试官问这个问题一方面可能是先想引入 Kotlin 这个话题,另一方面是想考察你的认知能力,是不是真的有思考过 Kotlin 的优势 / 价值,还是随波逐流别人用我也跟着用。你可以这么回答:


在 Android 生态中主要有 C++、Java、Kotlin 三种语言 ,它们的关系不是替换而是互补。其中,C++ 的语境是算法和高性能,Java 的语境是平台无关和内存管理,而 Kotlin 则融合了多种语言中的优秀特性,带来了一种更现代化的编程方式。 例如简化异步编程的协程(coroutines),提高代码质量的可空性(nullability),lambda 表达式等。


2. 语法糖的味道


  • == 和 equal() 相同,=== 比较内存地址
  • 顶级成员(函数 & 属性)的原理: Kotlin 顶级成员的本质是 Java 静态成员,编译后会自动生成文件名Kt的类,可以使用@Jvm:fileName注解修改自动生成的类名。
  • 默认参数的原理: Kotlin 默认参数的本质是将默认值 固化 到调用位置,所以在 Java 中无法直接调用带默认参数的函数,需要在 Kotlin 函数上增加@JvmOverloads注解,指示编译器生成重载方法(@JvmOverloads会为默认参数提供重载方法)。
  • 解构声明的原理: Kotlin 解构声明可以把一个对象的属性分解为一组变量,所以解构声明的本质是局部变量。


举例:
val (name, price) = Book("Kotlin入门", 66.6f)
println(name)
println(price)
-------------------------------------------
Kotlin 类需要声明`operator fun componentN()`方法来实现解构功能,否则是不具备解构声明的功能的,例如:
class Book(var name: String, var price: Float) {
    operator fun component1(): String { // 解构的第一个变量
        return name
    }
    operator fun component2(): Float { // 解构的第二个变量
        return price
    }
}
复制代码


  • Sequences 序列的原理: Sequences 提升性能的关键在于多个操作共享同一个 Iterator 迭代器,只需要一次循环就可以完成数据操作。Sequences 又是懒惰的,需要遇到终端操作才会开始工作。
  • 扩展函数的原理: 扩展函数的语义是在不修改类 / 不继承类的情况下,向一个类添加新函数或者新属性。本质是静态函数,静态函数的第一个参数是接收者类型,调用扩展时不会创建适配对象或者任何运行时的额外消耗。在 Java 中,我们只需要像调用普通静态方法那样调用扩展即可。相关资料:Kotlin | 扩展函数(终于知道为什么 with 用 this,let 用 it)
  • let、apply、with 的区别和应用场景: let、with、apply 都是标准库函数,它们的主要区别在 lambda 参数类型定义不同。apply、with 的 lambda 参数是 T 的扩展函数,因此在 lambda 内使用 this 引用接收者对象,而 let 的 lambda 参数是参数为 T 的高阶函数,因此 lambda 内使用 it 引用唯一参数。
  • 委托机制的原理: Kotlin 委托的语法关键字是 by,其本质上是面向编译器的语法糖,三种委托(类委托、对象委托和局部变量委托)在编译时都会转化为 “无糖语法”。例如类委托:编译器会实现基础接口的所有方法,并直接委托给基础对象来处理。例如对象委托和局部变量委托:在编译时会生成辅助属性(prop$degelate),而属性 / 变量的 getter() 和 setter() 方法只是简单地委托给辅助属性的 getValue() 和 setValue() 处理。相关资料:Kotlin | 委托机制 & 原理 & 应用
  • 中缀函数: 声明 infix 关键字的函数是中缀函数,调用中缀函数时可以省略圆点以及圆括号等程序符号,让语句更自然。


中缀函数的要求:
- 1、成员函数或扩展函数
- 2、函数只有一个参数
- 3、不能使用可变参数或默认参数
举例:
infix fun String.吃(fruit: String): String {
    return "${this}吃${fruit}"
}
调用: "小明" 吃 "苹果"
复制代码



3. 类型系统


  • 数值类型: Kotlin 将基本数据类型和引用型统一为:Byte、Short、Int、Long、Float、Double、Char 和 Boolean。需要注意的是,类型的统一并不意味着 Kotlin 所有的数值类型都是引用类型,大多数情况下,它们在编译后会变成基本数据类型,类型参数会被编译为引用类型。
  • 隐式转换: Kotlin 不存在隐式类型转换,即时是低级类型也需要显式转换为高级类型:


//隐式转换,编译器会报错
val anInt: Int = 5
val ccLong: Long = anInt 
//需要去显式的转换,下面这个才是正确的 
val ddLong: Long = anInt.toLong()
复制代码
  • 平台类型: 当可空性注解不存在时,Java 类型会被转换为 Kotlin 的平台类型。平台类型本质上是 Kotlin 编译器无法确定其可空信息,既可以把它当作可空类型,也可以把它当作非空类型。


如果所有来自 Java 的值都被看成非空是不合理的,反之把 Java 值都当作可空的,由会引出大量 Null 检查。综合考量,平台类型是 Kotlin 为开发者选择的折中的设计方案。


  • 类型转换: 较小类型并不是较大类型的子类型,较小的类型不能隐式转换为较大的类型。


val b: Byte = 1 // OK
val i: Int = b // 编译错误
val i: Int = b.toInt() // OK
复制代码
  • 只读集合和可变集合: 只读集合只可读,而可变集合可以增删该差(例如 List 只读,MutableList 可变)。需要注意,只读集合引用指向的集合不一定是不可变的,因为你使用的变量可能是众多指向同一个集合的其中一个。
  • Array 和 IntArray 的区别: Array 相当于引用类型数组 Integer[],IntArray 相当于数值类型数组 int[]。
  • Unit: Any 的子类,作为函数返回值时表示没有返回值,可以省略,与 Java void 类似。
  • Nothing: 表示表达式或者函数永远不会返回,Nothing? 唯一允许的值是 null。
  • Java Void: void 的包装类,与 void 类似表示一个函数没有有效的返回值,返回值只能是 null。


4. 面向对象


  • 类修饰符: Kotlin 类 / 方法默认是 final 的,如果想让继承类 / 重写方法,需要在基类 / 基方法添加 open 修饰符。


final:不允许继承或重写
open:允许继承或重写
abstract:抽象类 / 抽象方法
复制代码
  • 访问修饰符: Java 默认的访问修饰符是 protected,Kotlin 默认的访问修饰符是 public。


public:所有地方可见
internal:模块中可见,一个模块就是一组编译的 Kotlin 文件
protected:子类中可见(与 Java 不同,相同包不可见,Kotlin 没有 default 包可见)
private:类中可见
复制代码
  • 构造函数:
  • 默认构造函数: class 默认有一个无参主构造函数,如果显式声明了构造函数,则默认的无参主构造函数失效;
  • 主构造函数: 声明在 class 关键字后,其中 constructor 关键词可以省略;
  • 次级构造函数: 如果声明了次级构造函数,则默认的无参主构造函数会失效。如果存在主构造函数,次级构造函数需要直接或间接委托给主构造函数。
  • init 函数执行顺序: 主构造函数 > init > 次级构造函数
  • 内部类: Kotlin 默认为静态内部类,如果想访问类中的成员方法和属性,需要添加 inner 关键字称为非静态内部类;Java 默认为非静态内部类。
  • data 关键字原理: data 关键字用于定义数据类型,编译器会自动从主构造函数中提取属性并生成一系列函数:equals()/hashCode()、toString()、componentN()、copy()。
  • sealed 关键字原理: 密封类用来表示受限的类继承结构,密封类可以有子类,但是所有子类都必须内嵌在该密封类中。
  • object 与 companion object 的区别 object 有两层语义:静态匿名内部类 + 单例对象 companion object 是伴生对象,一个类只能有一个,代表了类的静态成员(函数 / 属性)
  • 单例: Kotlin 可以使用 Java 相似的方法实现单例,也可以采用 Kotlin 特有的语法。相关资料:Kotlin下的5种单例模式
  • object


// Kotlin实现
object SingletonDemo
复制代码
  • by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED)
class SingletonDemo private constructor() {
    companion object {
        val instance: SingletonDemo by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            SingletonDemo() 
        }
    }
}
复制代码


5. lambda 表达式


  • lambda 表达式本质上是「可以作为值传递的代码块」。在老版本 Java 中,传递代码块需要使用匿名内部类实现,而使用 lambda 表达式甚至连函数声明都不需要,可以直接传递代码块作为函数值。
  • it: 当 lambda 表达式只有一个参数,可以用 it 关键字来引用唯一的实参。
  • lambda 表达式的种类
  • 1、普通 Lambda 表达式:例如 ()->R
  • 2、带接收者对象的 Lambda 表达式:例如 T.()->R
  • lambda 表达式访问局部变量的原理: 在 Java 中,匿名内部类访问的局部变量必须是 final 修饰的,否则需要使用数组或对象做一层包装。在 Kotlin 中,lambda 表达式可以直接访问非 final 的局部变量,其原理是提供了一层包装类,修改局部变量本质上是修改包装类中的属性。


class Ref<T>(var value:T)
复制代码
  • lambda 表达式编译优化: 在循环中使用 Java 8 与 Kotlin 中的 lambda 表达式时,会存在编译时优化,编译器会将 lambda 优化为一个 static 变量,除非 lambda 表达式中访问了外部的变量或函数。
  • inline 内联函数的原理:
  • 内联 lambda 表达式参数(主要优点): 内联函数的参数如果是 lambda 表达式,则该参数默认也是 inline 的。lambda 表达式也会被固化的函数调用位置,从而减少了为 lambda 表达式创建匿名内部类对象的开销。当 lambda 表达式被经常调用时,可以减少内存开销。
  • 减少入栈出栈过程(次要优点): 内联函数的函数体被固化到函数调用位置,执行过程中减少了栈帧创建、入栈和出栈过程。需要注意:如果函数体太大就不适合使用内联函数了,因为会大幅度增加字节码大小。
  • @PublishApi 注解: 编译器要求内联函数必须是 public 类型,使用 @PublishApi 注解可以实现 internal 等访问修饰的同时又实现内联
  • noinline 非内联: 如果在内联函数内部,lambda 表达式参数被其它非内联函数调用,会报编译时错误。这是因为 lambda 表达式已经被拉平而无法传递给其他非内联函数。可以给参数加上 noinline 关键字表示禁止内联。


inline fun test(noinline inlined: () -> Unit) {
    otherNoinlineMethod(inlined)
}
复制代码
  • 非局部返回(Non-local returns): 一个不带标签的 return 语句只能用在 fun 声明的函数中使用,因此在 lambda 表达式中的 return 必须带标签,指明需要 return 的是哪一级的函数:


fun song(f: (String) -> Unit) {
    // do something
}
fun behavior() {
    song {
        println("song $it")
        return //报错: 'return' is not allowed here
        return@song // 局部返回
        return@behavior // 非局部返回
    }
}
复制代码
  • 唯一的例外是在内联函数中的 lambda 表达式参数,可以直接使用不带标签的 return,返回的是调用内联函数的外部函数,而不是内联函数本身,默认就是非局部返回。


inline fun song(f: (String) -> Unit) {
    // do something
}
fun behavior() {
    song {
        println("song $it")
        return // 非局部返回
        return@song // 局部返回
        return@behavior // 非局部返回
    }
}
复制代码
  • crossinline 非局部返回: 禁止内联函数的 lambda 表达式参数使用非局部返回
  • 实化类型参数 reified: 因为泛型擦除的影响,运行期间不清楚类型实参的类型,Kotlin 中使用 带实化类型参数的内联函数 可以突破这种限制。实化类型参数在插入到调用位置时会使用类型实参的确切类型代替,因此可以确定实参类型。


在这个函数里,我们传入一个List,企图从中过滤出 T 类型的元素:
Java:
<T> List<T> filter(List list) {
    List<T> result = new ArrayList<>();
    for (Object e : list) {
        if (e instanceof T) { // compiler error
            result.add(e);
        }
    }
    return result;
}
---------------------------------------------------
Kotlin:
fun <T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
        if (e is T) { // cannot check for instance of erased type: T
            result.add(e)
        }
    }
    return result
}
调用:
val list = listOf("", 1, false)
val strList = filter<String>(list)
---------------------------------------------------
内联后:
val result = ArrayList<String>()
for (e in list) {
    if (e is String) {
        result.add(e)
    }
}
复制代码



5. DSL 领域特定语言


DSL 是专门用于解决某个问题的语言,虽然没有通用语言那么全面,但在解决特定问题时更加高效。案例:Compose 的 UI 代码也是采用了 DSL,使得 Compose 拥有了不输于 XML 的编码效率。实现 DSL 需要可以利用的 Kotlin 语法特性,相关资料:Kotlin DSL 实战:像 Compose 一样写代码

  • 高阶函数: 使得 lambda 参数脱离圆括号,减少一个参数;
  • 扩展函数: 传递 Receiver,减少一个参数;
  • Context Receivers: 传递多个 Receiver,在扩展函数的基础上减少多个参数;
  • 中缀函数: 让语法更简洁自然;
  • @DSLMarker: 用于限制 lambda 中不带标签的 this 只能访问到最近的 Receiver 类型,当调用更外层的 Receiver 时必须显式指定 this@XXX。


context(View)
val Float.dp 
    get() = this * this@View.resources.displayMetrics.density
class SomeView : View {
  val someDimension = 4f.dp
}
复制代码



6. 总结


少部分比较聪明的小伙伴就会问了,你这怎么没有涉及协程、Flow 这些知识点?那是因为这些知识点比较多,小彭决定单独放在一篇文章里。一篇文章拆成两篇用,它不香吗

目录
相关文章
|
20天前
|
Android开发
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制
|
20天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
|
20天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
|
22天前
|
消息中间件 Android开发 索引
Android面试高频知识点(4) 详解Activity的启动流程
Android面试高频知识点(4) 详解Activity的启动流程
24 3
|
23天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
21 2
|
23天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
43 1
|
23天前
|
Android开发
Android面试高频知识点(1) 图解 Android 事件分发机制
Android面试高频知识点(1) 图解 Android 事件分发机制
35 1
|
4月前
|
存储 算法 安全
Java面试题:Java内存模型及相关知识点深度解析,Java虚拟机的内存结构及各部分作用,详解Java的垃圾回收机制,谈谈你对Java内存溢出(OutOfMemoryError)的理解?
Java面试题:Java内存模型及相关知识点深度解析,Java虚拟机的内存结构及各部分作用,详解Java的垃圾回收机制,谈谈你对Java内存溢出(OutOfMemoryError)的理解?
69 0
|
2月前
|
Web App开发 前端开发 Linux
「offer来了」浅谈前端面试中开发环境常考知识点
该文章归纳了前端开发环境中常见的面试知识点,特别是围绕Git的使用进行了详细介绍,包括Git的基本概念、常用命令以及在团队协作中的最佳实践,同时还涉及了Chrome调试工具和Linux命令行的基础操作。
「offer来了」浅谈前端面试中开发环境常考知识点
|
29天前
|
XML 前端开发 Android开发
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制