不断学习,做更好的自己!💪

【Android -- 面试】复习指南之 Kotlin_面试

1. 基础

==、===和equal的区别?
==和​​​equal​​的作用相同,===比较内存地址

var和val的区别?

  • ​var​​:可变引用,具有可读和可写权限,值可变,类型不可变
  • ​val​​:不可变引用,具有可读权限,值不可变,但是对象的属性可变

2. 函数

Kotlin 中默认参数的作用以及原理?
作用:配合 ​​​@JavaOverloads​​​ 可以解决​​Java​​​调用​​Kotlin​​​函数重载的问题。
原理:​​​Kotlin​​ 编译的默认参数是被编译到调用的函数中的,所以默认参数改变的时候,是需要重新编译这个函数的。

Kotlin 中顶层函数的原理
顶层函数实质就是 ​​​Java​​​ 中的静态函数,可以通过 ​​Kotlin​​​ 中的 ​​@Jvm:fileName​​​ 自动生成对应的 ​​Java​​ 调用类名。

中缀函数是什么?注意点?
中缀函数需要是用infix关键字修饰,如​​​downTo​​:

public infix fun Int.downTo(to: Int): IntProgression {
return IntProgression.fromClosedRange(this, to, -1)
}

注意点是函数的参数只能有一个,函数的参与者只能有两个。

解构函数的本质?
解构声明将对象中的所有属性,解构成一组属性变量,而且这些变量可以单独使用,可以单数使用的原因是通过获取对应的​​​component()​​方法对应着类中每个属性的值,这些属性的值被存储在局部变量中,所以解构声明的实质是局部变量。

扩展函数的本质?
扩展函数的本质就是对应​​​Java​​中的静态函数,这个静态函数参数为接受者类型的对象,然后利用这个对象去访问对象中的属性和成员方法,最后返回这个对象的本身。

扩展函数和成员函数的区别?

  • 实质不同:扩展函数实质是静态函数,是外部函数,成员函数是内部函数。
  • 权限不同:扩展函数访问不了私有的属性和成员方法,成员函数可以。
  • 继承:扩展函数不可复写,成员函数可以复写。

3. 类、对象和接口

Kotlin 中常用的类的修饰符有哪些?

  • ​open​​:运行创建子类或者复写子类的方法。
  • ​final​​:不允许创建子类和复写子类的方法。
  • ​abstract​​:抽象类,必须复写子类的方法。

在​​Kotlin​​​中,默认的类和方法的修饰符都是​​final​​​的,如果想让类和方法能够被继承或者复写,需要显示的添加​​open​​修饰符。

Kotlin中可见性修饰符有哪些?

  • ​public​​:所有地方可见
  • ​protected​​:子类中可见
  • ​private​​:类中可见
  • ​internal​​:模块中可见,一个模块就是一组一起编译的Kotlin文件

​Java​​​默认的访问权限是包访问权限,​​Kotlin​​​ 中默认的访问权限是 ​​public​​。

Kotlin中的内部类和Java中的内部类有什么不同?

  • ​Kotlin​​:默认相当于Java中的静态内部类,如果想访问类中的成员方法和属性,需要添加inner关键字修饰。
  • ​Java​​:默认持有外部类引用,可以访问成员方法和属性,如果想声明为静态内部类,需要添加static关键字修饰。

Kotlin属性代理背后原理?
可以简单理解为属性的settter、getter访问器内部实现交给了代理对象来实现,相当于使用一个代理对象代替了原来简单属性的读写过程,而暴露外部属性操作还是不变 的,照样是属性赋值和读取,只是​​​setter​​​、​​getter​​内部具体实现变了。

object 和 companion object 的一些特点?
共同点:
定义单例的一种方式,提供静态成员和方法。

不同点:

  • object:用来生成匿名内部类。
  • companion object:提供工厂方法,访问私有的构造方法。

4. lambda

lambda 表达式有几种?

  • 普通表达式:()->R。
  • 带接收者对象的表达式:T.()->R,可以访问接收者对象的属性和成员方法。如apply。

kotlin 和 Java 内部类或者 lambda 表达式访问局部变量有什么不同?

  • ​Java​​​中的内部类:局部变量必须是​​final​​声明的,无法去修改局部变量的值。
  • ​Kotlin​​​中​​lambda​​​表达式:不要求​​final​​​声明,对于非​​final​​​修饰的​​lambda​​表达式,可以修改局部变量的值。

如果想在Java中的内部类修改外层局部变量的值,有两种方法:用数组包装或者提供包装类,Kotlin中lambda能够访问并修改局部变量的本质就是提供了一层包装类:

class Ref<T>(var value:T)

修改局部变量的值就是修改 ​​value​​ 中的值。

使用 lambda 表达式访问的局部变量有什么不同?
默认情况下,局部变量的生命周期会被限制在声明这个变量的函数中,但是如果它被lambda捕捉了,使用这个变量的代码可以被存储并稍后执行。

class Apple {
lateinit var num:(() -> Int)

fun initCount(){
val count = 2
num = fun():Int{
return count * count
}
}

fun res():Int{
return num()
}
}

fun main(args: Array<String>) {
val a = Apple()
a.initCount()
val res = a.res()
println(res)
}

