Kotlin,简直是 Java 的 Pro Max!(笔记3 进阶篇)

目录

拓展

拓展函数

拓展属性

运算符重载

operator

高阶函数

通过高阶函数,模拟实现标准函数 apply

内联函数

inline

noinline

crossinline

泛型

泛型类

泛型方法

限定泛型类型

模拟实现 apply 标准函数(泛型版)

泛型高级特性

回顾 Java 中的协变和逆变

Kotlin 的协变和逆变

委托

类委托

属性委托

lazy 懒加载

infix 中缀函数

to  和 Pair 的使用

JSON 序列化和反序列化


拓展


拓展函数

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 + ba.plus(b)
a - ba.minus(b)
a * ba.times(b)
a / ba.div(b)
a % ba.rem(b)
a++

a.inc()

+aa.unaryPlus()
-aa.unaryMinus()
!aa.not()
a == ba.equals(b)
a > b自定义
a < b自定义
a >= ba.compareTo(b)
a <= b
a…ba.rangeTo(b)
a[b]a.get(b)
a[b] = ca.set(b, c)
a in bb.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:

  1. 中缀函数不能是顶层函数,它必须是某个类的成员函数或扩展函数。
  2. 中缀函数必须接收且只能接收一个参数,尽管这个参数的类型没有限制。

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

JSON 序列化和反序列化

a)需要添加以下依赖

        <dependency>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-module-kotlin</artifactId>
        </dependency>

b)在需要的地方注入即可

c)复杂对象的序列化和反序列化如下 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈亦康

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值