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

Posted Seachal的笔记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了From Java To Kotlin:空安全扩展函数Lambda很详细,这次终于懂了相关的知识,希望对你有一定的参考价值。

Kotlin 是什么? 可以做什么? Android 官方开发语言从Java变为Kotlin,Java 有哪些问题? Kotlin的优点 Kotlin 特性(Features)

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


可以做什么?


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 :


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

这是因为 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  // 编译通过





函数声明( 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 代码:


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


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)就是函数类型
在 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 声明一个函数式接口:

@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)// 高阶函数, 参数是函数类型对象,传 是函数式接口对象 是不可以的。

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

反过来不可以:

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

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

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

说明:

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

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

高阶函数应用

在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实战》

Better Kotlin

Better Kotlin

转眼间使用 Kotlin 已经有两个月了,时间不长,我也算搭上了 Google 宣布 Kotlin 作为官方支持语言的一波末班车。可能大家早已从纯 Java 开发 Android 转为了混合使用开发甚至是 Kotlin 开发,那你转向 Kotlin 的初衷又是什么呢?

对于我,很简单,只是因为一句话:「Google 爸爸都推荐的语言,我们没理由不用!」

Kotlin 有着诸多的特性,比如空指针安全、方法扩展、支持函数式编程、丰富的语法糖等。这些特性使得 Kotlin 的代码比 Java 简洁优雅许多,提高了代码的可读性和可维护性,节省了开发时间,提高了开发效率,但同样作为 Kotlin 使用者的你,我相信你一定也有不少小建议和小技巧,一直想迫不及待地分享给大家。

那就给你一个机会,愿你把你的黑科技悄悄留言在本文下方!截止到明天早上 9 点,点赞最多的找我有小奖励哟~

我想给大家的一些小建议

这么有趣的活动,那我作为一名两个月的 Kotlin 开发,自然也应该来这个活动凑凑热闹。

1. 避免使用 IDE 自带的插件转换 Java 代码

想必 IDE 里面的插件 "Covert Java File To Kotlin File" 早已被大家熟知,要是不知道的小伙伴,赶紧写个 Java 文件,尝试点击 Android Studio 工具栏的 Code 下面的 "Convert Java File To Kotlin File",看看都有什么小妙用。

这也是南尘最开始喜欢使用的方式,没有技术却有一颗装 ✘ 的内心,直接写成 Java 文件,再直接一键转换为 Kotlin。甚至宝宝想告诉你,我 GitHub 上 1k Star 的 AiYaGilr 项目的 Kotlin 分支,也是这样而来。但真是踩了不少的坑。

这样的方式足够地快,但却会出现很多很多的 !!,这是由于 Kotlin 的 null safety 特性。这是 Kotlin 在 Android 开发中的很牛逼的一大特性,想必不少小伙伴都被此 Android 的 NullPointException 困扰许久。我们直接转换 Java 文件造成的各种 !!,其实也就意味着你可能存在潜在的未处理的 KotlinNullPointException

2. 尽量地使用 val

val 是线程安全的,并且不需要担心 null 的问题,我们自然应该尽可能地使用它。

比如我们常用的 Android 解析的服务器数据,我们应该为自己的 data class 设置为 val,因为它本身就不应该是可写的。

当我第一次使用 Kotlin 的时候,我以为val 和 var 的区别在于val 代表不可变,而 var 代表是可变的。但事实比这更加微妙:val 不代表不可变,val 意味着只读。。这意味着你不允许明确声明为 val,它就不能保证它是不可变的。

对于普通变量来说,「不可变」和「只读」之间并没什么区别,因为你没办法复写一个 val 变量,所以在此时却是是不可变的。但在 class 的成员变量中,「只读」和「不可变」的区别就大了。

在 Kotlin 的类中,val 和 var 是用于表示属性是否有 getter/setter:

  • var:同时有 getter 和 setter。

  • val:只有 getter。

这里是可以通过自定义 getter 函数来返回不同的值:

class Person(val birthDay: DateTime) {  
  val age: Int
    get() = yearsBetween(birthDay, DateTime.now())
}

可以看到,虽然没有方法来设置 age 的值,但会随着当前日期的变化而变化。