如上面代码所示,局部变量​​count​​​就被存储在​​lambda​​​表达式中,最后通过​​Apple#res​​方法引用表达式。

原理:当你捕捉​​final​​​变量的时候,它的值会和​​lambda​​​代码一起存储。对于非​​final​​​变量,它的值会被封装在一层包装器中,包装器的引用会和​​lambda​​代码一起被存储。

带来的问题:默认情况下,lambda表达式会生成匿名内部类,在非显示声明对象的情况下可以多次重用,但是如果捕获了局部变量,每次调用的时候都需要生成新的实例。

序列是什么?集合类和序列的操作符比较?
​​​Sequence​​(序列)是一种惰性集合,可以更高效地对元素进行链式操作,不需要创建额外的集合保存过程中产生的中间结果,简单来讲,就是序列中所有的操作都是按顺序应用在每一个元素中。比如:

fun main(args: Array<String>) {
val list = mutableListOf<String>("1","2","3","4","5","6","7","8","9")
val l = list.asSequence()
.filter { it.toCharArray()[0] < '4' }
.map { it.toInt() * it.toInt() }
.toList()
}

对于上述序列中的"1",它会先执行​​filter​​​,再执行​​map​​,之后再对"2"重复操作。除此以外,序列中所有的中间操作都是惰性的。

集合和序列操作符的比较:

  • 集合类:​​map​​​和​​filter​​​方法是内联,不会生成匿名类的实例,但每次进行​​map​​​和​​filter​​都会生成新的集合,当数据量大的时候,消耗的内存也比较大。
  • 序列:​​map​​​和​​fitler​​非内联,会生成匿名类实例,但不需要创建额外的集合保存中间操作的结果。

为什么要使用内联函数?内联函数的作用?
使用​​​lambda​​表达式可能带来的开销:

  • lambda表达式正常会被编译成匿名类。
  • 正常情况下,使用lambda表达式至少会生成一个对象,如果很不幸的使用了局部变量,那么每次使用该lambda表达式都会生成一个新的对象,导致使用lambda的效率比不使用还要低。

使用内联函数可以减少运行时的开销。内联函数主要作用:

  • 使用内联函数可以减少中间类和对象的创建,进而提升性能。主要原因是内联函数可以做到函数被使用的时候编译器不会生成函数调用的代码,而是使用函数实现的真实代码区替换每一次的调用。
  • 结合​​reified​​实化类型参数,解决泛型类型运行时擦除的问题。

5. 类型系统

Kotlin中的基本数据类型的理解?
在​​​Kotlin​​​中,使用的时候是不区分基本类型的,统一如下:
​​​Int​​​、​​Byte​​​、​​Short​​​、​​Long​​​、​​Float​​​、​​Double​​​、​​Char​​​和​​Boolean​​。

使用统一的类型并不意味着Kotlin中所有的基本类型都是引用类型,大多数情况下,对于变量、参数、返回类型和属性都会被编译成基本类型,泛型类会被编译成Java中的包装类,即引用类型。

只读集合和可变集合的区别?
在Kotlin中,集合会被分为两大类型,只读集合和可变集合。

  • 只读集合:对集合只有读取权限。
  • 可变集合:能够删除、新增、修改和读取元素。

但是有一点需要注意,只读集合不一定是不可变的,如果你使用的变量是只读集合,它可能是众多集合引用中的一个,任何一个集合引用都有可能是可变集合。

Array和IntArray的区别?
​​​Array<Int>​​​相当于​​Java​​​中的​​Integer[]​​​,​​IntArray​​​对应​​Java​​​中的​​int[]​​。

使用实化类型参数解决泛型擦除的原理是什么?
内联函数的原理是编译器把实现的字节码动态插入到每一次调用的地方。实化类型参数也正是基于这个原理,每次调用实化类型参数的函数的时候,编译器都知道此次作为泛型类型实参的具体类型,所以编译器每次调用的时候生成不同类型实参调用的字节码插入到调用点。

6. 协程

协程是什么?协程的有什么特点?
Kotlin官方文档上说:

协程的本质是轻量级的线程。

为什么说它是轻量级的线程,因为从官方角度来讲,创建十万个协程没什么问题打印任务不会存在问题,创建十万个线程会造成内存问题,可能会造成内存溢出。但是这个对比有问题,因为协程本质上是基于Java的线程池的,你去用线程池创建十万个打印任务是不会造成内存溢出的。

从上面我们可以得出结果,协程就是基于线程实现的更上层的Api,只不过它可以用阻塞式的写法写出非阻塞式的代码,避免了大量的回调,核心就是协程可以帮我自动的切换线程。

协程的原理?
很多人都会讲,协程中处理耗时任务,协程会先挂起,执行完,再切回来。我在这就浅显的分析这两步。

  • 挂起:协程挂起的时候会从挂起处将后面的代码封装成续体,协程挂起的时候,将挂起的任务根据调度器放到线程池中执行,会有一个线程监视任务的完成情况。
  • 线程切回:监视线程看到任务结束以后,根据需要再切到指定的线程中(主线程or子线程),执行续体中剩余的代码。
    详解请查看:
    ​​​《Kotlin/JVM 协程实现原理》​