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" />
可以做什么?
Kotlin Multiplatform Mobile is in Beta!
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
简约:使用一行代码创建一个包含
getters
、setters
、equals()
、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 当中,如果我们要声明变量,我们必须要声明它的类型,后面跟着变量的名称和对应的值,然后以分号结尾。就像这样:
而 Kotlin 则不一样,我们要使用val
或者是var
这样的关键字作为开头,后面跟“变量名称”,接着是“变量类型”和“赋值语句”,最后是分号结尾。就像这样:
在 Kotlin 里面,代码末尾的分号省略不写,就像这样:
另外,由于 Kotlin 支持类型推导,大部分情况下,我们的变量类型可以省略不写,就像这样:
var 声明的变量,我们叫做可变变量,它对应 Java 里的普通变量。
val 声明的变量,我们叫做只读变量,它相当于 Java 里面的 final 变量。
var, val 反编译成 Java :
我们已经知道了 val 属性只有 getter,只能保证引用不变,不能保证内容不变。例如,下面的代码:
属性 nickname 的值并非不可变,当调用 grow() 方法时,它的值会从 "xiaozhang" 变为 "laozhang",
不过因为没有 setter,所以无法直接给 nickname 赋值
编译时常量
const 只能修饰没有自定义 getter 的 val 属性,而且它的值必须在编译时确定。
基本数据类型( Basic Data Type )
Kotlin 的基本数值类型包括 Byte、Short、Int、Long、Float、Double 等。
在 Kotlin 语言体系当中,是没有原始类型这个概念的。这也就意味着,在 Kotlin 里,一切都是对象。
空安全(Null Safety )
既然 Kotlin 中的一切都是对象,那么对象就有可能为空。如果我写这样的代码:
以上的代码并不能通过 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 的变量,我们需要在声明的时候,在变量类型后面加一个问号“?”:
并且由于 Kotlin 对可能为空的变量类型做了强制区分,这就意味着,“可能为空的变量”无法直接赋值给“不可为空的变量”,反过来 “不可为空的变量” 可以赋值给“可能为空的变量” 。
这么设计的原因是,从集合逻辑上:可能为空 包含 不可为空
而如果我们实在有这样的需求,也不难实现,只要做个判断即可:
函数声明( Define Function )
在 Kotlin 当中,函数的声明与 Java 不太一样。Java:
Kotlin :
使用了 fun 关键字来定义函数;
返回值类型,紧跟在参数的后面,这点和 Java 不一样。
如果函数体中只有一行代码,可以简写
return 可以省略
{ } 花括号可以省略
直接用
=
连接,变成一种类似 变量赋值的 函数形式
我们称之为单表达式函数
由于 Kotlin 支持类型推导,返回值类型可以省略:
这样看起来就更简洁了。
让函数更好的调用( Making functions easier to call )
命名参数/具名参数 (Named arguments)
以前面的函数为例子,我们调用它:
和 Java 一样。
不过,Kotlin 提供了一些新的特性,如命名函数参数举个例子,现在有一个函数:
如果像 Java 那样调用:
就要严格按照参数顺序传参:
参数顺序调换,参数就传错了,不好维护。
当参数是一堆数字,很难知道数字对应的形参,可读性不高。
Kotlin 参数调用:
我们把函数的形参加了进来,形参和实参用 =
连接,建立了两者的对应关系。这样可读性更强。
如果想修改某个参数例如feedCount
也可以很方便的定位到参数。 这样易维护
参数默认值(Default arguments)
gender、likeCount 等参数被赋予了默认值,当我们调用时,有些有默认值的参数就可以不传参,Kotlin 编译器自动帮我们填上默认值。
在 Java 当中要实现类似的逻辑,我们就必须手动定义新的“3 个参数的 createUser 函数”,或者是使用 Builder 设计模式。
Classes and Objects
类 (Class)
Java
Class
Kotlin
Kotlin 定义类,同样使用 class 关键字。
Kotlin 定义的类在默认情况下是 public 的。
编译器会帮我们生成“构造函数”,
对于类当中的属性,Kotlin 编译器也会根据实际情况,自动生成 getter 和 setter。
和 Java 相比 Kotlin 定义一个类足够简洁。
抽象类与继承
抽象类 (Abstract Class)
继承(Extend)
接口和实现 (Interface and implements)
Kotlin 当中的接口(interface),和 Java 也是大同小异的,它们都是通过 interface 这个关键字来定义的。
可以看到在以上的代码中,我们定义了一个新的接口 Behavior,它里面有一个需要被实现的方法 walk,然后我们在 Person 类当中实现了这个接口。
Kotlin 的继承和接口实现语法基本上是一样的。
Kotlin 的接口,跟 Java 最大的差异就在于,接口的方法可以有默认实现,同时,它也可以有属性。
我们在接口方法当中,为 walk() 方法提供了默认实现,如果 canWalk 为 true,才执行 walk 内部的具体行为。
Kotlin 当中的接口,被设计得更加强大了。
在 Java 1.8 版本当中,Java 接口也引入了类似的特性。
嵌套类和内部类( Nested and Inner Classes )
Java 当中,最常见的嵌套类分为两种:非静态内部类、静态内部类。Kotlin 当中也有一样的概念。
以上代码中,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" />
通过 javac 命令 编译成 class 文件后:
InnerClass
StaticInnerClass
通过.class 可以发现,
$InnerClass
持有外部类的引用。
$StaticInnerClass
不持有外部类的引用。
Java 当中的嵌套类,默认情况下,没有 static 关键字 时,它就是一个内部类,这样的内部类是会持有外部类的引用的。所以,这样的设计在 Java 当中会非常容易出现内存泄漏! 而我们之所以会犯这样的错误,往往只是因为忘记加static
关键字。
Kotlin 则恰好相反,在默认情况下,嵌套类变成了静态内部类,而这种情况下的嵌套类是不会持有外部类引用的。只有当我们真正需要访问外部类成员的时候,我们才会加上 inner 关键字。这样一来,默认情况下,开发者是不会犯错的,只有手动加上 inner
关键字之后,才可能会出现内存泄漏,而当我们加上 inner 之后,其实往往也就能够意识到内存泄漏的风险了。
数据类(Data Class )
Koltin 数据类 ,就是用于存放数据的类,等价于 POJO (Plain Ordinary Java Object)。要定义一个数据类,我们只需要在普通的类前面加上一个关键字 data
,就可以把它变成一个"数据类"。
编译器会为数据类自动生成一些 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 开发当中,我们经常需要写类似这样的代码:
这是典型的匿名内部类写法。
在 Kotlin 当中,我们会使用 object
关键字来创建匿名内部类。
object:单例模式
在 Kotlin 当中,要实现单例模式其实非常简单,我们直接用 object 修饰类即可:
可以看出,Kotlin 生成单例,代码量非常少
反编译后的 Java 代码:
Kotlin 编译器会将其转换成静态代码块的单例模式。
虽然具有简洁的优点,但同时也存在两个缺点。
不支持懒加载。
不支持传参构造单例。
object:伴生对象
Kotlin 当中没有 static 关键字,所以我们没有办法直接定义静态方法和静态变量。不过,Kotlin 还是为我们提供了伴生对象,来帮助实现静态方法和变量。
Kotlin 伴生:
反编译后的 Java 代码:
可以看到 jumpToMe()并不是静态方法,它实际上是通过调用单例 Companion 的实例上的方法实现的。
扩展 (Extension)
Kotlin 的扩展(Extension),主要分为两种语法:
第一个是扩展函数,
第二个是扩展属性。
从语法上看,扩展看起来就像是我们从类的外部为它扩展了新的成员。
场景:假如我们想修改 JDK 当中的 String,想在它的基础上增加一个方法“lastElement()”来获取末尾元素,如果使用 Java,我们是无法通过常规手段实现的,因为我们没办法修改 JDK 的源代码。任何第三方提供的 SDK,我们都无权修改。
不过,借助 Kotlin 的扩展函数,我们就完全可以在语义层面,来为第三方 SDK 的类扩展新的成员方法和成员属性。
扩展函数
扩展函数,就是从类的外部扩展出来的一个函数,这个函数看起来就像是类的成员函数一样
注释①,fun 关键字,代表我们要定义一个函数。也就是说,不管是定义普通 Kotlin 函数,还是定义扩展函数,我们都需要 fun 关键字。
注释②,“String.”,代表我们的扩展函数是为 String 这个类定义的。在 Kotlin 当中,它有一个名字,叫做接收者(Receiver),也就是扩展函数的接收方。
注释③,lastElement(),是我们定义的扩展函数的名称。
注释④,“Char?”,代表扩展函数的返回值是可能为空的 Char 类型。
注释⑤,“this.”,代表“具体的 String 对象”,当我们调用 msg.lastElement() 的时候,this 就代表了 msg。
扩展函数反编译成 Java 代码:
而如果我们将上面的 StringExtKt 修改成 StringUtils,它就变成了典型的 Java 工具类
所以 Kotlin 扩展函数 本质 上和 Java 静态方法 是一样的。
只是编译器帮我们做了很多事情, 让代码写起来更简洁。
扩展属性
而扩展属性,则是在类的外部为它定义一个新的成员属性。
扩展函数/扩展属性对比
转换成 Java 代码后,扩展函数和扩展属性代码一致,
和 StringUtils.lastElement(msg); }
用法是一样的。
扩展最主要的用途,就是用来取代 Java 当中的各种工具类,比如 StringUtils、DateUtils 等等。
扩展函数在 Android 中的案例
用扩展函数简化 Toast 的用法:
这是 Toast 的标准用法,在界面上弹出一段文字提示,代码很长。
还容易忘记调 show()函数,造成 Toast 没有弹出。
用扩展函数改写后:
调用时,只需要在要展示的内容后面调一下 showToast(),这样就简洁了很多。
函数与 Lambda 表达式
函数类型(Function Type)
函数引用 (Function reference)
高阶函数(Higher-order function)
匿名函数 (Anonymous function)
Lambda Expressions
函数式(SAM)接口
SAM 转换
高阶函数应用
函数类型(Function Type)
函数类型(Function Type)就是函数<u>的</u>类型,在 Kotlin 的世界里,函数是一等公民既然变量可以有类型,函数也可以有类型。
将第三行代码里的“ Int Int Float”抽出来,就可以确定该函数的类型。
将函数的“参数类型”和“返回值类型”抽象出来后,加上()
,->
符号加工后,就得到了“函数类型”。
(Int, Int) ->Float
就代表了参数类型是两个 Int,返回值类型为 Float 的函数类型。
函数引用(Function reference)
普通的变量有引用的概念,我们可以将一个变量赋值给另一个变量,这一点,在函数上也是同样适用的,函数也有引用,并且也可以赋值给变量。
前面定义的 add 函数,赋值给另一个函数变量时,不能直接用的,
需要使用::操作符 , 后跟要引用的函数名,获得函数引用后才可以去赋值。
加了双冒号:: , 这个函数才变成了一个对象,只有对象才能被赋值给变量。
通过反编译成 Java 代码,可以看出。
::add
等价于 Function2 var1 = new Function2(...)
,
是一个 FunctionN 类型的对象。
反编译成 Java 代码:
将 testGaojie()转换成 Java 代码。可以看到在 Java 里,函数类型被声明为普通的接口:一个函数类型的变量是 FunctionN 接口的一个实现。Kotlin标准库定义了一系列的接口,这些接口对应于不同参数数量的函数:Function0<R>
(没有参数的函数)、Function2<P1,P2,R>
(2 个参数的函数)...Function22<P1,P2 ... R>
。每个接口定义了一个invoke()
方法,调用这个方法就会执行函数。一个函数类型的变量就是实现了对应的 FunctionN 接口的实现类的实例。实现类的invoke()
方法包含了 函数引用对应的函数的函数体
反编译成 Java 代码:
总结
Kotlin 中,函数引用和函数调用有以下区别:
函数引用可以视为函数类型的变量,它持有函数的引用。而函数调用则执行函数本身。因此,可以将函数引用传递给其他函数,并在需要时执行。
函数引用可以简化调用代码,避免冗长的代码。而函数调用则需要编写完整的函数名称、参数和参数类型。
函数引用不会立即执行函数代码,只有在需要时才执行。而函数调用则立即执行函数代码。例如,假设我们有一个名为“double”的函数,它接受一个整数并返回它的两倍。那么,函数引用和函数调用的代码如下所示:
在这个例子中,我们定义了一个函数引用,它可以在需要时传递给其他函数,也可以在需要时执行。
第 2 行代码我们还调用了函数“double”,它立即执行代码并返回结果。
高阶函数 (Higher-order function)
高阶函数的定义:高阶函数是将函数用作参数或者返回值的函数。
如果一个函数的参数类型是函数类型或者返回值类型是函数类型,那么这个函数就是就是高阶函数 。
或者说,如果一个函数的参数或者返回值,其中有一个是函数,那么这个函数就是高阶函数。
higherOrderAdd 有一个参数是函数类型,所以它是高阶函数
匿名函数
匿名函数看起来跟普通函数很相似,除了它的名字和参数类型被省略了外。匿名函数示例如下:
上面的匿名函数是没法直接调用的,赋值给变量后才可以调用
匿名函数本质上也是函数类型的对象,所以可以赋值给变量。
匿名函数不能单独声明在 ()外面,因为匿名函数是(函数的声明与函数引用合二为一)
// 具名函数不能直接赋值给变量,因为它不是对象
// 函数()内不能直接 声明 具名函数,因为它不是对象
这几个个报错是因为,匿名函数是把函数的声明与函数引用合二为一了,所以在需要匿名函数的地方,声明一个具名函数是报错的,正确的做法是改用具名函数引用 例如:
Lambda
Java 在 Java8 中引入的 Lambda。
Java Lambda 的基本语法是
或(请注意语句的花括号)
Kotlin 语言的是可以用 Lambda 表达式作为函数参数的,Lambda 就是一小段可以作为参数传递的代码,那么到底多少代码才算一小段代码呢?Kotlin 对此并没有进行限制,但是通常不建议在 Lambda 表达式中编写太长的代码,否则可能会影响代码的可读性。
Lambda 也可以理解为是匿名函数的简写。
我们来看一下 Lambda 表达式的语法结构:
首先最外层是一对花括号{ },如果有参数传入到 Lambda 表达式中的话,我们还需要声明参数列表,参数列表的结尾使用一个 '->' 符号 ,表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码,并且最后一行代码会自动作为 Lambda 表达式的返回值。
相比匿名函数,lambda 表达式定义与引用函数更 简洁 。
函数式(SAM)接口
SAM 是 Single Abstract Method 的缩写,只有一个抽象方法的接口称为函数式接口或 SAM(单一抽象方法)接口。函数式接口可以有多个非抽象成员,但只能有一个抽象成员。
在 Java 中可以用注解 @FunctionalInterface 声明一个函数式接口:
在 Kotlin 中可以用 fun 修饰符在 Kotlin 中声明一个函数式接口:
SAM 转换
对于函数式接口,可以通过 lambda 表达式实现 SAM 转换,从而使代码更简洁、更有可读性。
使用 lambda 表达式可以替代手动创建 实现函数式接口的类。 通过 SAM 转换, Kotlin 可以将 签名与接口的单个抽象方法的签名匹配的任何 lambda 表达式,转换成实现该接口的类的实例。
普通函数,参数是函数式接口对象,传 函数类型对象 也是可以的
反过来不可以:
高阶函数, 参数是函数类型对象,传 是函数式接口对象 是不可以的。
前面说的都是函数传不同的参数类型。
<img src="http://seachal-blog-picture-host.oss-cn-beijing.aliyuncs.com/MWeb/2023/03/23/16795466653507.jpg"/>
这张图中的三处报错都是,类型不匹配。
说明:
作为函数实参时, 函数类型对象 单向代替 函数式接口对象。
但是在创建对象时, 函数类型、函数式接口两种类型是泾渭分明的。
高阶函数应用
在 Android 开发时,我们经常会遇到给自定义 View 绑定点击事件的场景。以往通常的做法如下:
看完了这两段代码之后,你有没有觉得这样的代码会很啰嗦?因为,真正逻辑只有一行代码:gotoPreview(),而实际上我们却写了 6 行代码。
用 Kotlin 高阶函数 改写后
如果我们将前面 Java 写的例子的核心逻辑提取出来,会发现这样才是最简单明了的:
Kotlin 语言的设计者是怎么做的呢?实际上他们是分成了两个部分:
用函数类型替代接口定义;
用 Lambda 表达式作为函数参数。
Kotlin 中引入高阶函数会带来几个好处:一个是针对定义方,代码中减少了接口类的定义;另一个是对于调用方来说,代码也会更加简洁。这样一来,就大大减少了代码量,提高了代码可读性,并通过减少类的数量,提高了代码的性能。
最后总结
思考讨论
本文主要分享了 空安全、扩展函数、高阶函数、Lambda,
本文分享的 Kotlin 内容,您认为哪些特性是最有趣或最有用的?
参考文档:
《Kotlin 实战》
《Kotlin 核心编程》
《Kotlin 编程权威指南》
《Java 8 实战》
评论