这种情况下,我建议不要自定义 val 属性的 getter 方法。如果一个只读的类属性会随着某些条件而变化,那么应当用函数来替代:

class Person(val birthDay: DateTime) {  
  fun age()Int = yearsBetween(birthDay, DateTime.now())
}

这也是 Kotlin 代码约定 中所提到的,当具有下面列举的特点时使用属性,不然更推荐使用函数:

  • 不会抛出异常。

  • 具有 O(1) 的复杂度。

  • 计算时的消耗很少。

  • 同时多次调用有相同的返回值。

因此上面提到的,自定义 getter 方法并随着当前时间的不同而返回不同的值违反了最后一条原则。大家也要尽量的避免这种情况。

3. 你真的应该好好注意一下伴生对象

伴生对象通过在类中使用 companion object 来创建,用来替代静态成员,类似于 Java 中的静态内部类。所以在伴生对象中声明常量是很常见的做法,但如果写法不对,可能就会产生额外开销。

比如下面的这段代码:

class CompanionKotlin {
    companion object {
        val DATA = "CompanionKotlin_DATA"
    }

    fun getData(): String = DATA
}

挺简洁地一段代码。但将这段简洁的 Kotlin 代码转换为等同的 Java 代码后,却显的晦涩难懂。

public final class CompanionKotlin {
   @NotNull
   private static final String DATA = "CompanionKotlin_DATA";
   public static final CompanionKotlin.Companion Companion = new CompanionKotlin.Companion((DefaultConstructorMarker)null);

