写点什么

From Java To Kotlin:空安全、扩展、函数、Lambda 很详细,这次终于懂了

作者:Seachal
  • 2023-05-22
    北京
  • 本文字数:15082 字

    阅读完需:约 49 分钟

From Java To Kotlin:空安全、扩展、函数、Lambda很详细,这次终于懂了

From Java To Kotlin, 空安全、扩展、函数、Lambda

概述(Summarize)



Kotlin 是什么?

Kotlin 出自于捷克一家软件研发公司 JetBrains ,这家公司开发出很多优秀的 IDE,如 IntelliJ IDEA、DataGrip 等都是它的杰作,包括 Google 官方的 Android IDE -- Android Studio ,也是 IntelliJ IDEA 的插件版。


Kotlin 源于 JetBrains 的圣彼得堡团队,名称取自圣彼得堡附近的一个小岛 ( Kotlin Island ) ,和 Java 一样用岛屿命名,JetBrains 在 2010 年首次推出 Kotlin 编程语言,并在次年将之开源。


  • Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言,被称之为 Android 世界的 Swift。

  • Kotlin 可以编译成 Java 字节码。也可以编译成 JavaScript,方便在没有 JVM 的设备上运行。

  • 在 Google I/O 2017 中,Google 宣布 Kotlin 成为 Android 官方开发语言,替代 Java 语言




Kotlin 代码会被编译成 Java 字节码,所以和 Java 兼容


<img src="http://seachal-blog-picture-host.oss-cn-beijing.aliyuncs.com/MWeb/2023/03/16/16789367124770.jpg" class="m-0 h-110 rounded shadow" />



可以做什么?

  • Android

  • Server-side

  • Multiplatform Mobile

  • Kotlin Multiplatform Mobile is in Beta!

  • Multiplatform libraries

  • Create a multiPlatform library for JVM, JS, and Native platforms.

  • <img src="http://seachal-blog-picture-host.oss-cn-beijing.aliyuncs.com/MWeb/2023/03/13/16786878005647.jpg" class="m-0 h-40 rounded shadow" />可以做很多方向的开发!



Android 官方开发语言从 Java 变为 Kotlin,Java 有哪些问题?

  • 空引用(Null references):Java 中的 null 值是经常导致程序运行出错的原因之一,因为 Java 不支持空安全。

  • 更少的函数式编程特性:Java 语言在函数式编程方面的支持相对较弱,虽然 Java 8 引入了 Lambda 表达式和 Stream API,但是 Kotlin 语言在这方面的支持更加全面和友好。

  • 不够灵活,缺乏扩展能力:我们不能给 第三方 SDK 中的 classes 或者 interfaces 增加新的方法。。

  • 语法繁琐,不够简洁:Java 语言比 Kotlin 语言更为冗长,需要写更多的代码来完成相同的任务,这可能会降低开发效率。

Kotlin 的优点

Modern, concise and safe programming language


  • 简约:使用一行代码创建一个包含 getterssettersequals()hashCode()toString() 以及 copy() 的 POJO:

  • 安全:彻底告别那些烦人的 NullPointerException

  • 互操作性: Kotlin 可以与 Java 混合编程,Kotlin 和 Java 可以相互调用,目标是 100% 兼容。



Kotlin 特性(Features)

  • 空安全(Null safety)

  • 类型推断(Type inference)

  • 数据类 (Data classes)

  • 扩展函数 (Extension functions)

  • 智能转换(Smart casts)

  • 字符串模板(String templates)

  • 单例(Singletons)

  • 函数类型 (Function Type )

  • Lambda 表达式

  • 高阶函数(Primary constructors)

  • 函数字面量和内联函数(Function literals & inline functions)

  • 类委托(Class delegation)

  • 等等......



基本语法 (Basic Syntax )



变量(Variables)

在 Java/C 当中,如果我们要声明变量,我们必须要声明它的类型,后面跟着变量的名称和对应的值,然后以分号结尾。就像这样:


Integer price = 100;
复制代码


而 Kotlin 则不一样,我们要使用val或者是var这样的关键字作为开头,后面跟“变量名称”,接着是“变量类型”和“赋值语句”,最后是分号结尾。就像这样:


/*关键字     变量类型 ↓          ↓           */var price: Int = 100;   /*     ↑            ↑   变量名        变量值   */
复制代码


在 Kotlin 里面,代码末尾的分号省略不写,就像这样:


var price = 100 // 默认推导类型为: Int
复制代码


另外,由于 Kotlin 支持类型推导,大部分情况下,我们的变量类型可以省略不写,就像这样:



var price = 100 // 默认推导类型为: Int
复制代码


