共计 19079 个字符,预计需要花费 48 分钟才能阅读完成。
From Java To Kotlin, 空平安、扩大、函数、Lambda
概述(Summarize)
- Kotlin 是什么?
- 能够做什么?
- Android 官网开发语言从 Java 变为 Kotlin,Java 有哪些问题?
- Kotlin 的长处
- Kotlin 个性(Features)
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
- 简洁:应用一行代码创立一个蕴含
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)
- 根本数据类型(Basic Data Type)
- 空平安(Null Safety)
- 函数申明(Define Function)
-
让函数更好的调用(Making functions easier to call)
- 命名参数 / 具名参数(Named arguments)
- 参数默认值(Default arguments)
变量(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 编译。
<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.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)
- 抽象类 (Abstract Class)
- 继承(Extend)
- 接口和实现(Interface and implements)
- 嵌套类和外部类(Nested and Inner Classes)
- 数据类(Data Class)
-
object 关键字
- object:匿名外部类
- object:单例模式
- object:伴生对象
-
扩大 (Extension)
- 什么是扩大函数和扩大属性?
- 扩大函数在 Android 中的案例
类(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 中,函数援用和函数调用有以下区别:
- 函数援用能够视为函数类型的变量,它持有函数的援用。而函数调用则执行函数自身。因而,能够将函数援用传递给其余函数,并在须要时执行。
- 函数援用能够简化调用代码,防止简短的代码。而函数调用则须要编写残缺的函数名称、参数和参数类型。
- 函数援用不会立刻执行函数代码,只有在须要时才执行。而函数调用则立刻执行函数代码。
例如,假如咱们有一个名为“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)// 高阶函数,参数是函数类型对象,传 是函数式接口对象 是不能够的。}
一般函数,参数是函数式接口对象,传 函数类型对象 也是能够的
反过来不能够:
高阶函数,参数是函数类型对象,传 是函数式接口对象 是不能够的。
后面说的都是函数传不同的参数类型。
<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 中引入高阶函数会带来几个益处:一个是针对 定义方 ,代码中 缩小 了接口类的定义;另一个是对于 调用方 来说,代码也会更加 简洁。这样一来,就大大减少了代码量,进步了代码可读性,并通过缩小类的数量,进步了代码的性能。
不应用高阶函数 | 应用高阶函数 | |
---|---|---|
定义方 | 须要额定定义接口 | 不须要额定定义接口 |
调用方 | 代码繁琐 | 代码简洁清晰 |
性能 | 差 | 借助 inline 的状况,性能更高 |
最初总结
思考探讨
本文次要分享了 空平安、扩大函数、高阶函数、Lambda,
本文分享的 Kotlin 内容,您认为哪些个性是最乏味或最有用的?
参考文档:
- Kotlin 语言中文站
- 《Kotlin 实战》
- 《Kotlin 外围编程》
- 《Kotlin 编程权威指南》
- 《Java 8 实战》