目录
拓展
拓展函数
a)拓展函数就是动态的给类添加方法.
Java 中是不支持对系统的类进行拓展,而 Kotlin 支持.
例如,统计一个 List<Int> 中,元素大于 0 的元素个数. 如果使用 Java,我们可能会创建一个 ListUtils 然后在里面编写这样一个方法.
b)Kotlin 就可以拓展函数实现:创建一个 List.kt,职责就是对 List 进行拓展(创建新文件可使得拓展函数拥有全局访问域,不定义新文件也是可以的,但是郭霖大佬是建议定义新文件).
fun List<Int>.gtZeroCount(): Int {
var count = 0
//this 就是当前作用的对象
this.forEach {
if(it > 0) count++
}
return count
}
Kotlin 可以直接使用拓展方法:
var count = listOf(-7, 4, 6).gtZeroCount()
Java 则需要调用方法来实现:
ListUtils.gtZeroCount(list);
拓展属性
拓展属性就是可以对类的属性进行动态拓展.
创建一个 String.kt 文件,中加入以下代码,相当于给 String 添加了一个值为 1 的 int
val String.value : Int get() = 1
Ps:get() 是固定语法.
Kotlin 访问如下:
val value = "".value
println(value) //打印 1
运算符重载
operator
Kotlin 运算符会在编译的时候替换成方法调用. 比如 加法 会替换成 plus 方法.
Kotlin 中,对象也可以使用运算符操作,但是需要使用 operator 关键字来标记一个方法是重构方法.
a)例如创建一个 Coin 类,通过 operator 标记 plus 是一个重载方法
class Coin(val value: Int) {
operator fun plus(coin: Coin): Coin {
val sum = coin.value + this.value
return Coin(sum)
}
}
fun main() {
val coin = Coin(10) + Coin(20)
println(coin.value) //输出 30
}
当两个 Coin 相加,编译时就会替换成我们重载的 plus 方法.
b)如果想让 Coin 类可以和 int 直接相加,可如下重载:
class Coin(val value: Int) {
operator fun plus(value: Int): Coin {
val sum = this.value + value
return Coin(sum)
}
}
fun main() {
val coin = Coin(10) + 20
println(coin.value) //输出 30
}
c)可重载的不仅有加法运算,还有支持如下表:
语法糖表达式 | 实际调用函数 |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a % b | a.rem(b) |
a++ | a.inc() |
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
a == b | a.equals(b) |
a > b | 自定义 |
a < b | 自定义 |
a >= b | a.compareTo(b) |
a <= b |
a…b | a.rangeTo(b) |
a[b] | a.get(b) |
a[b] = c | a.set(b, c) |
a in b | b.contains(a) |
d)Kotlin 代码如下
fun main() {
val coin = Coin(10) + 20
}
对应的 Java 代码如下:
public final class SolutionKt {
public static final void main() {
Coin coin = (new Coin(10)).plus(20);
}
}
高阶函数
Kotlin 中的高阶函数:一个函数的参数是另一个一个函数,或者返回值是另一个函数.
一个函数的参数是另一个函数,这个参数该怎么定义呢?如下:
//() 内就是参数列表,Unit 表示这个函数没有返回值
() -> Unit
//再例如
(String, Int) -> Int
将这种类型的参数放到方法上,这个方法就是高阶函数.
例如,定义一个高阶函数,其中有一个参数是另一个函数(参数是两个 Int),两个 Int 具体的操作由调用者来决定,如下:
a)高阶函数的定义
fun test(num1: Int, num2: Int, func: (Int, Int) -> Int) = func(num1, num2)
b)使用
fun main() {
val res1 = test(1, 2) { n1, n2 -> n1 + n2 }
val res2 = test(2, 3) {n1, n2 -> n1 - n2}
println(res1) //输出 3
println(res2) //输出 -1
}
通过高阶函数,模拟实现标准函数 apply
apply 内部可以对调用者本身进行操作,也就是说再 lambda 中可以拿到调用者的上下文. 因此这里可以使用拓展函数来完成,如下
a)定义高阶函数
fun StringBuilder.myApply(sb: StringBuilder.() -> Unit): StringBuilder {
sb()
return this
}
Ps:类名. 再加上 () ,表示可以在该 lambda 中定义了该对象,可以直接操作.
b)调用如下
val sb = StringBuilder().myApply {
append("aaa")
append("bbb")
append("ccc")
}
println(sb) //输出 aaabbbccc
c)底层原理:Lambda 会生成一个内部类,且包含了此类的静态变量 instance,类内部还会生成 invoke 方法.
执行高阶函数时,Lambda 参数会被编译成上述 instance 对象,高阶函数内部会去调用此对象的 invoke 方法,invoke 内部就是 Lambda 的逻辑,因此 Lambda 就被执行了.
内联函数
inline
inline 是一个关键字,可以用来修饰函数或类.
- 修饰函数:用来减少函数调用的开销. 编译时期,编译器就会把调用 inline 函数的地方替换成函数的方法体,而不是通过常规的函数调用进行. 这样可以减少因函数调用产生的压栈和出栈的开销,提高性能.
- 修饰类:被修饰的类也叫 “内联类”,主要作用就是节省类创建对象的开销. 当类的实例只有一个属性,并且整个类主要提供获取该属性的方法,使用内联类可以优化新能.
缺陷:inline 的过度使用可能会导致代码膨胀,以内联函数的代码会被直接复制过来.
例如,Lambda 会生成内部类,会造成一定的内存和性能开销,使用 Kotlin 就可以将 Lamdba 表达式的弊端去除.
a)回顾上一个栗子中,模拟实现 apply,调用如下:
fun main() {
val sb = StringBuilder().myApply {
append("aaa")
append("bbb")
append("ccc")
}
println(sb) //输出 aaabbbccc
}
反编译 Java 的结果如下:
public final class SolutionKt {
public static final void main() {
StringBuilder sb = ListKt.myApply(new StringBuilder(), (Function1)null.INSTANCE);
System.out.println(sb);
}
}
b)如果使用 inline 修饰 myApply 方法
inline fun StringBuilder.myApply(sb: StringBuilder.() -> Unit): StringBuilder {
sb()
return this
}
反编译 Java 结果如下:
public final class SolutionKt {
public static final void main() {
StringBuilder $this$myApply$iv = new StringBuilder();
int $i$f$myApply = false;
int var4 = false;
$this$myApply$iv.append("aaa");
$this$myApply$iv.append("bbb");
$this$myApply$iv.append("ccc");
System.out.println($this$myApply$iv);
}
}
noinline
如果一个函数的参数中有多个函数,此时加上 inline 会使全部参数参与内联,如果不想某些函数参数内联,就可以在不需要参加内联的参数前加上 noinline 关键字.
例如 test 函数的参数是两个函数(func1 和 func2),此时我不想让 func2 参与 内联,如下代码:
inline fun test(func1: () -> Unit, noinline func2: () -> Unit) {
func1()
func2()
}
crossinline
内联还存在一个问题:当内联函数的结束并非是调用者来控制,就会报错,如下
上述代码中,task 的结束,并非由调用者控制,而是由 Runnable 的 run 方法,因此导致冲突.
此时有两种解决办法:
- 不使用内联,去掉 inline
- 使用 crossinine 关键字修饰该参数.
实际上,这里如果通过 alt + enter,也可以看到提示给你的解决方式:
泛型
泛型类
Kotlin 中的泛型 和 Java 中的泛型感觉差不太多.
如下代码:
class ApiResp<T> {
private var data: T? = null
fun setData(data: T) {
this.data = data
}
}
fun main() {
val result = ApiResp<Int>()
result.setData(1)
}
泛型方法
如下代码:
fun <T> result(value: T): T {
return value
}
fun main() {
val result = result("aaa")
}
限定泛型类型
若不指定类型,T 会被类型擦除为 Any?, ? 表示可以为空,Any 相当于 Java 中的 Object
如果我们需要对泛型类型进行限制,可以类似 Java 实现泛型上界,如下:
fun <T: Number> result(value: T): T {
return value
}
fun main() {
val result1 = result("aaa") //编译错误
val result2 = result(1) //成功
val result3 = result(1L) //成功
val result4 = result(1.0) //成功
}
Ps:Kotlin 中 Number 是一个抽象类,是所有数字类型的超类,包括 Byte、Short、Int、Long、Double 等。它提供了一些通用的方法和属性,用于处理数字类型的通用操作,比如转换、比较等。由于 Number 是一个抽象类,你不能直接实例化它,但可以使用它的子类来表示具体的数字类型。
模拟实现 apply 标准函数(泛型版)
之前编写了 StringBuilder 的拓展函数 myApply,但是缺只是针对于 StringBuilder 的拓展. 刚刚我们讲到了泛型,这下就可以实现一个几乎和 apply 一样的标准函数了.
fun <T> T.myApply(func: T.() -> Unit): T {
func()
return this
}
fun main() {
val sb = StringBuilder()
sb.myApply {
append("aaa")
append("bbb")
append("ccc")
}
}
泛型高级特性
回顾 Java 中的协变和逆变
Java 中的协变和逆变分别是通过 extends 和 super 实现的.
a)先来看一个栗子:
定义三个类,其中 AAA 是另外两个类的父类
class AAA { }
class BBB extends AAA {}
class CCC extends AAA {}
定义一个泛型类,主要用来处理上述三种类型:
class Data<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
提供一个 print 方法,参数是 Data<AAA> 类型
public static void print(Data<AAA> obj) {
System.out.println(obj.getData());
}
public static void main(String[] args) {
Data<AAA> a = new Data<>();
print(a);
Data<BBB> b = new Data<>();
print(b); //编译错误
Data<CCC> c = new Data<>();
print(c); //编译错误
}
b)分析:print(b) 和 print(c) 报错的原因主要是 Java 不认识泛型的子类.
上述问题就可以通过 extends 协变来解决,如下:
public static void print(Data<? extends AAA> obj) {
System.out.println(obj.getData());
}
c)逆变 super 的使用和 extends 正好相反. 上述使用 extends ,使得 obj 可以传入泛型的子类,而 super 则是可以传父类.
public static void print(Data<? super AAA> obj) {
System.out.println(obj.getData());
}
public static void main(String[] args) {
Data<AAA> a = new Data<>();
print(a);
Data<BBB> b = new Data<>();
print(b); //编译错误
Data<CCC> c = new Data<>();
print(c); //编译错误
}
Kotlin 的协变和逆变
例如如下场景,和 Java 一样
open class AAA
class BBB: AAA()
class CCC: AAA()
data class Data<T>(
val data: T?
)
fun test(data: Data<AAA>) {
}
fun main() {
val a = Data(AAA())
val b = Data(BBB())
val c = Data(CCC())
test(a)
test(b) //编译错误
test(c) //编译错误
}
a)协变
Kotlin 中提供 out 关键字来标记协变类型参数.
fun test(data: Data<out AAA>) {
}
fun main() {
val a = Data(AAA())
val b = Data(BBB())
val c = Data(CCC())
test(a)
test(b)
test(c)
}
b)逆变
Kotlin 中提供的 in 关键字用来标记逆变类型参数
fun test(data: Data<in AAA>) {
}
fun main() {
val a = Data(AAA())
val b = Data(BBB())
val c = Data(CCC())
test(a)
test(b) //编译错误
test(c) //编译错误
}
委托
委托是一种设计模式,本质是 操作对象自己不会去处理某个逻辑,而是把工作委托给另外一个辅助对象去处理.
Java 没有在语法层面对委托进行支持,而 Kotlin 是支持的.
类委托
类委托就是将一个类的具体实现委托给另一个类去完成
例如我们想通过委托模式自己实现一个 Set,代码如下:
class MySet<T> (val helpSet: HashSet<T>): Set<T> {
override val size: Int
get() = helpSet.size
override fun isEmpty(): Boolean {
return helpSet.isEmpty()
}
override fun iterator(): Iterator<T> {
return helpSet.iterator()
}
override fun containsAll(elements: Collection<T>): Boolean {
return helpSet.containsAll(elements)
}
override fun contains(element: T): Boolean {
return helpSet.contains(element)
}
}
可能有人会说了,这和直接调用 HashSet 没什么区别呀,简直就是脱了裤子放屁!
委托模式允许我们加入独有的方法,使得 MySet 成为一个全新的数据结构,或者是重写原有的接口的实现逻辑,这是委托存在的意义.
并且,以上写法是有点问题的,如果要委托的接口种有很多方法,就需要把每个方法都实现一遍?Java 中就没有很好的解决办法,但是 Kotlin 可以通过 by 关键字来解决
例如,假设我只想要 set 中的 contains 方法,如下代码
class MySet<T> (val helpSet: HashSet<T>): Set<T> by helpSet {
override fun contains(element: T): Boolean {
println("在原有接口基础上更改了一些逻辑")
return helpSet.contains(element)
}
//定义自己的方法
fun newFunc() {
println("这是一个新的方法")
}
}
属性委托
属性委托就是将要给属性的数据实现交给另一个类去完成.
a)例如,创建一个 Test 类,声明需要委托的属性.
class Test {
var value by TestHelp()
}
b)创建 TestHelp 类,用来委托属性,并且必须要重载 get 和 set 方法(必须使用重载运算符 operator),如下:
class TestHelp {
var valueHelp: Any? = null
//必须要实现 get 和 set 方法
operator fun getValue(test: Test, property: KProperty<*>): Any? {
return valueHelp
}
operator fun setValue(test: Test, property: KProperty<*>, value: Any?) {
valueHelp = value
}
}
重载方法的参数说明(以 set 方法举例):
- 第一个参数:表示在为哪个类做委托.
- 第二个参数:KProperty<*> 是 Kotlin 中的一个属性操作类,用于获取各种属性相关的值,当前场景用不到,但必须声明(<*> 类似 Java 中 <?>).
- 第三个参数:value 就是被委托的属性.
原理:当我们给 Test 的 value 赋值的时候,就会调用到 TestHelp 中的 setValue 方法. 获取时就会调用 getValue 方法.
lazy 懒加载
Kotlin 中,lazy 可以实现懒加载. 这意味着,它允许我们在实际需要使用某个对象的时候才进行初始化,而不是在对象创建时就进行初始化.
注意:lazy 只能修常量 val. 是线程安全的.
使用方式:接收一个 Lambda 表达式作为参数,并返回一个 Lazy<T> 的实例函数.
例如委托属性,如下
val value by lazy {
//初始化操作
}
只有真正调用到 value 的时候才会执行 lambda 表达式,对 value 进行初始化.
infix 中缀函数
Kotlin 中,infix 也成为中缀函数,主要用于调整变成语言函数调用的语法规则,提高代码的可读性和简洁性.
例如,我们我有一个名为 to 的中缀函数,那么可以使用 A to B 这样的语法来调用它,底层实际上会被 Kotlin 编译器转化成 A.to(B).
举一个有趣的栗子,例如写一个模拟向量加法:
data class Vector2D(val x: Double, val y: Double) {
//定义一个名为 plus 的中缀函数来模拟向量加法
infix fun plus(other: Vector2D): Vector2D {
return Vector2D(this.x + other.x, this.y + other.y)
}
}
fun main() {
//创建两个向量
val v1 = Vector2D(1.0, 2.0)
val v2 = Vector2D(3.0, 4.0)
//使用中缀函数进行向量加法
val sum = v1 plus v2
println("x: ${sum.x}, y: ${sum.y}")
}
Ps:
- 中缀函数不能是顶层函数,它必须是某个类的成员函数或扩展函数。
- 中缀函数必须接收且只能接收一个参数,尽管这个参数的类型没有限制。
to 和 Pair 的使用
在 Kotlin 中,to 是中缀函数,用来创建 Pair 对象. Pair 表示两个元素对的数据结构,通常用来表示一个键值对.
a)使用 to 创建 Pair
val pair = "key" to "value"
println(pair.first) //输出 key
println(pair.second) //输出 value
其中 pair 是一个 Pair<String, String> 对象,第一个元素是 "key",第二个元素是 "value".
b)结构声明中也可以使用 Pair
val (key, value) = "key" to "value"
println(key) //输出 key
println(value) //输出 value
c)map 集合中使用 Pair
val map = mapOf("k1" to "v1", "k2" to "v2")
println(map["k1"]) //输出 v1