高阶函数:
- 高阶函数是指可以接受函数作为参数,或者将函数作为返回值的函数。每一个高阶函数都是一个对象
- lambda 表达式与匿名函数是函数字面值,函数字面值即没有声明而是立即做为表达式传递的函数。(函数字面值是指在编程语言中可以直接表示一个函数的值,而无需将其绑定到特定的名称。)
- 使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包。 内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。 但是在许多情况下通过内联化 lambda 表达式可以消除这类的开销。
- 闭包是一种编程语言的特性,它指的是一个函数(或者称为子程序),以及定义该函数的环境。换句话说,闭包可以捕获并保存其周围作用域中的变量状态,即使在其定义的位置已经执行完毕后,闭包仍然可以访问和操作这些变量。
例如:在这个例子中,outerFunction 是一个外部函数,它定义了一个局部变量 count 并返回一个函数。这个返回的函数捕获了 count 变量,并且每次被调用时,都会增加 count 的值并输出结果。这个返回的函数形成了一个闭包,因为它捕获了其所在作用域(outerFunction)中的变量 count。
fun outerFunction(): () -> Unit {
var count = 0
return {
count++
println("Count: $count")
}
}
- 定义一个高阶函数:
fun testFun(string1: String, string2: String, method: (String, String) -> String) {
println(method(string1, string2))
}
- 调用高阶函数:
方法一:
testFun("你好啊", " hello") { string1: String, string2: String ->
"$string1 ++ $string2"
}
方法二
val stringFun = fun(string1: String, string2: String): String {
return "$string1 ++ $string2"
}
testFun("你好啊", " hello", stringFun)
定义lambda表达式:
- lambda 表达式总是括在花括号中。
- 完整语法形式的参数声明放在花括号内,并有可选的类型标注。lambda 表达式的参数类型是可选的,如果能够推断出来的话
- 函数体跟在一个 -> 之后, 参数放在之前。
- 如果推断出的该 lambda 的返回类型不是 Unit,那么该 lambda 主体中的最后一个(或可能是单个)表达式会视为返回值。
- 函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外;lambda 表达式是调用时唯一的参数,那么圆括号可以完全省略
- 单个参数可以使用隐式名称
- 可以使用限定的返回语法从 lambda 显式返回一个值。 否则,将隐式返回最后一个表达式的值。
- 下划线用于未使用的变量,例如:
map.forEach { _, value -> println("$value!") } - 定义lambda表达式:
val sum1: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
val sum2 = { x: Int, y: Int -> x + y }
val sum3 = fun(x: Int, y: Int): Int = x + y
sum1(1,2)
sum2(1,2)
sum3(1,2)
- 定义匿名函数:
val sum3 = fun(x: Int, y: Int): Int = x + y
sum3(1,2)
- 当匿名函数作为参数传递时,需将其放在括号内。 允许将函数留在圆括号外的简写语法仅适用于 lambda 表达式。
内联函数:
高阶函数效率损失可能主要体现在以下几个方面:
1、对象分配: 在 Kotlin 中,函数是一等公民,因此函数本身就是对象。当你定义一个高阶函数时,编译器会生成相应的函数对象。因此,如果频繁地创建和使用高阶函数,可能会导致额外的对象分配开销,增加了垃圾回收的压力,尤其是在性能敏感的场景下。
2、闭包捕获: 如果高阶函数形成了闭包,即内部函数捕获了外部函数的局部变量,这会导致额外的运行时开销。闭包需要在运行时捕获并存储其引用的变量,这可能增加内存使用并引入一些额外的访问开销,尤其是在闭包的生命周期比外部函数长的情况下。
3、额外的函数调用开销: 使用高阶函数可能会引入额外的函数调用开销。当你将一个函数作为参数传递给另一个函数时,会引入额外的函数调用以及相关的参数传递和返回值处理。尤其是对于小规模的、频繁调用的函数,这可能对性能产生一定的影响。在内联函数的调用处:编译器没有为参数(高阶函数)创建一个函数对象并生成一个调用。 Kotlin 的内联函数是一种特殊的优化机制,它可以减少函数调用的开销。当一个函数被标记为 inline 时,在编译时会将函数的代码直接插入到调用处,而不是通过函数调用的方式。这样做可以减少函数调用的开销,尤其是对于一些简单的函数或者 Lambdas。
内联可能导致生成的代码增加。不过如果使用得当(避免内联过大函数), 性能上会有所提升,尤其是在循环中的“超多态(megamorphic)”调用处。
如果不希望内联所有传给内联函数的 lambda 表达式参数都内联,那么可以用 noinline 修饰符标记不希望内联的函数参数:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { …… }
- 如果一个内联函数没有可内联的函数参数并且没有具体化的类型参数,编译器会产生一个警告,因为内联这样的函数很可能并无益处(如果你确认需要内联, 那么可以用
@Suppress("NOTHING_TO_INLINE")
注解关掉该警告)。 - 在 Kotlin 中,只能对具名或匿名函数使用正常的、非限定的 return 来退出。 要退出一个 lambda 表达式,需要使用一个标签。 在 lambda 表达式内部禁止使用裸
return
,因为 lambda 表达式不能使包含它的函数返回 - 但是如果 lambda 表达式传给的函数是内联的,该 return 也可以内联。
- break 和 continue 在内联的 lambda 表达式中还不可用,但我们也计划支持它们。
- inline也可以修饰属性:内联属性
局部函数:
- Kotlin 支持局部函数,即一个函数在另一个函数内部;局部函数可以访问外部函数(闭包)的局部变量。局部函数只在包含它们的函数内部可见和可用,不能在外部函数之外访问。其具有:封装性、可重用性
fun calculateArea(radius: Double): Double {
fun calculateCircleArea(r: Double): Double {
return Math.PI * r * r
}
// 调用局部函数
return calculateCircleArea(radius)
}
递归函数:
- 当一个函数用 tailrec 修饰符标记并满足所需的形式条件时,编译器会优化该递归
tailrec fun findFixPoint(x: Double = 1.0): Double =
if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))
中坠函数(infix)
- 中缀函数是 Kotlin 中的一种特殊类型函数,允许您使用特定的语法来调用方法,即中缀符号。
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
val result = point1 to point2
- Lambda 表达式的编译: Kotlin 中的 Lambda 表达式会被编译成函数对象(Function Object),这些函数对象可以在运行时被调用。
kotlin 扩展为什么优于继承
1、无需修改源代码: 扩展函数和属性可以在不修改原始类代码的情况下为现有类添加新功能。这对于无法访问源码或者源码受限的情况非常有用。
2、避免类的继承链过深
3、灵活性和可扩展性: 扩展函数使得你可以轻松地向任何类添加新的功能,而无需考虑类的层次结构或修改大量代码。这使得你的代码更具扩展性和灵活性,可以适应未来需求的变化。
4、代码组织和可读性: 使用扩展函数可以更清晰地组织代码,使得类的功能分散在不同的文件中,而不会使类的定义变得冗长。这有助于提高代码的可读性和可维护性。
kotlin的拓展方法
- Kotlin语言支持对现有的类进行扩展。 所谓扩展,就是在不使用继承的情况下,对已有的类新增方法、属性等操作,扩展不会破坏现有的类,仅仅在使用的时候进行动态添加。原则: 扩展优于继承
- 例:给User类添加Print方法:
class User(var name:String) {
companion object { } // 将被称为 "Companion"
}
// 给user类增加方法
fun User.Print(){
print("用户名 $name")
}
// 伴生对象的扩展
User.Companion.foo() {
println("伴随对象的扩展函数")
}
// 扩展属性,检查年龄是否大于等于 18
val User: Boolean
get() = age >= 18
//调用
fun main(arg:Array<String>){
var user = User("Runoob")
user.Print()
}
Java中,一个对象在内存中的标识主要涉及以下几个方面的信息:
- 内存地址(Memory Address): 每个Java对象都在内存中占据一块连续的存储空间,而对象的内存地址表示了这块存储空间的起始位置。这个地址是唯一的,用于在程序中标识对象的位置。
- 对象头(Object Header): 对象头是存在于对象内存布局中的一部分,它包含了一些元信息,比如对象的锁信息、GC标记等。对象头的内容是由Java虚拟机负责管理的,而开发者无法直接操作它。
- 哈希码(HashCode): 哈希码是一个整数值,通过对象的 hashCode() 方法计算得到。它是用来支持哈希表等数据结构的,通常用于快速查找对象。哈希码在一些集合类中被用作对象的唯一标识。
- 同步信息(Synchronization Information): 如果一个对象被设计为支持同步(即多线程访问),则需要存储一些同步信息,如锁状态、持有锁的线程等。这些信息用于在多线程环境下保证对象的正确同步访问。
- 对象的实际数据: 这部分存储了对象的实际数据,即对象的字段和属性。这些数据根据对象的类定义而有所不同。
19、组合(引用)跟继承的使用场景区别:
- 在 Kotlin 中,组合(Composition)是一种设计模式,它允许一个类包含其他类的实例作为其成员变量,从而实现代码重用和模块化。与继承不同,组合强调的是对象之间的关联关系,而不是直接的类层次结构。
以下是 Kotlin 中使用组合的基本示例:
// 定义一个 Car 类,它包含 Engine 和 Wheels 实例作为其成员变量
class Car(private val engine: Engine, private val wheels: Wheels) {
fun start() {
engine.start()
println("Car is running on ${wheels.count} wheels")
}
}
// 定义 Engine 类
class Engine {
fun start() {
println("Engine started")
}
}
// 定义 Wheels 类
class Wheels(val count: Int)
- 区别:继承与组合都是面向对象中代码复用的方式。在继承中:父类的内部细节对子类可见,其代码属于白盒式的复用;而组合中,对象之间的内部细节不可见,其代码属于黑盒式复用。
- 继承的优点:
代码重用、快速开发
抽象和多态
扩展性
维护性
统一接口
- 继承的缺点:
1、紧耦合(Tight Coupling): 继承会在父类和子类之间创建一种紧密的关联关系,使得子类依赖于父类的实现细节。如果父类的实现发生变化,可能会影响到所有的子类,导致系统的脆弱性增加。
2、单继承限制(Single Inheritance Restriction): 在很多面向对象编程语言中,类只能继承自一个父类,这被称为单继承。这限制了一个类同时从多个源继承行为,而有时候多重继承可能更为自然或更符合问题领域的模型。
3、层次结构复杂性(Hierarchy Complexity): 随着继承层次的增加,系统的类层次结构可能会变得非常复杂。这会增加理解和维护代码的难度,尤其是在大型项目中。
4、父类变更困难性(Difficulty in Modifying the Parent Class): 如果父类发生变更,可能需要修改所有的子类。这样的修改可能会导致不稳定性和破坏性的变化,特别是在没有适当测试的情况下。
5、过度继承(Overuse of Inheritance): 有时候开发者可能过度使用继承,导致一个庞大的类层次结构,其中很多类之间的关系不是很清晰。这样的过度继承可能会导致代码难以理解和维护。
- 组合是面向对象编程中的一种设计模式,它允许将对象组合成更大的结构,以构建复杂的系统。组合模式有一些优点和缺点:
- 优点:
1、灵活性: 组合模式提供了灵活性,可以通过组合不同的对象来构建不同的结构。这样可以轻松地添加、删除或替换组件,而不会影响系统的其他部分。
2、可扩展性: 由于组合模式将对象组织成树状结构,新的组件可以很容易地被添加到系统中,而不需要修改现有的代码。
3、简化客户端代码: 客户端代码不需要知道组合内部的具体结构,只需要通过通用的接口与组合对象交互。这简化了客户端代码,使其更易于理解和维护。
3、统一接口: 组合模式通过使用相同的接口来表示单个对象和组合对象,使得客户端可以一致地处理单个对象和组合对象。
4、代码复用: 由于组合模式允许对象组合成树状结构,可以通过复用已有的对象来构建新的组合对象,从而提高代码的复用性。 - 缺点:
1、复杂性增加: 随着组合对象的增加,系统的复杂性可能会增加。管理和理解一个深层次的组合结构可能会变得复杂,尤其是在大型系统中。
3、性能问题: 在某些情况下,由于组合模式的结构,可能导致性能问题。例如,对整个组合结构执行某些操作可能比直接操作单个对象要慢。
4、难以限制组件类型: 在组合模式中,通常无法限制组件的类型。这意味着客户端可以向组合对象中添加不同类型的组件,这可能导致运行时错误。
操作符
- ?:
对象A ?: 对象B 表达式:当对象 A值为 null 时,那么它就会返回后面的对象 B - as运算符和as?运算符
as运算符用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容,转换就会成功进行;如果类型不兼容,使用as?运算符就会返回值null。在Kotlin中,父类是禁止转换为子类型的。 - is运算符和 !is 运算符
kotlin中API提供的 is 运算符类似于Java中的 instanceof 关键字的用法。is 运算符可以检查对象是否与特定的类型兼容(兼容:此对象是该类型,或者派生类),同时也用来检查对象(变量)是否属于某数据类型(如Int、String、Boolean等)。 !is运算符是它的否定形式。 - 联合使用:
foo as? Type -> foo is Type retrun (foo as Type)
-> foo !is Type return null
//as?和?:联合使用
object as? Person ?: "not human"
object as? Person ?: return false
(Boolean).yes {
// dosomething
}
- TODO():将代码标记为不完整(代码执行到此处会报错)
TODO("提示内容")
- === 表示比较对象地址,== 表示比较两个值大小
- for循环:
for (i in array.indices) {
println(array[I])
}
for ((index, value) in array.withIndex()) {
println("the element at $index is $value")
}
伴生对象:
-
Kotlin中没有静态成员,主要原因在于它允许包级属性和函数的存在;Kotlin为静态成员提供了多种替代的方案:
1、使用包级属性和函数:主要用于全局常量和工具函数;
2、使用伴生对象:主要用于与类有紧密联系的变量和函数;
3、使用@JvmStatic注解:与伴生对象搭配使用,将变量和函数声明为真正的JVM静态成员。 - 在类加载时就会运行伴生对象的代码块,其作用相当于Java里面的static { ... }代码块
- java静态成员的优点:
1、可以使类中的某些变量和方法与该类绑定。
2、可以使该类的所有对象共享这个变量和方法,无需为每个对象分配该变量的资源,充分节省资源。 - Kotlin中没有静态变量,So它使用了伴生对象来模仿Java中静态变量的作用。伴生对象也是在类加载阶段初始化,同样生命周期与该类的生命周期一致且也可以直接通过类名.(attribute,method)来调用。该类的多个对象共享该伴生对象。
- 密封类:
密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。密封类和枚举同作为一个值的列举功能,但是密封类的功能更加强大。体现在:
密封类的每个子类可以有多个对象,枚举常量就是枚举类型的一个对象
密封类子类可以持有其他引用数据,枚举常量只能持有枚举类构造方法传入的值
密封类可以自定义方法,枚举常量只能重写枚举类的方法
- Kotlin 和 Java 都是流行的编程语言,用于开发各种类型的应用程序,特别是在 Android 开发领域。它们各自有一些优点和缺点,让我们来看看:
Kotlin 的优点:
- 简洁性: Kotlin 可以显著减少代码量,相对于 Java,它具有更简洁、更易读的语法,同时保留了与 Java 类似的表达能力。
- 安全性: Kotlin 在类型系统中引入了空安全(null safety)的概念,这意味着编译器会在编译时捕获可能导致空指针异常的代码,从而减少了空指针异常的风险。
- 互操作性: Kotlin 可以与现有的 Java 代码无缝地互操作,这使得开发者可以逐步地将现有的 Java 项目迁移到 Kotlin,而无需重写所有代码。
- 函数式编程支持: Kotlin 具有对函数式编程的良好支持,包括 lambda 表达式、高阶函数、数据类等,这使得编写函数式风格的代码更加简洁和直观。
- 扩展函数和属性: Kotlin 允许通过扩展函数和扩展属性为现有类添加新的功能,而不需要继承或修改原始类的代码,这提高了代码的灵活性和可维护性。
Kotlin 的缺点:
- 学习曲线: 尽管 Kotlin 的语法相对简洁,但对于有 Java 经验的开发者来说,学习 Kotlin 仍然需要一定的时间和投入。
- 编译时间: Kotlin 编译器相对于 Java 编译器可能会更慢,尤其是在项目规模较大时,这可能会导致稍长的编译时间。Kotlin 编译器本身相对于 Java 编译器更复杂。它需要解析 Kotlin 的语法,并将其转换为 Java 字节码或其他目标平台的代码。复杂的编译器逻辑可能会导致编译时间增加。
- 库和工具支持: Kotlin 虽然在不断增加其库和工具的支持,但与 Java 相比,其生态系统仍然相对较小。因此,在某些特定领域的库和工具支持可能不如 Java。
Java 的优点:
- 稳定性和成熟度: Java 是一门成熟的编程语言,已经存在多年,拥有庞大的社区和丰富的库支持。它已经在各种领域广泛应用,并且被广泛认可。
- 广泛的平台支持: Java 可以在多种平台上运行,包括桌面、服务器、移动设备等,这使得它成为开发各种类型应用程序的理想选择。
- 强大的工具支持: Java 生态系统拥有丰富的开发工具和集成开发环境(IDE),如 IntelliJ IDEA、Eclipse 等,这些工具可以极大地提高开发效率。
- 大型项目的可维护性: Java 的静态类型系统和面向对象编程范式使得它非常适合开发大型项目,有助于保持代码的结构清晰和可维护性。
Java 的缺点:
- 冗长的语法: Java 的语法相对冗长,编写相同功能的代码通常需要比 Kotlin 更多的代码行数,这可能会导致代码的可读性下降。
- 空指针异常: Java 的空指针异常(NullPointerException)是一个常见的问题,开发者需要自行处理潜在的空指针异常情况,这可能会增加代码的复杂性。
- 缺乏某些现代特性: 相对于一些新兴的编程语言,如 Kotlin、Swift 等,Java 在某些现代特性上可能稍显滞后,如空安全、扩展函数等。
1、Kotlin中init代码块和构造方法以及伴生对象中代码的调用时机及执行顺序
- 在kotlin中代码的执行顺序:伴生对象 —— init代码块 ——次构造函数
- 可以在init代码块中使用类声明的属性
-
Kotlin中的init代码块就相当于Java中的普通代码块,在创建对象的时候代码块会先执行。注意是每次创建都会执行一遍
2、kotlin的主构造函数和次构造函数:
- 在Kotlin中一个类可以有一个主构造函数和一个或多个次构造函数;
主构造函数是类头的一部分:它跟在类名后。如果主构造函数没有任何注解或可见性修饰符,constructor关键字可省略,否则是必须的 - 类定义了主构造器,次构造器必须直接或间接调用主构造器;
class Person (name: String){
init {
println(name)
}
// 次构造器(通过this直接调用主构造器)
constructor(age: Int): this("HAHA"){
println(age)
}
}
- 子类的构造函数必须调用父类的构造函数。所以这也是 Kotlin 中继承类有括号的原因。
- 当父类主构造函数带参数时,由于子类必须实现父类主构造函数,所以可以在子类的主构造函数中加入父类构造函数需要的参数。
class Student(sno: String, grade: Int, name: String, age: Int) : People(name, age) {
}
- Kotlin 规定所有次构造函数必须调用主构造函数
- 如果一个类没有主构造函数,次级构造函数就不必显示调用主构造函数
推荐使用 Kotlin 关键字 Reified - 简书 (jianshu.com)
Kotlin 中,reified 是一个关键字,通常用于泛型函数的类型擦除问题。在 Java 中,由于类型擦除的影响,泛型类型的实际参数在运行时是不可获知的。而 Kotlin 中的 reified 关键字允许我们在运行时获取泛型类型的信息。reified 主要用于在内联函数(inline functions)中获取泛型的实际类型参数。内联函数会在编译时将函数的字节码插入到调用处,因此可以在运行时访问泛型信息。这对于需要在函数体内获取泛型类型的场景非常有用。需要注意的是,reified 关键字只能在内联函数中使用。当函数被内联时,编译器可以在调用处直接插入相应的代码,以实现对泛型类型的运行时访问。
重写的要求:
- 方法名相同,参数类型相同
- 子类返回类型小于等于父类方法返回类型
- 子类抛出异常小于等于父类方法抛出异常
- 子类访问权限大于等于父类方法访问权限:子类不能将父类中的方法、属性的作用域变小,因为可能会造成方法的“失传”
AtomicReference 是 Java 并发包中的一个类,用于提供对单个引用变量的原子操作。它保证了在多线程环境下对引用的读取和写入是线程安全的。主要方法包括:
get(): 获取当前引用的值。
set(T newValue): 设置引用的值为 newValue。
compareAndSet(T expect, T update): 如果当前引用的值是 expect,则将其更新为 update,返回更新是否成功。
其他方法如 getAndSet(), lazySet() 等。
AtomicReference<Object>fun <S : MavericksState, A> change(prop1: KProperty1<S, A>, value: Any)
prop1 的类型是 KProperty1<S, A>,表示这是一个 Kotlin 的属性引用类型,它指向了一个 S 类型对象的 A 类型属性。data class: https://juejin.cn/post/7156233100199100447