   @NotNull
   public final String getData() {
      return DATA;
   }
    // ...
   public static final class Companion {
      @NotNull
      public final String getDATA() {
         return CompanionKotlin.DATA;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

与 Java 直接读取一个常量不同,Kotlin 访问一个伴生对象的私有常量字段需要经过以下方法:

  • 调用伴生对象的静态方法

  • 调用伴生对象的实例方法

  • 调用主类的静态方法

  • 读取主类中的静态字段

为了访问一个常量,而多花费调用4个方法的开销,这样的 Kotlin 代码无疑是低效的。

我们可以通过以下解决方法来减少生成的字节码:

  1. 对于基本类型和字符串,可以使用 const 关键字将常量声明为编译时常量。

  2. 对于公共字段,可以使用 @JvmField 注解。

  3. 对于其他类型的常量,最好在它们自己的主类对象而不是伴生对象中来存储公共的全局常量。

4. @JvmStatic、@JvmFiled 和 object 还有这种故事?

我们在 Kotlin 中发现了 object 这个东西,我以前就一直对这个东西很好奇,不知道这是个什么玩意儿。

object ?难道又一个对象?

之前有人写过这样的代码,表示很不解,一个接口类型的成员变量,访问外部类的成员变量 name。这不是理所应当的么?

interface Runnable {
    fun run()
}

class Test {
    private val name: String = "nanchen"

    object impl : Runnable {
        override fun run() {
            // 这里编译器会报红报错。对 name
            println(name)
        }
    }
}

即使查看 Kotlin 官方文档,也有这样一段描述:

Sometimes we need to create an object of a slight modification of some class, without explicitly declaring a new subclass for it. Java handles this case with anonymous inner classes. Kotlin slightly generalizes this concept with object expressions and object declarations.

核心意思是:Kotlin 使用 object 代替 Java 匿名内部类实现。

很明显,即便如此,这里的访问应该也是合情合理的。从匿名内部类中访问成员变量在 Java 语言中是完全允许的。

这个问题很有意思,解答这个我们需要生成 Java 字节码,再反编译成 Java 看看具体生成的代码是什么。

public final class Test {
   private final String name = "nanchen";
   public static final class impl implements Runnable {
      public static final Test.impl INSTANCE;

      public void run() {
      }

      static {
         Test.impl var0 = new Test.impl();
         INSTANCE = var0;
      }
   }
}

public interface Runnable {
   void run();
}

静态内部类!确实,Java 中静态内部类是不允许访问外部类的成员变量的。但,说好的 object 代替的是 Java 的匿名内部类呢?那这里为啥是静态内部类。

这里一定要注意,如果你只是这样声明了一个object,Kotlin认为你是需要一个静态内部类。而如果你用一个变量去接收object表达式,Kotlin认为你需要一个匿名内部类对象。

因此,这个类应该这样改进:

interface Runnable {
    fun run()
}

class Test {
    private val name: String = "nanchen"

    private val impl = object : Runnable {
        override fun run() {
            println(name)
        }
    }
}

为了避免出现这个问题,谨记一个原则:如果 object 只是声明,它代表一个静态内部类。如果用变量接收 object 表达式,它代表一个匿名内部类对象。

讲到这,自然也就知道了 Kotlin 对 object 的三个作用:

  • 简化生成静态内部类

  • 生成匿名内部类对象

  • 生成单例对象

咳咳,说了那么多,到底和 @JvmStatic 和 @JvmField 有啥关系呢?

实际上,目前我们大多数的 Android 项目都是 Java 和 Kotlin 混编的,包括我们的项目在内也是如此。所以我们总是免不了 Java 和 Kotlin 互调的情况。我们可能经常会在代码中这样编写:

object Test1 {
    val NAME = "nanchen"
    fun getAge() = 18
}

在 Java 中会调用是这样的:

System.out.println("name:"+Test1.INSTANCE.getNAME()+",age:"+Test1.INSTANCE.getAge());

作为强迫症重度患者的我,自然是无法接受上面这样奇怪的代码。所以我强烈建议大家在 object 和 companion object 中分别为变量和方法增加上 @JvmField 和 @JvmStatic 注解。

object Test1 {
    @JvmField
    val NAME = "nanchen"
    @JvmStatic
    fun getAge() = 18
}

这样外面 Java 调用起来就好看多了。

5. by lazy 和 lateinit 相爱相杀

在 Android 开发中,我们经常会有不少的成员变量需要在 onCreate() 中对其进行初始化,特别是我们在 XML 中使用的各种控件,而 Kotlin 要求声明成员变量的时候默认需要为它声明一个初始值。这时候就会出现不少的下面这样的代码。

private var textView:TextView? = null

迫于压力,我们不能不为这些 View 加上 ? 代表它们可以为空,然后为它们赋值为 null。实际上,我们在使用中一点都不希望它们为空。这样造成的后果就是,我们每次要使用它的时候都必须去先判断它不为空。这样无用的代码,无疑是浪费了我们的工作时间。

好在 Kotlin 推出了 lateinit 关键字:延迟加载。这样我们可以先绕过 kotlin 的强制要求,在后面使用的时候,再也不需要先判断它是否为空了。但要注意,访问未初始化的 lateinit 属性会导致UninitializedPropertyAccessException

并且 lateinit 不支持基础数据类型,比如 Int。对于基础数据类型,我们可以这样:

private var mNumber: Int by Delegates.notNull<Int>()

当然,我们还可以使用 let 函数来进行上面的这种情况,但无疑都是画蛇添足的。

我们前面说了,在一些明知是只读不可写不可变的变量,我们尽可能地用 val 去修饰它。而 lateinit 仅仅能修饰 var 变量,所以 by lazy 懒加载,是时候表演真正的技术了。

对于很多不可变的变量,比如上个页面通过 bundle 传递过来的用于该页面请求网络的参数,比如 MVP 架构开发中的 Presenter,我们都应该用 by lazy 关键字去初始化它。

lazy() 委托属性可以用于只读属性的惰性加载,但是在使用 lazy() 时经常被忽视的地方就是有一个可选的model参数:

  • LazyThreadSafetyMode.SYNCHRONIZED:初始化属性时会有双重锁检查,保证该值只在一个线程中计算,并且所有线程会得到相同的值。

  • LazyThreadSafetyMode.PUBLICATION:多个线程会同时执行,初始化属性的函数会被多次调用,但是只有第一个返回的值被当做委托属性的值。

  • LazyThreadSafetyMode.NONE:没有双重锁检查,不应该用在多线程下。

lazy() 默认情况下会指定 LazyThreadSafetyMode.SYNCHRONIZED,这可能会造成不必要线程安全的开销,应该根据实际情况,指定合适的model来避免不需要的同步锁。

6.注意 Kotlin 中的 for 循环

Kotlin提供了 downTostepuntilreversed 等函数来帮助开发者更简单的使用 For 循环,如果单一的使用这些函数确实是方便简洁又高效,但要是将其中两个结合呢?比如下面这样:

class A {
    fun loop() {
        for (i in 10 downTo 0 step 3) {
            println(i)
        }
    }
}

上面使用了 downTo 和 step 两个关键字,我们看看 Java 是怎样实现的。

public final class A {
   public final void loop() {
      IntProgression var10000 = RangesKt.step(RangesKt.downTo(100), 3);
      int i = var10000.getFirst();
      int var2 = var10000.getLast();
      int var3 = var10000.getStep();
      if (var3 > 0) {
         if (i > var2) {
            return;
         }
      } else if (i < var2) {
         return;
      }

      while(true) {
         System.out.println(i);
         if (i == var2) {
            return;
         }

         i += var3;
      }
   }
}

毫无疑问:IntProgression var10000 = RangesKt.step(RangesKt.downTo(10, 0), 3); 一行代码就创建了两个 IntProgression 临时对象,增加了额外的开销。

7. 注意 Kotlin 的可空和不可空

最近闹了一个笑话,在项目中需要写一个上传跳绳数据的功能。于是有了下面的代码。

public interface ISkipService {
    /**
     * 上传用户跳绳数据
     */

    @POST("v2/rope/upload_jump_data")
    Observable<BaseResponse<Object>> uploadJumpData(@Field("data") List<SkipHistoryBean> data);
}

写毕上面的接口,我们再到 ViewModel 中进行网络请求。

private List<SkipHistoryBean> list = new ArrayList<>();

public void uploadClick() {
    mNavigator.showProgressDialog();
    list.add(bean);
    RetrofitManager.create(ISkipService.class)
        .uploadJumpData(list)
        .compose(RetrofitUtil.schedulersAndGetData())
        .subscribe(new BaseSubscriber<Object>() {
            @Override
            protected void onSuccess(Object data) {
                mNavigator.hideProgressDialog();
                mNavigator.uploadDataSuccess();
                // 点击上传成功,删除数据库
                deleteDataFromDB();
            }

            @Override
            protected void onFail(ErrorBean errorBean) {
                super.onFail(errorBean);
                mNavigator.hideProgressDialog();
                mNavigator.uploadDataFailed(errorBean.error_description);
            }
        });
}

运行其实并没有什么问题。但由于某些原因,当我把上面的 ISkipService 类修改为了 Kotlin 实现,却发生了崩溃,从代码上暂时没看出问题。

interface ISkipService {
    /**
     * 上传用户跳绳数据
     */

    @POST("v2/rope/upload_jump_data")
    fun uploadJumpData(@Field("data") dataList<SkipHistoryBean>): Observable<BaseResponse<Any>>
}

但确实就是崩溃了。仔细一看,发现 Java 编写这个接口的时候,会被认为这个参数 "data" 对应的 "value" 是可以为 null 的,而改为 Kotlin 后,由于 Kotlin 默认不为空的机制,所以需要的参数是一个不可以为 null 的 List 集合。而我们的 ViewModel 中使用的 Java 代码,由于 Java 认为我们的 List 是可以为 null 的,所以导致了类型不匹配的崩溃。

找到了原因,解决方案也就很简单,在 Kotlin 接口中允许参数 data 为 null 或者直接在调用点加上 @NotNull 注解即可。

写在最后

真是想继续写呀,但参考了不少的资料,大家如果觉得有意思可以尽情地参见原文。

参考链接:
https://blog.danlew.net/2017/05/30/mutable-vals-in-kotlin/
https://juejin.im/post/5ad18d705188255c5668ddf0
https://tech.meituan.com/Kotlin_code_inspect.html

—————END—————



推荐阅读:



以上是关于From Java To Kotlin:空安全扩展函数Lambda很详细,这次终于懂了的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin空安全 ① ( Kotlin 的空安全机制 | 变量可空性 | 默认变量不可赋空值 | 声明可空类型变量 )

Kotlin初探

Better Kotlin

Kotlin 基础学习

不会用kotlin?这篇看完不懂,我跪搓衣板

Kotlin 有而 Java 没有的东西