12

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

 1 year ago
source link: https://www.cnblogs.com/Seachal/p/17436254.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
1.From Java To Kotlin:空安全、扩展、函数、Lambda很详细,这次终于懂了05-27

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 兼容

16789367124770.jpg

可以做什么?


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 = 100
price = 101

val num = 1
num = 2 // 编译器报错

var, val 反编译成 Java :

16794727076711.jpg

我们已经知道了 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 等。

类型 位宽度 备注
Double 64 Kotlin 没有 double
Float 32 Kotlin 没有 float
Long 64 Kotlin 没有 long
Int 32 Kotlin 没有 int/Intege
Short 16 Kotlin 没有 short
Byte 8 Kotlin 没有 byte

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


空安全(Null Safety )

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

val i: Double = null // 编译器报错

以上的代码并不能通过 Kotlin 编译。

16786156409163.jpg

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

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

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

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


var i: Double = 1.0
var j: Double? = null

i = j  // 编译器报错
j = i  // 编译通过

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

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

var i: Double = 1.0
val j: Double? = null

if (j != null) {
    i = j  // 编译通过
}

16794913913780.jpg

16794913994802.jpg
16794915198282.jpg

16794915259524.jpg

16794915323167.jpg

函数声明( 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)

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 代码:

16788807777726.jpg

 public class JavaOuterInnerClass2 {
   // 内部类
    public  class InnerClass {
    }
    // 静态内部类
    public  static  final   class  StaticInnerClass{
    }
}

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

  • InnerClass
    16794701435641.jpg
  • StaticInnerClass
    16794701485798.jpg

通过.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代码:

16788793363013.jpg

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
}

扩展函数/扩展属性对比

16791315638138.jpg

转换成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)就是函数的类型
在 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 函数,赋值给另一个函数变量时,不能直接用的,
16795388827942.jpg

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

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()}) // 匿名函数
    }

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


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

16793015073416.jpg

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

16793021437622.jpg

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

16793025512019.jpg

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

  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 声明一个函数式接口:

@FunctionalInterface
public interface Runnable {
    void run();
}

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

// 注意 interface 前的 fun
fun 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)// 高阶函数, 参数是函数类型对象,传 是函数式接口对象 是不可以的。
}
16795464399284.jpg

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

反过来不可以:

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

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

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

不使用高阶函数 使用高阶函数
定义方 需要额外定义接口 不需要额外定义接口
调用方 代码繁琐 代码简洁清晰
性能 借助inline的情况,性能更高

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

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


参考文档:

  • Kotlin 语言中文站
  • 《Kotlin实战》
  • 《Kotlin核心编程》
  • 《Kotlin编程权威指南》
  • 《Java 8实战》

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK