Kotlin 面试题

你的支持对我意义重大!

Hi,我是旭锐。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长路线笔记 & 博客,有志同道合的朋友,欢迎跟着我一起成长。(联系方式 & 入群方式在 GitHub)


前言

  • 在软件开发领域,经常会涌现出新的编程语言,但很少有新语言能够像 Kotlin 一样在如此短的时间内成为最受欢迎的语言之一;
  • 在 Android 生态中主要有 C++、Java、Kotlin 三种语言 ,它们的关系不是替换而是互补。其中,C++ 的语境是算法和高性能,Java 的语境是平台无关和内存管理,而 Kotlin 则融合了多种语言中的优秀特性,带来了一种更现代化的编程方式。 例如简化异步编程的协程(coroutines),提高代码质量的可空性(nullability),lambda 表达式等;
  • 在这篇文章里,我将讨论 Kotlin 相较于 Java 的优势。另外,作为客户端同学,我还会讨论 Kotlin 在 Android 开发中的应用实践。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

目录


1. Kotlin 基础

  • == 和 equal() 相同,=== 比较内存地址
  • 顶级成员(函数 & 属性)的原理: Kotlin 顶级成员的本质是 Java 静态成员,编译后会自动生成文件名Kt的类,可以使用@Jvm:fileName注解修改自动生成的类名
  • 默认参数的原理: Kotlin 默认参数的本质是将默认值 固化 到调用位置,所以在 Java 中无法直接调用带默认参数的函数,需要在 Kotlin 函数上增加@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
    }
}
  • 扩展函数的原理: 扩展函数的语义是在不修改类 / 不继承类的情况下,向一个类添加新函数或者新属性。本质是静态函数,静态函数的第一个参数是接收者类型,调用扩展时不会创建适配对象或者任何运行时的额外消耗。在 Java 中,我们只需要像调用普通静态方法那样调用扩展即可。相关深入文章:《Kotlin | 扩展函数(终于知道为什么 with 用 this,let 用 it)》
  • 委托机制的原理: Kotlin 委托的语法关键字是 by,其本质上是面向编译器的语法糖,三种委托(类委托、对象委托和局部变量委托)在编译时都会转化为 “无糖语法”。例如类委托:编译器会实现基础接口的所有方法,并直接委托给基础对象来处理。例如对象委托和局部变量委托:在编译时会生成辅助属性(prop$degelate),而属性 / 变量的 getter() 和 setter() 方法只是简单地委托给辅助属性的 getValue() 和 setValue() 处理。相关深入文章:《Kotlin | 委托机制 & 原理 & 应用》
  • let、with、apply 的区别和应用场景:
  • 中缀函数: 声明 infix 关键字的函数是中缀函数,可以使用中缀表示法调用(忽略点和括号)
中缀函数的要求:
- 1、成员函数或扩展函数
- 2、函数只有一个参数
- 3、不能使用可变参数或默认参数

举例:
infix fun String.吃(fruit: String): String {
    return "${this}吃${fruit}"
}
调用: "小明" 吃 "苹果"

类型系统

  • 数值类型: Kotlin 将基本数据类型和引用型统一为:Byte、Short、Int、Long、Float、Double、Char 和 Boolean。需要注意的是,类型的统一并不意味着 Kotlin 所有的数值类型都是引用类型,大多数情况下,它们在编译后会变成基本数据类型,类型参数会被编译为引用类型。
  • 类型转换: 较小类型并不是较大类型的子类型,较小的类型不能隐式转换为较大的类型。
val b: Byte = 1 // OK, 字面值是静态检测的
val i: Int = b // 错误
val i: Int = b.toInt() // OK
  • 只读集合和可变集合: 只读集合只可读,而可变集合可以增删该差(例如 List 只读,MutableList 可变)。需要注意,只读集合引用指向的集合不一定是不可变的,因为你使用的变量可能是众多指向同一个集合的其中一个。
  • Array 和 IntArray 的区别: Array 相当于引用类型数组 Integer[],IntArray 相当于数值类型数组 int[]。

面向对象

  • 类修饰符
    Kotlin 类 / 方法默认是 final 的,如果想让继承类 / 重写方法,需要在基类 / 基方法添加 open 修饰符。
