首页 > 编程语言 >Kotlin,简直是 Java 的 Pro Max!(笔记3 进阶篇)

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

时间:2024-03-20 23:29:53浏览次数:25  
标签:Java 函数 val Kotlin Pro value fun Data

目录

拓展

拓展函数

拓展属性

运算符重载

operator

高阶函数

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

内联函数

inline

noinline

crossinline

泛型

泛型类

泛型方法

限定泛型类型

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

泛型高级特性

回顾 Java 中的协变和逆变

Kotlin 的协变和逆变

委托

类委托

属性委托

lazy 懒加载

infix 中缀函数

to  和 Pair 的使用


拓展


拓展函数

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

标签:Java,函数,val,Kotlin,Pro,value,fun,Data
From: https://blog.csdn.net/CYK_byte/article/details/136872359

相关文章

  • 9.JavaWeb& javaScript基础
    目录导语:一、JavaWeb概述二、JavaScript基础概念:功能:1.基本语法(1)与html结合方式(2)注释(3)数据类型(4)变量(5)运算符(6)流程控制语句:(7)JS特殊语法:案例:99乘法表2.基本对象(1)Function:函数(方法)对象(2)Array:数组对象(3)Boolean(4)Date:日期对象(5)Math:数学对象(6)Number(7)String(8......
  • 您真的了解Java中的锁吗?这7种不同维度下的锁知道吗?
    写在开头在上几篇博文中,我们聊到过volatile关键字,用它修饰变量可以保证可见性与有序性,但它并不是锁,在使用时并不会阻塞线程,且不保证原子性,属于一种轻量级、高效的同步方式,因此,如果我们的使用场景仅需要保持可见性或者有序性,那可选择volatile,但如果必须保证原子性的话,volatile就不......
  • Modbus转Profinet网关解决Modbus轮询速度慢的问题
    当面临Modbus轮询速度慢的情况时,可以通过使用Modbus转Profinet网关(XD-MDPN100)来解决这一问题。Modbus转Profinet网关可以帮助提高数据传输的效率和速度,使传输更加快捷和稳定。Modbus转Profinet网关(XD-MDPN100)通过将Modbus协议转换为Profinet协议,可以实现不同设备之间的无缝通信,避......
  • 复习java的第一天3.18的文章
    大家好,我准备在这里记录我每天的学习(复习)java的成果,以及计划和规划,为的就是希望能找几个月后能找一份工作,并且我希望自己能坚持下去,养成一个良好的习惯,让自己不再那么迷茫,与其内耗不如做点有意义有方向的事情.之前我一直想不明白自己到底想要干什么,因为网上看着大家都说......
  • JavaScript 中的函数式编程
    一、问题背景例子下面的代码是一个函数式编程的例子我们先观察一下里面有哪些特殊的语法点,再一一讲解constprogrammerOutput=[{name:'UncleBobby',linesOfCode:500,},{name:'SuzieQ',linesOfCode:1500,},{name:'JimmyGosl......
  • 复习Java的第三天3.20
    今天的复习学习内容就这么多虽然不多但是贵在坚持如果能把每一天的事情都做好就很满足了最重要的是享受过程而不是一天一天的重复学习而不感兴趣感受不到新知识所带来的快乐以下是今天复习内容:1.运用关系运算符完成数据的比较2.复习了逻辑运算符的运算以及特点(&|!^......
  • Java如何修改框架源码(以ZooKeeper框架为例)
    1、缘由:在Zookeeper框架内部源码中,org.apache.zookeeper.ClientCnxn.SendThread#logStartConnect方法会打印客户端与服务器端的连接状态,如果在网络出现波动时会出现连接异常并在日志中打印出INFO级别信息【java.lang.IllegalArgumentException】,而这个关键词会触发运维告警。2......
  • 基于Springboot的在线装修管理系统(有报告)。Javaee项目,springboot项目。
    演示视频:基于Springboot的在线装修管理系统(有报告)。Javaee项目,springboot项目。项目介绍:采用M(model)V(view)C(controller)三层体系结构,通过Spring+SpringBoot+Mybatis+Vue+Maven+Layui+Elementui来实现。MySQL数据库作为系统数据储存平台,实现了基于B/S结构的Web系统......
  • 史上最全Java核心面试题(带全部答案)2024年最新版
    今天要谈的主题是关于求职,求职是在每个技术人员的生涯中都要经历多次。对于我们大部分人而言,在进入自己心仪的公司之前少不了准备工作,有一份全面细致面试题将帮助我们减少许多麻烦。在跳槽季来临之前,特地做这个系列的文章,一方面帮助自己巩固下基础,另一方面也希望帮助想要换工......
  • Java递归拷贝文件夹
    importjava.io.*;importjava.util.Scanner;publicclassDemo{publicstaticvoidmain(String[]args){FilesrcDirFile=getDirFile("输入源文件夹路径");FiledestDirFile=getDirFile("输入目标路径");if(srcDirFile.e......