var 声明的变量,我们叫做可变变量,它对应 Java 里的普通变量。


val 声明的变量,我们叫做只读变量,它相当于 Java 里面的 final 变量。


var price = 100price = 101
val num = 1num = 2 // 编译器报错
复制代码


var, val 反编译成 Java :





我们已经知道了 val 属性只有 getter,只能保证引用不变,不能保证内容不变。例如,下面的代码:


class PersonZ {    var name = "zhang"    var age = 30    val nickname: String        get() {            return if (age > 30) "laozhang" else "xiaozhang"        }    fun grow() {        age += 1    }
复制代码


属性 nickname 的值并非不可变,当调用 grow() 方法时,它的值会从 "xiaozhang" 变为 "laozhang",


不过因为没有 setter,所以无法直接给 nickname 赋值


编译时常量


const 只能修饰没有自定义 getter 的 val 属性,而且它的值必须在编译时确定


val time = System.currentTimeMillis()// 这种会报错const val constTime = System.currentTimeMillis()
复制代码



基本数据类型( Basic Data Type )

Kotlin 的基本数值类型包括 Byte、Short、Int、Long、Float、Double 等。



在 Kotlin 语言体系当中,是没有原始类型这个概念的。这也就意味着,在 Kotlin 里,一切都是对象。



空安全(Null Safety )

既然 Kotlin 中的一切都是对象,那么对象就有可能为空。如果我写这样的代码:


val i: Double = null // 编译器报错
复制代码


以上的代码并不能通过 Kotlin 编译。<img src="http://seachal-blog-picture-host.oss-cn-beijing.aliyuncs.com/MWeb/2023/03/13/16786156409163.jpg" class="m-0 h-30 rounded shadow" />


这是因为 Kotlin 强制要求开发者在定义变量的时候,指定这个变量是否可能为 null


对于可能为 null 的变量,我们需要在声明的时候,在变量类型后面加一个问号“?”:


val i: Double = null // 编译器报错val j: Double? = null // 编译通过
复制代码




并且由于 Kotlin 对可能为空的变量类型做了强制区分,这就意味着,“可能为空的变量”无法直接赋值给“不可为空的变量”,反过来 “不可为空的变量” 可以赋值给“可能为空的变量” 。



var i: Double = 1.0var j: Double? = null
i = j // 编译器报错j = i // 编译通过
复制代码


这么设计的原因是,从集合逻辑上:可能为空 包含 不可为空


而如果我们实在有这样的需求,也不难实现,只要做个判断即可:


var i: Double = 1.0val j: Double? = null
if (j != null) { i = j // 编译通过}
复制代码












函数声明( Define Function )

在 Kotlin 当中,函数的声明与 Java 不太一样。Java:


   public String helloFunction(@NotNull String name) {      return "Hello " + name + " !";   }
复制代码


Kotlin :


/*关键字    函数名          参数类型   返回值类型 ↓        ↓                ↓       ↓      */fun helloFunction(name: String): String {    return "Hello $name !"}/*   ↑   花括号内为:函数体*/
复制代码


  • 使用了 fun 关键字来定义函数;

  • 返回值类型,紧跟在参数的后面,这点和 Java 不一样。




如果函数体中只有一行代码,可以简写


  • return 可以省略

  • { } 花括号可以省略

  • 直接用 = 连接,变成一种类似 变量赋值的 函数形式


fun helloFunton(name:String):String = "Hello $name !"
复制代码


我们称之为单表达式函数


由于 Kotlin 支持类型推导,返回值类型可以省略:


fun helloFunton(name:String):= "Hello $name !"
复制代码


这样看起来就更简洁了。



让函数更好的调用( Making functions easier to call )

命名参数/具名参数 (Named arguments)

以前面的函数为例子,我们调用它:


helloFunction("Kotlin")
复制代码


和 Java 一样。


不过,Kotlin 提供了一些新的特性,如命名函数参数举个例子,现在有一个函数:



fun createUser( name: String, age: Int, gender: Int, friendCount: Int, feedCount: Int, likeCount: Long, commentCount: Int) { //..}
复制代码




如果像 Java 那样调用:


createUser("Tom", 30, 1, 78, 2093, 10937, 3285)
复制代码


就要严格按照参数顺序传参:


  • 参数顺序调换,参数就传错了,不好维护

  • 当参数是一堆数字,很难知道数字对应的形参,可读性不高


Kotlin 参数调用:


createUser(    name = "Tom",    age = 30,    gender = 1,    friendCount = 78,    feedCount = 2093,    likeCount = 10937,    commentCount = 3285)
复制代码


我们把函数的形参加了进来,形参和实参用 = 连接,建立了两者的对应关系。这样可读性更强。


如果想修改某个参数例如feedCount也可以很方便的定位到参数。 这样易维护



参数默认值(Default arguments)

fun createUser(    name: String,    age: Int,    gender: Int = 1,    friendCount: Int = 0,    feedCount: Int = 0,    likeCount: Long = 0L,    commentCount: Int = 0) {    //..}
复制代码


gender、likeCount 等参数被赋予了默认值,当我们调用时,有些有默认值的参数就可以不传参,Kotlin 编译器自动帮我们填上默认值。



createUser( name = "Tom", age = 30, friendCount = 50)
复制代码


在 Java 当中要实现类似的逻辑,我们就必须手动定义新的“3 个参数的 createUser 函数”,或者是使用 Builder 设计模式。



Classes and Objects



类 (Class)

Java


public class Person {    private String name;    private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
// 属性 name 没有 setter public String getName() { return name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }}
复制代码

Class

Kotlin


class Person(val name: String, var age: Int)
复制代码


Kotlin 定义类,同样使用 class 关键字。


Kotlin 定义的类在默认情况下是 public 的。


编译器会帮我们生成“构造函数”,


对于类当中的属性,Kotlin 编译器也会根据实际情况,自动生成 getter 和 setter。


和 Java 相比 Kotlin 定义一个类足够简洁。



抽象类与继承

抽象类 (Abstract Class)

abstract class Person(val name: String) {    abstract fun walk()    // 省略}
复制代码

继承(Extend)

//                      Java 的继承//                           ↓public class MainActivity extends Activity {    @Override    void onCreate(){ ... }}
复制代码


//              Kotlin 的继承//                 ↓class MainActivity : AppCompatActivity() {    override fun onCreate() { ... }}
复制代码



接口和实现 (Interface and implements)

Kotlin 当中的接口(interface),和 Java 也是大同小异的,它们都是通过 interface 这个关键字来定义的。



interface Behavior { fun walk()}
class Person(val name: String): Behavior { override fun walk() { // walk } // ...}
复制代码


可以看到在以上的代码中,我们定义了一个新的接口 Behavior,它里面有一个需要被实现的方法 walk,然后我们在 Person 类当中实现了这个接口。


Kotlin 的继承和接口实现语法基本上是一样的。




Kotlin 的接口,跟 Java 最大的差异就在于,接口的方法可以有默认实现,同时,它也可以有属性。


interface Behavior {    // 接口内的可以有属性    val canWalk: Boolean    // 接口方法的默认实现    fun walk() {        if (canWalk) {            // do something        }    }}class Person(val name: String): Behavior {    // 重写接口的属性    override val canWalk: Boolean        get() = true}
复制代码


我们在接口方法当中,为 walk() 方法提供了默认实现,如果 canWalk 为 true,才执行 walk 内部的具体行为。


Kotlin 当中的接口,被设计得更加强大了。


在 Java 1.8 版本当中,Java 接口也引入了类似的特性。



嵌套类和内部类( Nested and Inner Classes )

Java 当中,最常见的嵌套类分为两种:非静态内部类静态内部类。Kotlin 当中也有一样的概念。


class A {    class B {    }}
复制代码


以上代码中,B 类,就是 A 类里面的嵌套类。


注意: 无法在 B 类当中访问 A 类的属性和成员方法。


因为 Kotlin 默认嵌套类(B 类)是一个静态内部类


Kotlin 嵌套类反编译成 Java 代码:


<img src="http://seachal-blog-picture-host.oss-cn-beijing.aliyuncs.com/MWeb/2023/03/16/16788807777726.jpg" class="m-5 rounded shadow" />




 public class JavaOuterInnerClass2 {   // 内部类    public  class InnerClass {    }    // 静态内部类    public  static  final   class  StaticInnerClass{    }}
复制代码


通过 javac 命令 编译成 class 文件后:


  • InnerClass

  • StaticInnerClass


通过.class 可以发现,


$InnerClass 持有外部类的引用。


$StaticInnerClass 不持有外部类的引用。


Java 当中的嵌套类,默认情况下,没有 static 关键字 时,它就是一个内部类,这样的内部类是会持有外部类的引用的。所以,这样的设计在 Java 当中会非常容易出现内存泄漏! 而我们之所以会犯这样的错误,往往只是因为忘记加static关键字。


Kotlin 则恰好相反,在默认情况下,嵌套类变成了静态内部类,而这种情况下的嵌套类是不会持有外部类引用的。只有当我们真正需要访问外部类成员的时候,我们才会加上 inner 关键字。这样一来,默认情况下,开发者是不会犯错的,只有手动加上 inner 关键字之后,才可能会出现内存泄漏,而当我们加上 inner 之后,其实往往也就能够意识到内存泄漏的风险了。



数据类(Data Class )

Koltin 数据类 ,就是用于存放数据的类,等价于 POJO (Plain Ordinary Java Object)。要定义一个数据类,我们只需要在普通的类前面加上一个关键字 data,就可以把它变成一个"数据类"。


    // 数据类当中,最少要有一个属性data class Person(val name: String, val age: Int)
复制代码


编译器会为数据类自动生成一些 POJO 常用的方法


  • getter()

  • setter()

  • equals();

  • hashCode();

  • toString();

  • componentN() 函数;

  • copy()。




Koltin 数据类反编译成 Java 代码:


<img src="http://seachal-blog-picture-host.oss-cn-beijing.aliyuncs.com/MWeb/2023/03/15/16788793363013.jpg" class="m-0 h-110 rounded shadow" />



object 关键字

fun 关键字代表了定义函数,class 关键字代表了定义类,这些都是固定的,object 关键字,却有三种迥然不同的语义,分别可以定义:


  • 匿名内部类;

  • 单例模式;

  • 伴生对象。


之所以会出现这样的情况,是因为 Kotlin 的设计者认为:


这三种语义本质上都是在定义一个类的同时还创建了对象


在这样的情况下,与其分别定义三种不同的关键字,还不如将它们统一成 object 关键字。



object:匿名内部类

在 Java 开发当中,我们经常需要写类似这样的代码:


  public interface Runnable {      void run();  }  public static void main(String[] args) {      // 创建Runnable对象并使用匿名内部类重写run方法      Runnable runnable = new Runnable() {          public void run() {              System.out.println("Runnable is running");          }      };      // 创建Thread对象并将Runnable作为参数传入      Thread thread = new Thread(runnable);      // 启动线程      thread.start();  }
复制代码


这是典型的匿名内部类写法。


在 Kotlin 当中,我们会使用 object 关键字来创建匿名内部类。


   interface Runnable {        fun run()    }        @JvmStatic    fun main(args: Array<String>) {        // 创建Runnable对象并使用匿名内部类重写run方法        val runnable: Runnable = object : Runnable {            override fun run() {                println("Runnable is running")            }        }        // 创建Thread对象并将Runnable作为参数传入        val thread: Thread = Thread(runnable)        // 启动线程        thread.start()    }
复制代码



object:单例模式

在 Kotlin 当中,要实现单例模式其实非常简单,我们直接用 object 修饰类即可:


object UserManager {    fun login() {}}
复制代码


可以看出,Kotlin 生成单例,代码量非常少


反编译后的 Java 代码:


public final class UserManager {
public static final UserManager INSTANCE;
static { UserManager var0 = new UserManager(); INSTANCE = var0; }
private UserManager() {}
public final void login() {}}
复制代码


Kotlin 编译器会将其转换成静态代码块的单例模式


虽然具有简洁的优点,但同时也存在两个缺点。


  • 不支持懒加载。

  • 不支持传参构造单例。

object:伴生对象

Kotlin 当中没有 static 关键字,所以我们没有办法直接定义静态方法和静态变量。不过,Kotlin 还是为我们提供了伴生对象,来帮助实现静态方法和变量。


Kotlin 伴生:


    companion object {        const val LEARNING_FRAGMENT_INDEX = 0               fun jumpToMe(context: Context, index: Int) {            context.startActivity(Intent(context, TrainingHomeActivity::class.java).apply {                putExtra(FRAGMENT_INDEX, index)            })        }    }
复制代码


反编译后的 Java 代码:


   private Companion() { }   public static final Companion Companion = new Companion((DefaultConstructorMarker)null);      public static final int LEARNING_FRAGMENT_INDEX = 0;     public static final class Companion {      public final void jumpToMe(@NotNull Context context, int index) {            } }
复制代码


可以看到 jumpToMe()并不是静态方法,它实际上是通过调用单例 Companion 的实例上的方法实现的。



扩展 (Extension)

Kotlin 的扩展(Extension),主要分为两种语法:


第一个是扩展函数


第二个是扩展属性


从语法上看,扩展看起来就像是我们从类的外部为它扩展了新的成员。


场景:假如我们想修改 JDK 当中的 String,想在它的基础上增加一个方法“lastElement()”来获取末尾元素,如果使用 Java,我们是无法通过常规手段实现的,因为我们没办法修改 JDK 的源代码。任何第三方提供的 SDK,我们都无权修改


不过,借助 Kotlin 的扩展函数,我们就完全可以在语义层面,来为第三方 SDK 的类扩展新的成员方法和成员属性。

扩展函数

扩展函数,就是从类的外部扩展出来的一个函数,这个函数看起来就像是类的成员函数一样


Extension.kt /* ①    ②      ③            ④ ↓     ↓       ↓            ↓   */     fun String.lastElement(): Char? {    //   ⑤    //   ↓    if (this.isEmpty()) {        return null    }
return this[length - 1]}
// 使用扩展函数fun main() { val msg = "Hello Wolrd" // lastElement就像String的成员方法一样可以直接调用 val last = msg.lastElement() // last = d}
复制代码


  • 注释①,fun 关键字,代表我们要定义一个函数。也就是说,不管是定义普通 Kotlin 函数,还是定义扩展函数,我们都需要 fun 关键字。

  • 注释②,“String.”,代表我们的扩展函数是为 String 这个类定义的。在 Kotlin 当中,它有一个名字,叫做接收者(Receiver),也就是扩展函数的接收方。

  • 注释③,lastElement(),是我们定义的扩展函数的名称。

  • 注释④,“Char?”,代表扩展函数的返回值是可能为空的 Char 类型。

  • 注释⑤,“this.”,代表“具体的 String 对象”,当我们调用 msg.lastElement() 的时候,this 就代表了 msg。




扩展函数反编译成 Java 代码:


public final class StringExtKt {   @Nullable   public static final Character lastElement(@NotNull String $this$lastElement) {      // 省略   }}
复制代码


而如果我们将上面的 StringExtKt 修改成 StringUtils,它就变成了典型的 Java 工具类


public final class StringUtils {
public static final Character lastElement(String $this) { // 省略 }}public static final void main() { Character last = StringUtils.lastElement(msg);}
复制代码


所以 Kotlin 扩展函数 本质 上和 Java 静态方法 是一样的。


只是编译器帮我们做了很多事情, 让代码写起来更简洁。



扩展属性

而扩展属性,则是在类的外部为它定义一个新的成员属性。



// 接收者类型// ↓val String.lastElement: Char? get() = if (isEmpty()) { null } else { get(length - 1) }
fun main() { val msg = "Hello Wolrd" // lastElement就像String的成员属性一样可以直接调用 val last = msg.lastElement // last = d}
复制代码




扩展函数/扩展属性对比



转换成 Java 代码后,扩展函数和扩展属性代码一致,


StringUtils.lastElement(msg); } 用法是一样的。


扩展最主要的用途,就是用来取代 Java 当中的各种工具类,比如 StringUtils、DateUtils 等等。



扩展函数在 Android 中的案例

用扩展函数简化 Toast 的用法:


这是 Toast 的标准用法,在界面上弹出一段文字提示,代码很长。


Toast.makeText(context, "This is Toast",Toast.LENGTH_SHORT).show()
复制代码


还容易忘记调 show()函数,造成 Toast 没有弹出。


用扩展函数改写后:


fun String.showToast(context: Context) {       Toast.makeText(context, this, Toast.LENGTH_SHORT).show() }
复制代码


调用时,只需要在要展示的内容后面调一下 showToast(),这样就简洁了很多。


"This is Toast".showToast(context)
复制代码



函数与 Lambda 表达式

  • 函数类型(Function Type)

  • 函数引用 (Function reference)

  • 高阶函数(Higher-order function)

  • 匿名函数 (Anonymous function)

  • Lambda Expressions

  • 函数式(SAM)接口

  • SAM 转换

  • 高阶函数应用



函数类型(Function Type)

函数类型(Function Type)就是函数<u>的</u>类型,在 Kotlin 的世界里,函数是一等公民既然变量可以有类型,函数也可以有类型。



// (Int, Int) ->Float 这就是 add 函数的类型// ↑ ↑ ↑fun add(a: Int, b: Int): Float { return (a+b).toFloat() }
复制代码


将第三行代码里的“ Int Int Float”抽出来,就可以确定该函数的类型。


将函数的“参数类型”和“返回值类型”抽象出来后,加上()-> 符号加工后,就得到了“函数类型”。


(Int, Int) ->Float 就代表了参数类型是两个 Int,返回值类型为 Float 的函数类型。



函数引用(Function reference)

普通的变量有引用的概念,我们可以将一个变量赋值给另一个变量,这一点,在函数上也是同样适用的,函数也有引用,并且也可以赋值给变量。


前面定义的 add 函数,赋值给另一个函数变量时,不能直接用的,



需要使用::操作符 , 后跟要引用的函数名,获得函数引用后才可以去赋值。


fun add(a: Int, b: Int): Float { return (a+b).toFloat() }
// 变量 函数类型 函数引用 // ↑ ↑ ↑val function: (Int, Int) -> Float = ::add println(function(2, 3)) // 输出 5
复制代码


加了双冒号:: , 这个函数才变成了一个对象,只有对象才能被赋值给变量




 fun add(a: Int, b: Int): Float { return (a+b).toFloat() }       fun testGaojie() {     println( ::add )     println( (::add)(2, 3) )// 输出 5.0    }
复制代码


通过反编译成 Java 代码,可以看出。


::add 等价于 Function2 var1 = new Function2(...)


是一个 FunctionN 类型的对象。


反编译成 Java 代码:


 public final void testGaojie() { //  println( ::add )      Function2 var1 = new Function2((GaojieFunTest)this) {         public Object invoke(Object var1, Object var2) {            return this.invoke(((Number)var1).intValue(), ((Number)var2).intValue());         }         public final float invoke(int p1, int p2) {            return ((GaojieFunTest)this.receiver).add(p1, p2);         }      };      System.out.println(var1);//  println( (::add)(2, 3) )      float var2 = ((Number)((Function2)(new Function2((GaojieFunTest)this) {         public Object invoke(Object var1, Object var2) {            return this.invoke(((Number)var1).intValue(), ((Number)var2).intValue());         }         public final float invoke(int p1, int p2) {            return ((GaojieFunTest)this.receiver).add(p1, p2);         }      })).invoke(2, 3)).floatValue();      System.out.println(var2);   }
复制代码




   fun add(a: Int, b: Int): Float { return (a+b).toFloat() }       fun testGaojie() {     println(  add(2, 3)  )// 输出 5.0     val function: (Int, Int) -> Float = ::add     println( function(2, 3) ) // 输出 5.0     println(  function.invoke(2, 3)  )  // 输出 5.0    }
复制代码


将 testGaojie()转换成 Java 代码。可以看到在 Java 里,函数类型被声明为普通的接口:一个函数类型的变量是 FunctionN 接口的一个实现。Kotlin标准库定义了一系列的接口,这些接口对应于不同参数数量函数Function0<R>(没有参数的函数)、Function2<P1,P2,R>(2 个参数的函数)...Function22<P1,P2 ... R>。每个接口定义了一个invoke()方法,调用这个方法就会执行函数。一个函数类型的变量就是实现了对应的 FunctionN 接口的实现类实例。实现类的invoke()方法包含了 函数引用对应的函数函数体


反编译成 Java 代码:


 public final void testGaojie() { // println(  add(2, 3)  )      float var1 = this.add(2, 3);      System.out.println(var1);//  val function: (Int, Int) -> Float = ::add           Function2 function = (Function2)(new Function2((GaojieFunTest)this) {         // $FF: synthetic method         // $FF: bridge method         public Object invoke(Object var1, Object var2) {            return this.invoke(((Number)var1).intValue(), ((Number)var2).intValue());         }
public final float invoke(int p1, int p2) { return ((GaojieFunTest)this.receiver).add(p1, p2); } });// println( function(2, 3) ) // 输出 5.0 float var2 = ((Number)function.invoke(2, 3)).floatValue(); System.out.println(var2);// println( function.invoke(2, 3) ) // 输出 5.0 var2 = ((Number)function.invoke(2, 3)).floatValue(); System.out.println(var2); }
复制代码



总结

Kotlin 中,函数引用和函数调用有以下区别:


  1. 函数引用可以视为函数类型的变量,它持有函数的引用。而函数调用则执行函数本身。因此,可以将函数引用传递给其他函数,并在需要时执行。

  2. 函数引用可以简化调用代码,避免冗长的代码。而函数调用则需要编写完整的函数名称、参数和参数类型。

  3. 函数引用不会立即执行函数代码,只有在需要时才执行。而函数调用则立即执行函数代码。例如,假设我们有一个名为“double”的函数,它接受一个整数并返回它的两倍。那么,函数引用和函数调用的代码如下所示:


val doubleFunc: (Int) -> Int = ::double // 函数调用val result = double(5) // 返回 10
复制代码


在这个例子中,我们定义了一个函数引用,它可以在需要时传递给其他函数,也可以在需要时执行。


第 2 行代码我们还调用了函数“double”,它立即执行代码并返回结果。



高阶函数 (Higher-order function)

高阶函数的定义:高阶函数是将函数用作参数或者返回值的函数。


如果一个函数的参数类型函数类型或者返回值类型函数类型,那么这个函数就是就是高阶函数 。


或者说,如果一个函数的参数或者返回值,其中有一个是函数,那么这个函数就是高阶函数。


    //                            函数类型的变量   函数类型    //                                 ↓            ↓    fun  higherOrderAdd( a:Int,b: Int,block: (Int, Int) -> Float):Float{//                   函数类型的变量//                       ↓        var  result = block.invoke(a,b) //                   函数类型的变量//                       ↓        var  result2 = block(a,b)        println("result:$result")        return result    }
复制代码


higherOrderAdd 有一个参数是函数类型,所以它是高阶函数



匿名函数

匿名函数看起来跟普通函数很相似,除了它的名字参数类型被省略了外。匿名函数示例如下:


fun(a :Int, b :Int) = a + b
复制代码


上面的匿名函数是没法直接调用的,赋值给变量后才可以调用


 val anonymousFunction = fun(a :Int, b :Int) = a + b
复制代码


  fun anonymousFunctionTest() {        higherOrderAdd(2,2,::add) // 函数引用        higherOrderAdd(2,2,anonymousFunction) // 函数变量        higherOrderAdd(2,2,            fun (a:Int,b:Int):Float{ return (a+b).toFloat()}) // 匿名函数    }
复制代码


匿名函数本质上也是函数类型的对象,所以可以赋值给变量。




匿名函数不能单独声明在 ()外面,因为匿名函数是(函数的声明函数引用合二为一)



// 具名函数不能直接赋值给变量,因为它不是对象



// 函数()内不能直接 声明 具名函数,因为它不是对象



这几个个报错是因为,匿名函数是把函数的声明函数引用合二为一了,所以在需要匿名函数的地方,声明一个具名函数是报错的,正确的做法是改用具名函数引用 例如:


  higherOrderAdd(2,2,::add) // 函数引用
复制代码



Lambda

Java 在 Java8 中引入的 Lambda。


Java Lambda 的基本语法是


(parameters) -> expression 
复制代码


或(请注意语句的花括号)


  (parameters) -> { statements; }
复制代码


Kotlin 语言的是可以用 Lambda 表达式作为函数参数的,Lambda 就是一小段可以作为参数传递的代码,那么到底多少代码才算一小段代码呢?Kotlin 对此并没有进行限制,但是通常不建议在 Lambda 表达式中编写太长的代码,否则可能会影响代码的可读性


Lambda 也可以理解为是匿名函数简写


我们来看一下 Lambda 表达式的语法结构:


{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}
复制代码


首先最外层是一对花括号{ },如果有参数传入到 Lambda 表达式中的话,我们还需要声明参数列表,参数列表的结尾使用一个 '->' 符号 ,表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码,并且最后一行代码会自动作为 Lambda 表达式的返回值




    fun  higherOrderAdd( a:Int,b: Int,block: (Int, Int) -> Float):Float{        var  result = block(a,b)        println("result:$result")        return result    }      @Test    fun anonymousFunctionTest() {        higherOrderAdd(2,2,::add) // 函数引用        higherOrderAdd(3,3,            fun (a:Int,b:Int):Float{ return (a+b).toFloat()}) // 匿名函数        higherOrderAdd(4,4,             { a:Int,b:Int ->  (a+b).toFloat()}) //    Lambda表达式        println(            fun (a:Int,b:Int):Float{ return (a+b).toFloat()}(5,5) ) // 匿名函数直接调用        println(            { a:Int,b:Int ->  (a+b).toFloat()}(5,5)) // Lambda表达式调用    }   
复制代码


相比匿名函数,lambda 表达式定义与引用函数更 简洁



函数式(SAM)接口

SAM 是 Single Abstract Method 的缩写,只有一个抽象方法的接口称为函数式接口SAM(单一抽象方法)接口。函数式接口可以有多个非抽象成员,但只能有一个抽象成员


在 Java 中可以用注解 @FunctionalInterface 声明一个函数式接口:


@FunctionalInterfacepublic interface Runnable {    void run();}
复制代码


在 Kotlin 中可以用 fun 修饰符在 Kotlin 中声明一个函数式接口:


// 注意 interface 前的 funfun interface KRunnable {   fun invoke()}
复制代码



SAM 转换

对于函数式接口,可以通过 lambda 表达式实现 SAM 转换,从而使代码更简洁、更有可读性。


使用 lambda 表达式可以替代手动创建 实现函数式接口的类。 通过 SAM 转换, Kotlin 可以将 签名与接口的单个抽象方法的签名匹配的任何 lambda 表达式,转换成实现该接口的类的实例


// 注意需用fun 关键字声明fun  interface  Action{    fun run(str:String)}fun  runAction(action: Action){     action.run("this  run")}
复制代码


fun main() {//      创建一个 实现函数式接口 的类 的实例(匿名内部类)    val action = object :Action{        override fun run(str: String) {            println(str)        }    }    //   传入实例,不使用 SAM 转换    runAction(action)//    利用 Kotlin 的 SAM 转换,可以改为以下等效代码://    使用 Lambda表达式替代手动创建 实现函数式接口的类    runAction({            str-> println(str)    })}
复制代码




fun  interface  InterfaceApi{    fun run(str:String)}fun  runInterface(interfaceApi: InterfaceApi){    interfaceApi.run("this  run")}//  函数类型替代接口定义fun  factionTypeReplaceInterface(block:(String)->Unit){     block("this block run")}//=======Test====// 普通函数,参数是函数式接口对象,传 函数类型对象 也是可以的fun  testFactionTypeReplaceInterface(){    val function:(String)->Unit = { println(it) }    runInterface(function) //普通函数,参数是函数式接口对象,传 函数类型对象 也是可以的    factionTypeReplaceInterface(function)}// 高阶函数, 参数是函数类型对象,传 是函数式接口对象 是不可以的。fun  testInterface(){    val interfaceApi:InterfaceApi = object :InterfaceApi{        override fun run(str: String) {            println(str)        }    }    runInterface(interfaceApi)    factionTypeReplaceInterface(interfaceApi)// 高阶函数, 参数是函数类型对象,传 是函数式接口对象 是不可以的。}
复制代码



普通函数,参数是函数式接口对象,传 函数类型对象 也是可以的


反过来不可以:


高阶函数, 参数是函数类型对象,传 是函数式接口对象 是不可以的。


前面说的都是函数传不同的参数类型。


<img src="http://seachal-blog-picture-host.oss-cn-beijing.aliyuncs.com/MWeb/2023/03/23/16795466653507.jpg"/>


这张图中的三处报错都是,类型不匹配


说明:


作为函数实参时, 函数类型对象 单向代替 函数式接口对象。


但是在创建对象时, 函数类型、函数式接口两种类型是泾渭分明的。

高阶函数应用

在 Android 开发时,我们经常会遇到给自定义 View 绑定点击事件的场景。以往通常的做法如下:



// CustomView.java
// 成员变量private OnContextClickListener mOnContextClickListener;

// 监听手指点击内容事件public void setOnContextClickListener(OnContextClickListener l) { mOnContextClickListener = l;}
// 为传递这个点击事件,专门定义了一个接口public interface OnContextClickListener { void onContextClick(View v);}
复制代码



// 设置手指点击事件customView.setOnContextClickListener(new View.OnContextClickListener() { @Override public void onContextClick(View v) { gotoPreview(); }});
复制代码


看完了这两段代码之后,你有没有觉得这样的代码会很啰嗦?因为,真正逻辑只有一行代码:gotoPreview(),而实际上我们却写了 6 行代码。



用 Kotlin 高阶函数 改写后


//View.kt// (View) -> Unit 就是「函数类型 」// ↑ ↑ var mOnContextClickListener: ((View) -> Unit)? = null

// 高阶函数fun setOnContextClickListener(l: (View) -> Unit) { mOnClickListener = l;}
复制代码


如果我们将前面 Java 写的例子的核心逻辑提取出来,会发现这样才是最简单明了的:



// { gotoPreview() } 就是 Lambda// ↑customView.setOnContextClickListener({ gotoPreview() })
复制代码


Kotlin 语言的设计者是怎么做的呢?实际上他们是分成了两个部分:


  • 用函数类型替代接口定义;

  • 用 Lambda 表达式作为函数参数。




Kotlin 中引入高阶函数会带来几个好处:一个是针对定义方,代码中减少了接口类的定义;另一个是对于调用方来说,代码也会更加简洁。这样一来,就大大减少了代码量,提高了代码可读性,并通过减少类的数量,提高了代码的性能。


最后总结

思考讨论

本文主要分享了 空安全、扩展函数、高阶函数、Lambda,


本文分享的 Kotlin 内容,您认为哪些特性是最有趣或最有用的?



参考文档:

  • Kotlin 语言中文站

  • 《Kotlin 实战》

  • 《Kotlin 核心编程》

  • 《Kotlin 编程权威指南》

  • 《Java 8 实战》

用户头像

Seachal

关注

还未添加个人签名 2015-10-17 加入

还未添加个人简介

评论

发布
暂无评论
From Java To Kotlin:空安全、扩展、函数、Lambda很详细,这次终于懂了_Java_Seachal_InfoQ写作社区