final:不允许继承或重写
open:允许继承或重写
abstract:抽象类 / 抽象方法
  • 访问修饰符
    Java 默认的访问修饰符是 protected,Kotlin 默认的访问修饰符是 public。
public:所有地方可见
internal:模块中可见,一个模块就是一组编译的 Kotlin 文件
protected:子类中可见(与 Java 不同,相同包不可见)
private:类中可见
  • 内部类
    Kotlin:默认为静态内部类,如果想访问类中的成员方法和属性,需要添加 inner 关键字称为非静态内部类;
    Java:默认为非静态内部类。

  • object 与 companion object 的区别
    object 有两层语义:静态匿名内部类 + 单例对象
    companion object 是伴生对象,一个类只能有一个,代表了类的静态成员(函数 / 属性)

  • object 单例的原理

lambda 表达式

  • lambda 表达式本质上是 「可以作为值传递的代码块」。在老版本 Java 中,传递代码块需要使用匿名内部类实现,而使用 lambda 表达式甚至连函数声明都不需要,可以直接传递代码块作为函数值。
  • 当 lambda 表达式只有一个参数,可以用 it 关键字来引用唯一的实参。
  • lambda 表达式的种类
    1、普通 Lambda 表达式:例如 ()->R
    2、带接收者对象的 Lambda 表达式:例如 T.()->R
  • lambda 表达式访问局部变量的原理: 在 Java 中,匿名内部类访问的局部变量必须是 final 修饰的,否则需要使用数组或对象做一层包装。在 Kotlin 中,lambda 表达式可以直接访问非 final 的局部变量,其原理是提供了一层包装类,修改局部变量本质上是修改包装类中的值。
class Ref(var value:T)
  • lambda 编译优化: 在循环中使用 Java 8 与 Kotlin 中的 lambda 表达式时,会存在编译时优化,编译器会将 lambda 优化为一个 static 变量,除非 lambda 表达式中访问了外部的变量或函数。
  • 内联函数的原理: lambda 表达式编译后会变成匿名内部类,至少会生成一个中间对象,当 lambda 表达式被经常调用时,会增大运行开销。使用内联函数可以减少中间对象的开销,因为调用内联函数不会真正调用函数,而是把函数实现固化到函数调用的位置。需要注意:如果函数体太大就不适合使用内联函数了,因为会大幅度增加字节码大小。
  • 实化类型参数 reified: 因为泛型擦除的影响,运行期间不清楚类型实参的时机类型,Kotlin 中使用 带实化类型参数的内联函数 可以突破这种限制,实化类型参数在插入到调用位置时会使用类型实参的确切类型代替,因此可以确定实参类型。
在这个函数里,我们传入一个List,企图从中过滤出 T 类型的元素:

Java:
 List filter(List list) {
    List result = new ArrayList<>();
    for (Object e : list) {
        if (e instanceof T) { // compiler error
            result.add(e);
        }
    }
    return result;
}
---------------------------------------------------
Kotlin:
fun  filter(list: List<*>): List {
    val result = ArrayList()
    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(list)
---------------------------------------------------
内联后:
val result = ArrayList()
for (e in list) {
    if (e is String) {
        result.add(e)
    }
}

3. 协程

协程

  • 协程的原理: 使用同步代码编写异步程序

4. 框架

Kotlin-Flow


5. 总结

最后,回顾下我们的 Kotlin 路线系列文章:

  • Java | 关于泛型能问的都在这里了(含Kotlin)
  • Kotlin | 扩展函数(终于知道为什么 with 用 this,let 用 it)
  • Kotlin | 委托机制 & 原理 & 应用
  • Android | ViewBinding 与 Kotlin 委托双剑合璧

参考资料

  • Kotlin 官方文档
  • Android Kotlin 文档
  • 谷歌:选用 Kotlin 的五大理由 —— 刘志勇 译
  • Medium · 关于 Kotlin 的技术文章 —— Android 团队
  • Kotlin 实战 —— [俄] DmitryJeme 著

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

你可能感兴趣的:(Kotlin 面试题)