2024年Android面试总结
1.kotlin与Java的区别和优点?
语法简洁性
Kotlin的语法比Java更为简洁,减少了冗余代码。例如,Kotlin支持自动类型推断,避免了大量的样板代码。Kotlin的函数表达式和扩展函数也使得代码更加简洁12。
空指针安全
Kotlin内置空指针安全特性,通过区分可为空和不可为空的类型,避免了空指针异常。Java则需要手动进行空值检查,容易引发空指针异常12。
扩展函数
Kotlin支持扩展函数,可以向现有类添加新的功能而无需修改原有代码。Java不支持扩展函数,需要通过继承或其他设计模式来实现类似功能23。
协程
Kotlin支持协程,这是一种用于并发编程的工具,使得异步代码更加高效和易读。Java则使用传统的线程和回调处理异步编程,代码较为复杂且难以维护24。
互操作性
Kotlin与Java具有良好的互操作性,Kotlin代码可以与Java代码无缝集成。Kotlin编译器将代码编译为JVM字节码,可以在Java虚拟机上运行12。
编译时间
Kotlin的编译时间比Java更快,这意味着开发过程中可以更快地看到代码效果5。
优点总结
- 简洁性:Kotlin的简洁语法减少了样板代码,提高了开发效率。
- 空指针安全:内置的空安全机制避免了空指针异常,提高了程序的稳定性。
- 扩展函数:允许向现有类添加新功能,无需修改原始代码,增强了代码的可维护性。
- 协程:提供了更高效、更易读的异步编程方式。
- 编译时间:更快的编译速度意味着更快的开发反馈。
2.kotlin中内联函数的作用?
内联函数
- 使用inline关键字修饰的[函数]称为内联函数
内联函数主要用于减少高阶函数的使用开销
我们在使用高阶函数的时候,我们传入的lambda都会被编译成一个Function对象,这个过程会带来一定的性能开销。 - 在编译时期,把调用这个函数的地方用这个函数的方法体进行替换。
3.kotlin中高阶函数apply、let、also、run的区别?
1.let 函数
- let的函数参数block中, 对应的上下文this和第一个run函数是一致的
- let和第二个run函数一样是一个extension函数, 但是它的block参数支持一个参数, 就是任意类型T的实例, 在extension函数定义时候, “this”就对应扩展类型实例.
2. also函数
2.1 作用 & 应用场景
类似let函数,但区别在于返回值:
- let函数:返回值 = 最后一行 / return的表达式
- also函数:返回值 = 传入的对象的本身
3. with函数
3.1 作用
调用同一个对象的多个方法 / 属性时,可以省去对象名重复,直接调用方法名 / 属性即可
3.2 应用场景
需要调用同一个对象的多个方法 / 属性
4. run函数
4.1 作用 & 应用场景
结合了let、with两个函数的作用,即:
- 调用同一个对象的多个方法 / 属性时,可以省去对象名重复,直接调用方法名 / 属性即可
- 定义一个变量在特定作用域内
- 统一做判空处理
5. apply函数
5.1 作用 & 应用场景
与run函数类似,但区别在于返回值:
- run函数返回最后一行的值 / 表达式
- apply函数返回传入的对象的本身
5.2 应用场景
对象实例初始化时需要对对象中的属性进行赋值 & 返回该对象
4.kotlin中Lambda与Java中Lambda区别?
编译后的表现形式
- Kotlin:Kotlin的Lambda表达式在编译后会生成匿名类,这些类继承自
Lambda
类并实现了Function
接口。每个Lambda表达式都对应一个Function
类型的类。 - Java:Java的Lambda表达式依赖于
LambdaMetafactory
动态生成匿名类,这些类通常是通过InvokeDynamic
指令实现的。Java的Lambda表达式要求引用的变量为final
或等效于final
,而Kotlin允许非final
变量,并通过编译时优化处理。
语法差异
- Kotlin:Kotlin的Lambda表达式总是括在花括号中,参数声明放在花括号内,函数体跟在一个
->
符号之后。如果推断出的该lambda的返回类型不是Unit
,那么该lambda主体中的最后一个表达式会被视为返回值3。 - Java:Java的Lambda表达式使用
(argument) -> {body}
语法书写,参数类型可以明确声明,也可以根据上下文推断。如果Lambda表达式的主体只有一条语句,花括号可以省略。
应用场景和好处
- 应用场景:在Kotlin和Java中,Lambda表达式都可以作为函数参数传递,使代码结构更加紧凑。它们常用于集合操作、过滤、映射等函数式编程场景4。
- 好处:Lambda表达式语法简洁,避免了冗长的函数声明调用过程,使得代码更加简洁易读。
5.kotlin的泛型和Java泛型区别?
类型擦除
- Java:泛型在编译时进行类型检查,但在运行时会被擦除,因此不能直接访问泛型参数的类型信息。
- Kotlin:泛型在编译时进行类型检查,但不会擦除类型信息,因此在运行时可以保留泛型参数的类型信息。
类型约束
- Java:使用
:ml-search[extends]
关键字来限制泛型类型的上界。 - Kotlin:使用
:ml-search[符号]
来限制泛型类型的上界,并且支持多重界限。
星号投影
- Java:使用通配符
?
来表示未知类型,但只能用于读取操作,不能用于写入操作。 - Kotlin:使用星号投影
*
来表示未知类型,可以用于读取和写入操作。
内联类
- Java:没有内联类的机制。
- Kotlin:引入了内联类的概念,允许将数据类型直接嵌入到值中,以提高性能。
协变和逆变
- Java:只支持有限的协变和逆变,需要使用通配符来实现。
- Kotlin:支持完全的协变和逆变,不需要使用通配符。
扩展函数和扩展属性
- Java:不支持扩展函数和扩展属性。
- Kotlin:支持扩展函数和扩展属性,可以在已有的类上添加新的方法或属性。
泛型类的定义和使用
- Kotlin:泛型类中的类型参数默认是不可变的,如果需要支持协变或逆变,需要使用
:ml-search[out]
和:ml-search[in]
关键字来指定。泛型类中的类型参数如果可以被推断出来,可以省略类型参数。
泛型在编译时的行为
- Kotlin和Java的泛型在编译时都会进行类型检查,但Kotlin在运行时保留了泛型参数的类型信息,而Java则会擦除这些信息。
6.kotlin中密封类和枚举特点?
在Kotlin中,密封类(sealed class)和枚举(enum class)都是为了表示有限的类型。
1.密封类:
- 密封类是当你需要为多种类型定义一个基类型时很有用。
- 密封类不能被直接实例化,只能是其子类的实例。
- 你可以使用
when
表达式来匹配密封类的不同子类。
2.枚举类:
- 当你想要定义一组固定的对象时,可以使用枚举类。
- 枚举常量是枚举的实例,不能有方法。
- 可以为枚举类添加属性和方法。
3 . 密封类作用 :
定义一个密封类 , 该类只能有有限个指定的子类 , 不能在其它文件定义其它类型子类 ;
4. 密封类与枚举类 :
相同点 ( 类型限制 ) : 从类型种类角度对比 , 类与枚举类类似 , 枚举类的值的集合是受限制的 , 不能随意扩展 ;
** 不同点 ( 对象个数限制 ) 😗* 从每个类型对象个数对比 , 枚举类的每个类型只能存在一个实例 , 而密封类的每个类型可以创建无数个实例 ;
7.koltin中val和var区别?
**Kotlin中的val和var的主要区别在于变量的可变性。**
- val:用于声明一个只读变量(immutable variable),即一旦赋值就不能再修改。val声明的变量相当于Java中的final变量,只能被赋值一次。例如:
val name = "John"
,之后尝试将name
重新赋值为"Alice"
会导致编译错误。 - var:用于声明一个可变变量(mutable variable),可以随时被重新赋值。var声明的变量可以多次赋值。例如:
var age = 25
,之后可以将age
重新赋值为30,这是合法的操作。
具体使用场景
在实际编程中,应根据变量是否需要被修改来选择使用val还是var:
- 如果变量在初始化后不需要修改,使用val。
- 如果变量在程序运行过程中需要多次修改,使用var。
对象和类的使用场景
对于对象和类的实例,val声明的对象实例一旦创建后不能重新实例化,但对象的属性可以修改
8.kotlin中伴生对象作用?
声明:
Kotlin 中,在类中定义的对象(object)声明,可使用 companion
修饰,这样此对象(object)就是伴生对象了
调用:
使用 companion
关键字修改的对象之后,伴生对象就相当于是外部类的对象,我们可以使用类直接调用
伴生对象的作用:
通过上面的 NumberTest.plus(1, 2) 和 NumberTest.flag 代码不难看出来,类似于 Java 中使用类访问静态成员的语法。因为 Kotlin 取消了 static 关键字,所以 Kotlin 引入伴生对象来弥补没有静态成员的不足。可见,伴生对象的主要作用就是为其所在的外部类模拟静态成员。
总结:
- 每个类可以最多有一个半生对象;
- 伴生对象的成员类似于 Java 的静态成员;
- 使用 const 关键字修饰常量,类似于 Java 中的 static final修饰。
- 可以使用 @JvmField 和 @JvmStatic 类似于 Java 中调用静态属性和静态方法;
- 伴生对象可以扩展属性和扩展方法。
9.kotlin中函数如何声明?构造函数类型?区别是什么?
类的声明
使用class+类名来声明,若类体中无内容,类体后的花括号可以省略
构造函数类型:
Kotlin中的类可以有三种构造函数类型:主构造函数、带init
块的构造函数和次构造函数
主构造函数:
在类名后直接定义参数列表,没有花括号包围的函数体。主构造函数用于初始化类的成员变量
带init
块的构造函数:
在主构造函数之后定义一个init
块,用于执行初始化代码。
次构造函数:
在类体内部定义,使用constructor
关键字。次构造函数可以调用主构造函数或另一个次构造函数进行初始化
区别:
- 主构造函数是最简洁的方式,适用于简单的初始化操作。它不允许在函数体中包含任何代码。
- 带
init
块的构造函数适用于需要在构造函数中执行一些初始化代码的情况。它允许在主构造函数之后添加初始化代码块。 - 次构造函数提供了最大的灵活性,可以调用其他构造函数,适用于复杂的初始化逻辑。它允许在类体内部定义,并且可以包含多个次构造函数。
10.kotlin中协程的启动方式有哪些?有啥区别?
协程的三种启动方式:launch、runBlocking、async
launch:
launch是最常用的启动协程方式,它会立即返回一个Job对象,后台执行协程任务 如果在启动协程时使用了try-catch,那么异常会被try-catch块捕获。如果没有捕获,异常会传递给未捕获异常处理器进行处理
应用场景:如果需要启动一个独立协程,并且不关心其结果或异常,最好使用launch。
runBlocking:
runBlocking是一个阻塞式函数,用于测试和调试协程代码 它会启动一个新的协程,并等待该协程执行完毕后才会返回。在调用runBlocking时,当前线程将被阻塞,直到协程执行完毕。
应用场景:如果需要在测试和调试环境中启动协程,最好使用runBlocking。
async:
async用于启动一个独立协程,并返回Deferred对象,通过该对象可以获取协程的执行结果 如果在启动协程时使用了try-catch,那么异常会延迟到await函数调用时抛出。
应用场景:如果需要启动一个独立协程,并获取其执行结果,最好使用async。
协程的启动模式
Kotlin协程的启动模式包括以下几种:
- **[DEFAULT]**:创建后立即调度执行。如果协程在调度前被取消,将直接进入取消状态。
- **[ATOMIC]**:创建后立即调度执行,但在挂起点前不可取消。如果协程在挂起点前被取消,将直接进入异常状态。
- **[LAZY]**:只有在需要时(通过start、join或await等方法)才开始调度。如果协程在调度前被取消,将直接进入异常状态。
- **[UNDISPATCHED]**:在当前函数调用栈中执行,直到遇到第一个挂起点。如果协程在挂起点前被取消,将直接进入取消状态。
11.kotlin中协程原理及优点?
1. 协程的定义
协程是一种轻量级的线程,它们在用户代码中运行,可以挂起和恢复执行。与传统线程相比,协程更为轻量,不需要昂贵的上下文切换开销。Kotlin 中的协程允许开发者以同步的方式编写异步代码,从而使代码更加简洁和易读。
1.1 协程的特性
- 挂起和恢复:协程可以在执行过程中被挂起,稍后再恢复执行。这使得协程能够高效地处理耗时的操作,比如网络请求或文件读写。
- 轻量级:协程的创建和销毁开销非常小,通常可以在同一线程中创建成千上万的协程,而不必担心内存和性能问题。
- 可组合性:协程可以与其他协程进行组合,方便地构建复杂的异步操作。
2. Kotlin 中的协程工作原理
Kotlin 的协程是通过 suspend
函数和 CoroutineScope
来实现的。协程的基本构建块是挂起函数,这些函数可以在协程中调用,并且可以被挂起。
2.1 suspend
函数
suspend
函数是可以在协程中挂起的函数。它们的调用不会阻塞线程,而是将协程挂起,直到操作完成。
2.2 CoroutineScope
CoroutineScope
是协程的上下文,它定义了协程的生命周期和调度器。Kotlin 提供了多种协程作用域,如 GlobalScope
和自定义作用域。
2.3 协程的调度器
Kotlin 的协程调度器负责决定协程的执行线程。常见的调度器有:
Dispatchers.Main
:用于在主线程中执行协程,适用于 UI 更新。Dispatchers.IO
:用于进行 I/O 操作的协程,适用于网络请求或文件操作。Dispatchers.Default
:用于执行 CPU 密集型任务的协程。
3.特点:
创建协程:
可以使用 launch
或 async
函数来创建协程。
launch
:启动一个协程,不返回结果。async
:启动一个协程并返回Deferred
对象,可以通过该对象获取结果。
协程本质上可以认为是运行在线程上的代码块,协程提供的 挂起 操作会使协程暂停执行,而不会导致线程阻塞。
协程是一种轻量级资源,如上图所示,即使创建了上千个协程,对于系统来说也不是一种很大的负担。
包含关系上看,协程跟线程的关系,有点像“线程与进程的关系”,毕竟,协程不可能脱离线程运行;协程虽然不能脱离线程而运行,但可以在不同的线程之间切换,如上图所示。
协程的核心竞争力在于:简化异步并发任务(同步的写法实现异步操作)。
线程:
- 线程是操作系统级别的概念
- 我们开发者通过编程语言(Thread.java)创建的线程,本质还是操作系统内核线程的映射
- JVM 中的线程与内核线程的存在映射关系,有“一对一”,“一对多”,“M对N”。JVM 在不同操作系统中的具体实现会有差别,“一对一”是主流
- 一般情况下,我们说的线程,都是内核线程,线程之间的切换,调度,都由操作系统负责
- 线程也会消耗操作系统资源,但比进程轻量得多
- 线程,是抢占式的,它们之间能共享内存资源,进程不行
- 线程共享资源导致了多线程同步问题
- 有的编程语言会自己实现一套线程库,从而能在一个内核线程中实现多线程效果,早期 JVM 的“绿色线程” 就是这么做的,这种线程被称为“用户线程”
协程:
- Kotlin 协程,不是操作系统级别的概念,无需操作系统支持
- Kotlin 协程,有点像上面提到的“绿色线程”,一个线程上可以运行成千上万个协程
- Kotlin 协程,是用户态的(userlevel),内核对协程无感知
- Kotlin 协程,是协作式的,由开发者管理,不需要操作系统进行调度和切换,也没有抢占式的消耗,因此它更加高效
- Kotlin 协程,它底层基于状态机实现,多协程之间共用一个实例,资源开销极小,因此它更加轻量
- Kotlin 协程,本质还是运行于线程之上,它通过协程调度器,可以运行到不同的线程上
4. 协程的优势与挑战
4.1 协程的优势
- 简化异步代码:使用协程,开发者可以以同步的方式编写异步代码,使代码更易于理解和维护。
- 减少回调地狱:传统的异步编程通常依赖于回调函数,容易导致“回调地狱”,而协程通过挂起函数解决了这个问题。
- 轻量级和高效:协程的上下文切换开销非常小,可以在同一线程中高效管理多个协程。
4.2 协程的挑战
- 学习曲线:对于初学者,协程的概念和使用方式可能需要一些时间来掌握。
- 调试:协程的调试可能会更复杂,因为它们在后台异步执行,跟踪执行路径可能不如同步代码直观。
- 内存管理:尽管协程是轻量级的,但仍然需要注意协程的生命周期和资源管理,以避免内存泄漏。
12.kotlin协程调度器原理?
分类:
在Kotlin中,主要有以下几种Dispatcher
:
Default
- 默认的调度器,使用了默认的executor(通常是全局的后台线程池)。Main
- 专门用于与界面交互的调度器,在Android中表示UI主线程。IO
- 用于I/O密集型任务的调度器,执行环境是后台线程池。Unconfined
- 不带上下文的调度器,它在其他调度器之上提供了无阻塞的调用。NewSingleThread
- 创建单个线程的调度器,保证任务在同一线程中顺序执行。
作用:
协程就是通过Dispatchers调度器来控制线程切换的
什么是调度器?
从使用上来讲,调度器就是我们使用的Dispatchers.Main,Dispatchers.Default,Dispatcher.IO等
从作用上来讲,调度器的作用是控制协程运行的线程
从结构上来讲,Dispatchers的父类是ContinuationInterceptor,然后再继承于CoroutineContext
调度器的具体实现
我们首先明确下,CoroutineDispatcher是通过CoroutineContext取出来的,这也是协程上下文作用的体现
nternal class HandlerContext private constructor(
private val handler: Handler,
private val name: String?,
private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
public constructor(
handler: Handler,
name: String? = null
) : this(handler, name, false)
//...
override fun dispatch(context: CoroutineContext, block: Runnable) {
// 利用主线程的 Handler 执行任务
handler.post(block)
}
}
如上所示,其实就是一个装饰模式
1.调用CoroutinDispatcher.dispatch方法切换线程
2.切换完成后调用DispatchedTask.run方法,执行真正的协程体
delay是怎样切换线程的?
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ {
cont: CancellableContinuation<Unit> ->
// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
if (timeMillis < Long.MAX_VALUE) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay
internal class HandlerContext private constructor(
private val handler: Handler,
private val name: String?,
private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
// 利用主线程的 Handler 延迟执行任务,将完成的 continuation 放在任务中执行
val block = Runnable {
with(continuation) {
resumeUndispatched(Unit) }
}
handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))
continuation.invokeOnCancellation {
handler.removeCallbacks(block) }
}
//..
}
1.可以看出,其实也是通过handler.postDelayed实现延时效果的
2.时间到了之后,再通过resumeUndispatched方法恢复协程
3.如果我们用的是Dispatcher.IO,效果也是一样的,不同的就是延时效果是通过切换线程实现的
withContext是怎样切换线程的?
fun test(){
viewModelScope.launch(Dispatchers.Main) {
print("1:" + Thread.currentThread().name)
withContext(Dispatchers.IO){
delay(1000)
print("2:" + Thread.currentThread().name)
}
print("3:" + Thread.currentThread().name)
}
}
//1,2,3处分别输出main,DefaultDispatcher-worker-1,main
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
return suspendCoroutineUninterceptedOrReturn sc@ {
uCont ->
// 创建新的context
val oldContext = uCont.context
val newContext = oldContext + context
....
//使用新的Dispatcher,覆盖外层
val coroutine = DispatchedCoroutine(newContext, uCont)
coroutine.initParentJob()
//DispatchedCoroutine作为了complete传入
block.startCoroutineCancellable(coroutine, coroutine)
coroutine.getResult()
}
}
private class DispatchedCoroutine<in T>(
context: CoroutineContext,
uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
//在complete时会会回调
override fun afterCompletion(state: Any?) {
afterResume(state)
}
override fun afterResume(state: Any?) {
//uCont就是父协程,context仍是老版context,因此可以切换回原来的线程上
uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont))
}
}
-
1.withContext其实就是一层Api封装,最后调用到了startCoroutineCancellable,这就跟launch后面的流程一样了,我们就不继续跟了
-
2.传入的context会覆盖外层的拦截器并生成一个newContext,因此可以实现线程的切换
-
3.DispatchedCoroutine作为complete传入协程体的创建函数中,因此协程体执行完成后会回调到afterCompletion中
-
4.DispatchedCoroutine中传入的uCont是父协程,它的拦截器仍是外层的拦截器,因此会切换回原来的线程中
13.kotlin协程作用域有哪些?有啥区别?
**Kotlin协程作用域的类型和区别:
- **[GlobalScope]**:这是一个单例对象,不绑定到任何Job上,无法取消。通过它启动的子协程不会阻塞其所在线程,可以一直运行到应用程序停止。GlobalScope适用于全局性的协程,但需要注意,由于它不绑定到任何Job上,可能会导致内存泄漏和CPU资源浪费。
- **[ContextScope]**:通过上下文创建的协程作用域,包括[MainScope]等。MainScope默认使用[SupervisorJob],适用于Android开发中管理UI线程的协程生命周期。CoroutineScope则允许传入自定义的上下文对象。
- **[coroutineScope]**:创建一个新的协程作用域,所有在该作用域内启动的协程会在作用域结束时被取消。如果作用域内的协程失败,异常会向上传递到父协程。
- **[supervisorScope]**:与coroutineScope类似,但异常不会向上传递到父协程。这意味着在一个supervisorScope作用域内的协程失败时,不会影响其他协程的执行。
协程作用域的使用场景和区别:
- GlobalScope:适用于全局性的、长时间运行的协程,但需要小心管理以避免内存泄漏。
- ContextScope:适用于需要特定上下文的场景,如Android开发中的UI线程管理。
- coroutineScope:适用于需要确保所有子协程在作用域结束时被取消的场景。
- supervisorScope:适用于需要并发执行多个任务,但希望某个任务的失败不影响其他任务的场景。
协程作用域的优缺点:
- 优点:
- 结构化并发:协程作用域帮助管理并发任务的生命周期,确保资源正确释放。
- 异常处理:在supervisorScope中,异常不会向上传递,减少了异常对其他任务的影响。
- 内存管理:协程是轻量级的,不会像线程那样占用大量资源。
- 缺点:
- 滥用GlobalScope:如果不正确管理,可能会导致内存泄漏和资源浪费。
- 学习曲线:对于初学者来说,理解和正确使用协程作用域可能需要一定的时间
14.kotlin中静态方法和Java静态方法有啥区别?
Java&Kotlin–静态属性和静态方法
静态方法和属性是什么:
- Java:用 static 修饰符修饰的属性(成员变量)、常量和成员方法称为静态变量、常量和方法,它们统称为静态成员,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
- Kotlin:kotlin并没有static关键词,但仍然有静态成员的概念。kotlin会通过companion object生成一个内部静态类,和@JvmStatic注解搭配使用,可以将变量和函数声明为真正的静态成员。
使用静态方法和对象方法的时机:
- 需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见
- 当一个方法不访问这个类的数据和对象方法
- 在项目的多模块,多个类中不希望创建具体对象就能使用某个类的方法时
- 这个方法需要访问这个类的数据 和对象方法,例如需要对某个数据进行存储时
- 需要通过一个实例的对象进行某些操作时
使用静态成员函数和静态数据成员的优缺点:
优点:
- 节省内存:被所有对象公用,即使有多个对象也只存储一处
- 提高时间效率:静态函数会自动分配在一个一直使用的存储区,直到退出应用实例,避免了调用函数时压栈出栈
- 避免重名冲突:静态函数表示是内部函数,函数作用域仅局限于本文本,避免与其他文件重名冲突
缺点:
- 静态成员不能访问非静态成员函数和非静态数据成员
- 初始化时便加载到内存,若后续未被引用加大了内存负担和程序运行负担。
15.kotlin中when和Switch的区别?
功能和用法的区别
- 类型支持:
- Kotlin的when表达式:可以处理任意类型的数据,包括枚举类型、字符、字符串等。它不仅支持基本数据类型,还可以处理复杂的对象类型12。
- Java的switch语句:只能处理整数、字符和枚举类型,不支持复杂的对象类型1。
- 条件表达式:
- Kotlin的when表达式:可以使用表达式作为分支条件,使代码更加简洁和易读。它还支持范围、类型判断、集合等条件12。
- Java的switch语句:只支持常量值作为分支条件,不支持表达式1。
- 语法和灵活性:
- Kotlin的when表达式:功能更加强大和灵活,可以替代if-else语句,支持多种条件判断和自动转型12。
- Java的switch语句:语法相对固定,不支持复杂的条件表达式和自动转型1
16.kotlin中空安全的实现原理?
要点:
- Kotlin中通过「非空类型」与「可空类型」来规避 NPE;
- 可空操作符「
?
」、安全调用操作符「?.
」、非空断言运算符「!!
」; - **Elvis操作符「
?:
」**如果不为空返回它,否则返回另一个值; - 安全的类型转换「
as?
」 - ?.let{}
- 不是绝对的空指针安全:Kotlin调用返回空的Java代码;
Kotlin空安全的实现原理:
总结:
- 1、非空类型的属性编译器添加**@NotNull注解,可空类型添加@Nullable**注解;
- 2、非空类型直接对参数进行判空,如果为空直接抛出异常;
- 3、可空类型,如果是?.判空,不空才执行后续代码,否则返回null;如果是!!,空的话直接抛出NPE异常。
- 4、as操作符会判空,空的话直接抛出异常,不为空才执行后续操作,没做类型判断!运行时可能会报错!
- 5、as?则是新建一个变量,参数赋值给它,然后判断是否为特定类型,赋值为null,接着把这个变量的值赋值给另一个新的变量,这里有一点注意:as?处理后的参数可能为空!!!所以调用as?转换后的对象还需要添加安全调用操作符(?.)
17.@JvmStatic、@JvmOverloads、@JvmFiled 在 Kotlin 中有什么作用?
@JvmOverloads
在Kotlin
的方法里有多个默认参数时,如果在Java
中直接调用,只能调用一个包含完整参数的方法,如果想暴露更多的重载函数给Java
,可以使用@JvmOverloads
用于生成重载。对于每一个有默认值的参数,生成的重载会把当前有默认值的参数及其右边的参数都去掉,所以如果方法中所有的参数都有默认值,生成的重载函数中还会有一个无参的重载函数。
@JvmOverloads
主要用于构造函数、方法中,同时不能用于抽象方法、接口中的方法等。
@JvmStatic
@JvmStatic
用于声明静