从Java角度深入理解Kotlin
前言
前几个月,在组内分享了关于Kotlin相关的内容。但由于PPT篇幅的原因,有些内容讲的也不是很详细。另外本人也参与了扔物线组织的 码上开学,该社区主要用于分享 Kotlin
和 Jetpack
相关的技术, 如果您对Kotlin或者Jetpack使用上有想要分享的地方,也欢迎您一起来完善该社区,欢迎联系扔物线。
为了方便大家对本文有一个大概的了解,先总的说下本文主要讲Kotlin哪些方面的内容(下面的目录和我在组内分享时PPT目录是类似的):
- Kotlin数据类型、访问修饰符
- Kotlin和Java数据类型对比
- Kotlin和Java访问修饰符对比
- Kotlin中的Class和Interface
- Kotlin中声明类的几种方式
- Kotlin中interface原理分析
- lambda表达式
- lambda初体验
- 定义lambda表达式
- Member Reference
- 常用函数let、with、run、apply分析
- lambda原理分析
- 高阶函数
- 高阶函数的定义
- 高阶函数的原理分析
- 高阶函数的优化
- Kotlin泛型
- Java泛型:不变、协变、逆变
- Kotlin中的协变、逆变
- Kotlin泛型擦除和具体化
- Kotlin集合
- kotlin集合创建方式有哪些
- kotlin集合的常用的函数
- Kotlin集合Sequence原理
- Koltin和Java交互的一些问题
- 总结
Kotlin数据类型、访问修饰符
为什么要讲下Kotlin数据类型和访问修饰符修饰符呢?因为Kotlin的数据类型和访问修饰符和Java的还是有些区别的,所以单独拎出来说一下。
Kotlin数据类型
我们知道,在Java中的数据类型分基本数据类型和基本数据类型对应的包装类型。如Java中的整型int和它对应的Integer包装类型。
在Kotlin中是没有这样的区分的,例如对于整型来说只有Int这一个类型,Int是一个类(姑且把它当装包装类型),我们可以说在Kotlin中在编译前只有包装类型,为什么说是编译前呢?因为编译时会根据情况把这个整型(Int
)是编译成 Java 中的 int
还是 Integer
。 那么是根据哪些情况来编译成基本类型还是包装类型呢,后面会讲到。我们先来看下Kotlin和Java数据类型对比:
Java基本类型 | Java包装类型 | Kotlin对应 |
---|---|---|
char | java.lang.Character | kotlin.Char |
byte | java.lang.Byte | kotlin.Byte |
short | java.lang.Short | kotlin.Short |
int | java.lang.Integer | kotlin.Int |
float | java.lang.Float | kotlin.Float |
double | java.lang.Double | Kotlin.Double |
long | java.lang.Long | kotlin.Long |
boolean | java.lang.Boolean | kotlin.Boolean |
下面来分析下哪些情况编译成Java中的基本类型还是包装类型。下面以整型为例,其他的数据类型同理。
1. 如果变量可以为null(使用操作符?
),则编译后是包装类型
//因为可以为null,所以编译后为Integer
var width: Int? = 10
var width: Int? = null
//编译后的代码
@Nullable
private static Integer width = 10;
@Nullable
private static Integer width;
再来看看方法返回值为整型:
//返回值Int编译后变成基本类型int
fun getAge(): Int {
return 0
}
//返回值Int编译后变成Integer
fun getAge(): Int? {
return 0
}
所以声明变量后者方法返回值的时候,如果声明可以为null,那么编译后时是包装类型,反之就是基本类型。
2. 如果使用了泛型则编译后是包装类型,如集合泛型、数组泛型等
//集合泛型
//集合里的元素都是Integer类型
fun getAge3(): List<Int> {
return listOf(22, 90, 50)
}
//数组泛型
//会编译成一个Integer[]
fun getAge4(): Array<Int> {
return arrayOf(170, 180, 190)
}
//看下编译后的代码:
@NotNull
public static final List getAge3() {
return CollectionsKt.listOf(new Integer[]{22, 90, 50});
}
@NotNull
public static final Integer[] getAge4() {
return new Integer[]{170, 180, 190};
}
3. 如果想要声明的数组编译后是基本类型的数组,需要使用xxxArrayOf(…),如intArrayOf
从上面的例子中,关于集合泛型编译后是包装类型在Java中也是一样的。如果想要声明的数组编译后是基本类型的数组,需要使用Kotlin为我们提供的方法:
//会编译成一个int[]
fun getAge5(): IntArray {
return intArrayOf(170, 180, 190)
}
当然,除了intArrayOf,还有charArrayOf、floatArrayOf等等,就不一一列举了。
4. 为什么Kotlin要单独设计一套这样的数据类型,不共用Java的那一套呢?
我们都知道,Kotlin是基于JVM的一款语言,编译后还是和Java一样。那么为什么不像集合那样直接使用Java那一套,要单独设计一套这样的数据类型呢?
Kotlin中没有基本数据类型,都是用它自己的包装类型,包装类型是一个类,那么我们就可以使用这个类里面很多有用的方法。下面看下Kotlin in Action的一段代码:
fun showProgress(progress: Int) {
val percent = progress.coerceIn(0, 100)
println("We're $percent% done!")
}
编译后的代码为:
public static final void showProgress(int progress) {
int percent = RangesKt.coerceIn(progress, 0, 100);
String var2 = "We're " + percent + "% done!";
System.out.println(var2);
}
从中可以看出,在开发阶段我们可很方便地使用Int类扩展函数。编译后,依然编译成基本类型int,使用到的扩展函数的逻辑也会包含在内。
关于Kotlin中的数据类型就讲到这里,下面来看下访问修饰符
Kotlin访问修饰符
我们知道访问修饰符可以修饰类,也可以修饰类的成员。下面通过两个表格来对比下Kotlin和Java在修饰类和修饰类成员的异同点:
表格一:类访问修饰符:
类访问修饰符 | Java可访问级别 | Kotlin可访问级别 |
---|---|---|
public | 均可访问 | 均可访问 |
protected | 同包名 | 同包名也不可访问 |
internal | 不支持该修饰符 | 同模块内可见 |
default | 同包名下可访问 | 相当于public |
private | 当前文件可访问 | 当前文件可访问 |
表格二:类成员访问修饰符:
成员修饰符 | Java可访问级别 | Kotlin可访问级别 |
---|---|---|
public | 均可访问 | 均可访问 |
protected | 同包名或子类可访问 | 只有子类可访问 |
internal | 不支持该修饰符 | 同模块内可见 |
default | 同包名下可访问 | 相当于public |
private | 当前文件可访问 | 当前文件可访问 |
通过以上两个表格,有几点需要讲一下。
1. internal 修饰符是kotlin独有而Java中没有的
internal修饰符意思是只能在当前模块访问,出了当前模块不能被访问。
需要注意的是,如果A类是internal修饰,B类继承A类,那么B类也必须是internal的,因为如果kotlin允许B类声明成public的,那么A就间接的可以被其他模块的类访问。
也就是说在Kotlin中,子类不能放大父类的访问权限。类似的思想在protected修饰符中也有体现,下面会讲到。
2. protected 修饰符在Kotlin和Java中的异同点
1) protected修饰类
我们知道,如果protected修饰类,在Java中该类只能被同包名下的类访问。
这样也可能产生一些问题,比如某个库中的类A是protected的,开发者想访问它,只需要声明一个类和类A相同包名即可。
而在Kotlin中就算是同包名的类也不能访问protected修饰的类。
为了测试protected修饰符修饰类,我在写demo的时候,发现protected修饰符不能修饰*类,只能放在内部类上。
为什么不能修饰*类?
一方面,在Java中protected修饰的类,同包名可以访问,default修饰符已经有这个意思了,把*类再声明成protected没有什么意义。
另一方面,在Java中protected如果修饰类成员,除了同包名可以访问,不同包名的子类也可以访问,如果把*类声明成protected,也不会存在不同包名的子类了,因为不同包名无法继承protected类
在Kotlin中也是一样的,protected修饰符也不能修饰*类,只能修饰内部类。
在Kotlin中,同包名不能访问protected类,如果想要继承protected类,需要他们在同一个内部类下,如下所示:
open class ProtectedClassTest {
protected open class ProtectedClass {
open fun getName(): String {
return "chiclaim"
}
}
protected class ProtectedClassExtend : ProtectedClass() {
override fun getName(): String {
return "yuzhiqiang"
}
}
}
除了在同一内部类下,可以继承protected类外,如果某个类的外部类和protected类的外部类有继承关系,这样也可以继承protected类
class ExtendKotlinProtectedClass2 : ProtectedClassTest() {
private var protectedClass: ProtectedClass? = null
//继承protected class
protected class A : ProtectedClass() {
}
}
需要注意的是,继承protected类,那么子类也必须是protected,这一点和internal是类似的。Kotlin中不能放大访问权限,能缩小访问权限吗?答案是可以的。
可能有人会问,既然同包名都不能访问protected类,那么这个类跟私有的有什么区别?确实如果外部类没有声明成open,编译器也会提醒我们此时的protected就是private
所以在Kotlin中,如果要使用protected类,需要把外部声明成可继承的(open),如:
//继承ProtectedClassTest
class ExtendKotlinProtectedClass2 : ProtectedClassTest() {
//可以使用ProtectedClassTest中的protected类了
private var protectedClass: ProtectedClass? = null
}
2) protected修饰类成员
如果protected修饰类成员,在Java中可以被同包名或子类可访问;在Kotlin中只能被子类访问。
这个比较简单就不赘述了
3) 访问修饰符小结
- 如果不写访问修饰符,在Java中是default修饰符(package-private);在Kotlin中是public的
- internal访问修饰符是Kotlin独有,只能在模块内能访问的到
- protected修饰类的时候,不管是java和kotlin都只能放到内部类上
- 在Kotlin中,要继承protected类,要么子类在同一内部类名下;要么该类的的外部类和protected类的外部类有继承关系
- 在Kotlin中,继承protected类,子类也必须是protected的
- 在Kotlin中,对于protected修饰符,去掉了同包名能访问的特性
- 如果某个Kotlin类能够被继承,需要open关键字,默认是final的
虽然Kotlin的数据类型和访问修饰符比较简单,还是希望大家能够动手写些demo验证下,这样可能会有意想不到的收获。你也可以访问我的github上面有比较详细的测试demo,有需要的可以看下。
Kotlin中的Class和Interface
Kotlin中声明类的几种方式
在实际的开发当中,经常需要去新建类。在Kotlin中有如下几种声明类的方式:
1) class className
这种方式和Java类似,通过class关键字来声明一个类。不同的是,这个类是public final的,不能被继承。
class Person
编译后:
public final class Person {
}
2) class className([car/val] property: Type…)
这种方式和上面一种方式多加了一组括号,代表构造函数,我们把这样的构造函数称之为primary constructor。这种方式声明一个类的主要做了一下几件事:
- 会生成一个构造方法,参数就是括号里的那些参数
- 会根据括号的参数生成对应的属性
- 会根据val和var关键字来生成setter、getter方法
var和val关键字:var表示该属性可以被修改;val表示该属性不能被修改
class Person(val name: String) //name属性不可修改
---编译后---
public final class Person {
//1. 生成name属性
@NotNull
private final String name;
//2. 生成getter方法
//由于name属性不可修改,所以不提供name的setter方法
@NotNull
public final String getName() {
return this.name;
}
//3. 生成构造函数
public Person(@NotNull String name) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
}
}
如果我们把 name修饰符改成var,编译后会生成getter和setter方法,同时也不会有final关键字来修饰name属性
如果这个name不用 var 也不用val修饰, 那么不会生成属性,自然也不会生成 getter 和 setter方法。不过可以在 init代码块
里进行初始化, 否则没有什么意义。
class Person(name: String) {
//会生成getter和setter方法
var name :String? =null
//init代码块会在构造方法里执行
init {
this.name = name
}
}
----编译后
public final class Person {
@Nullable
private String name;
@Nullable
public final String getName() {
return this.name;
}
public final void setName(@Nullable String var1) {
this.name = var1;
}
public Person(@NotNull String name) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
}
}
从上面的代码可知,init代码块
的执行时机是构造函数被调用的时候,编译器会把init代码块里的代码copy到构造函数里。
如果有多个构造函数,那么每个构造函数里都会有init代码块的代码,但是如果构造函数里调用了另一个重载的构造函数,init代码块只会被包含在被调用的那个构造函数里。
说白了,构造对象的时候,init代码块里的逻辑只有可能被执行一次。
3) class className constructor([var/val] property: Type…)
该种方式和上面是等价的,只是多加了constructor关键字而已
4) 类似Java的方式声明构造函数
不在类名后直接声明构造函数 ,在类的里面再声明构造函数。我们把这样的构造函数称之为 secondary constructor
class Person {
var name: String? = null
var id: Int = 0
constructor(name: String) {
this.name = name
}
constructor(id: Int) {
this.id = id
}
}
primary constructor 里的参数是可以被var/val修饰,而 secondary constructor 里的参数是不能被var/val修饰的
secondary constructor 用的比较少,用得最多的还是 primary constructor
5) data class className([var/val] property: Type)
新建bean类的时候,常常需要声明equals、hashCode、toString等方法,我们需要写很多代码。在Kotlin中,只需要在声明类的时候前面加data关键字就可以完成这些功能。
节省了很多代码篇幅。需要注意的是,那么哪些属性参与equals、hashCode、toString方法呢?
primary constructor构造函数里的参数,都会参与equals、hashCode、toString方法里。
这个也比较简单,大家可以利用kotlin插件,查看下反编译后的代码即可。由于篇幅原因,在这里就不贴出来了。
6) object className
这种方法声明的类是一个单例类,以前在Java中新建一个单例类,需要写一些模板代码,在Kotlin中一行代码就可以了(类名前加上object
关键字)
在Kotlin中object关键字有很多用法,等介绍完了kotlin新建类方式后,单独汇总下object关键字的用法。
7) kotlin 新建内部类
在Kotlin中内部类默认是静态的(Java与此相反),不持有外部类的引用,如:
class OuterClass {
//在Kotlin中内部类默认是静态的,不持有外部类的引用
class InnerStaticClass{
}
//如果要声明非静态的内部类,需要加上inner关键字
inner class InnerClass{
}
}
编译后代码如下:
class OuterClass {
public static final class InnerStaticClass {
}
public final class InnerClass {
}
}
8) sealed class className
当我们使用when语句通常需要加else分支,如果添加了新的类型分支,忘记了在when语句里进行处理,遇到新分支,when语句就会走else逻辑
sealed class就是用来解决这个问题的。如果有新的类型分支且没有处理编译器就会报错。
当when判断的是sealed class,那么不需要加else默认分支,如果有新的子类,编译器会通过编译报错的方式提醒开发者添加新分支,从而保证逻辑的完整性和正确性
需要注意的是,当when判断的是sealed class,千万不要添加else分支,否则有新类编译器也不会提醒
sealed class 实际上是一个抽象类且不能被继承,构造方法是私有的。
object 关键字用法汇总
除了上面我们介绍的,object关键字定义单例类外,object关键字还有以下几种用法:
1) companion object
我把它称之为友元对象,C++也有类似的概念。companion object 就是不用对象就能调用里面的方法
companion object 需要定义在一个类的内部,里面的成员都是静态的。如下所示:
class ObjectKeywordTest {
//友元对象
companion object {
}
}
需要注意的是,在友元体里面不同定义的方式有不同的效果,虽然他们都是静态的:
companion object {
//公有常量
const val FEMALE: Int = 0
const val MALE: Int = 1
//私有常量
val GENDER: Int = FEMALE
//私有静态变量
var username: String = "chiclaim"
//静态方法
fun run() {
println("run...")
}
}
- 如果使用val来定义,而没有使用const 那么该属性是一个私有常量
- 如果使用const和val来定义则是一个公共常量
- 如果使用var来定义,则是一个静态变量
虽然只是一个关键字的差别,但是最终编译出的结果还是有细微的差别的,在开发中注意下就可以了。
为什么叫友元对象,我们来看下上面代码编译之后对应的Java代码:
class ObjectKeywordTest {
//公有常量
public static final int FEMALE = 0;
public static final int MALE = 1;
//私有常量
private static final int gender = 1;
//静态变量
@NotNull
private static String username = "chiclaim";
public static final ObjectKeywordTest.Companion Companion = new ObjectKeywordTest.Companion((DefaultConstructorMarker)null);
public static final class Companion {
public final void run() {
String var1 = "run...";
System.out.println(var1);
}
public final int getGENDER() {
return ObjectKeywordTest.GENDER;
}
@NotNull
public final String getUsername() {
return ObjectKeywordTest.username;
}
public final void setUsername(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
ObjectKeywordTest.username = var1;
}
private Companion() {
}
}
}
我们发现会生成一个名为Companion的内部类,如果友元体里是方法,则该方法定义在该内部类中,如果是属性则定义在外部类里。如果是私有变量在内部类中生成getter方法。
同时还会在外部声明一个名为Companion的内部类对象,用来访问这些静态成员。友元对象的默认名字叫做Companion,你也可以给它起一个名字,格式为:
companion object YourName{
}
除了给这个友元对象起一个名字,还可以让其实现接口,如:
class ObjectKeywordTest4 {
//实现一个接口
companion object : IAnimal {
override fun eat() {
println("eating apple")
}
}
}
fun feed(animal: IAnimal) {
animal.eat()
}
fun main(args: Array<String>) {
//把类名当作参数直接传递
//实际传递的是静态对象 ObjectKeywordTest4.Companion
//每个类只会有一个友元对象
feed(ObjectKeywordTest4)
}
2) object : className 创建匿名内部类对象
如下面的例子,创建一个MouseAdapter内部类对象:
jLabel.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
super.mouseClicked(e)
println("mouseClicked")
}
override fun mouseMoved(e: MouseEvent?) {
super.mouseMoved(e)
println("mouseMoved")
}
})
至此,object关键字有3种用法:
- 定义单例类,格式为:object className
- 定义友元对象,格式为:companion object
- 创建匿名内部类对象,格式为:object : className
Kotlin中的Interface
我们都知道,在Java8之前,Interface中是不能包含有方法体的方法和属性,只能包含抽象方法和常量。
在Kotlin中的接口在定义的时候可以包含有方法体的方法,也可以包含属性。
//声明一个接口,包含方法体的方法(plus)和一个属性(count)
interface InterfaceTest {
var count: Int
fun plus(num: Int) {
count += num
}
}
//实现该接口
class Impl : InterfaceTest {
//必须要覆盖count属性
override var count: Int = 0
}
我们来看下底层Kotlin接口是如何做到在接口中包含有方法体的方法、属性的。
public interface InterfaceTest {
//会为我们生成三个抽象方法:属性的getter和setter方法、plus方法
int getCount();
void setCount(int var1);
void plus(int var1);
//定义一个内部类,用于存放有方法体的方法
public static final class DefaultImpls {
public static void plus(InterfaceTest $this, int num) {
$this.setCount($this.getCount() + num);
}
}
}
//实现我们上面定义的接口
public final class Impl implements InterfaceTest {
private int count;
public int getCount() {
return this.count;
}
public void setCount(int var1) {
this.count = var1;
}
//Kotlin会自动为我们生成plus方法,方法体就是上面内部类封装好的plus方法
public void plus(int num) {
InterfaceTest.DefaultImpls.plus(this, num);
}
}
通过反编译,Kotlin接口里可以定义有方法体的方法也没有什么好神奇的。
就是通过内部类封装好了带有方法体的方法,然后实现类会自动生成方法
这个特性还是挺有用的,当我们不想是使用抽象类时,具有该特性的interface就派上用场了
lambda表达式
在Java8之前,lambda表达式在Java中都是没有的,下面我们来简单的体验一下lambda表达式:
//在Android中为按钮设置点击事件
button.setOnClickListener(new View.OnClickListener(){
@override
public void onClick(View v){
//todo something
}
});
//在Kotlin中使用lambda
button.setOnClickListener{view ->
//todo something
}
可以发现使用lambda表达式,代码变得非常简洁。下面我们就来深入探讨下lambda表达式。
什么是lambda表达式
我们先从lambda最基本的语法开始,引用一段Kotlin in Action中对lambda的定义:
总的来说,主要有3点:
- lambda总是放在一个花括号里 ({})
- 箭头左边是lambda参数 (lambda parameter)
- 箭头右边是lambda体 (lambda body)
我们再来看上面简单的lambda实例:
button.setOnClickListener{view -> //view是lambda参数
//lambda体
//todo something
}
lambda表达式与Java的functional interface
上面的 OnClickListener
接口和Button类是定义在Java中的。
该接口只有一个抽象方法,在Java中这样的接口被称作 functional interface
或 SAM
(single abstract method)
因为我们在实际的工作中可能和Java定义的API打的交道最多了,因为Java这么多年的生态,我们无处不再使用Java库,
所以在Kotlin中,如果某个方法的参数是Java定义的functional interface,Kotlin支持把lambda当作参数进行传递的。
需要注意的是,Kotlin这样做是指方便的和Java代码进行交互。但是如果在Kotlin中定义一个方法,它的参数类型是functional interface,是不允许直接将lambda当作参数进行传递的。如:
//在Kotlin中定义一个方法,参数类型是Java中的Runnable
//Runnable是一个functional interface
fun postDelay(runnable: Runnable) {
runnable.run()
}
//把lambda当作参数传递是不允许的
postDelay{
println("postDelay")
}
在Kotlin中调用Java方法,能够将lambda当作参数传递,需要满足两个条件:
- 该Java方法的参数类型是 functional interface (只有一个抽象方法)
- 该functional interface是Java定义的,如果是Kotlin定义的,就算该接口只有一个抽象方法,也是不行的
如果kotlin定义了方法想要像上面一样,把lambda当做参数传递,可以使用高阶函数。这个后面会介绍。
Kotlin允许lambda当作参数传递,底层也是通过构建匿名内部类来实现的:
fun main(args: Array<String>) {
val button = Button()
button.setOnClickListener {
println("click 1")
}
button.setOnClickListener {
println("click 2")
}
}
//编译后对应的Java代码:
public final class FunctionalInterfaceTestKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Button button = new Button();
button.setOnClickListener((OnClickListener)null.INSTANCE);
button.setOnClickListener((OnClickListener)null.INSTANCE);
}
}
发现反编译后对应的Java代码有的地方可读性也不好,这是Kotlin插件的bug,比如 (OnClickListener)null.INSTANCE
所以这个时候需要看下它的class字节码:
//内部类1
final class lambda/FunctionalInterfaceTestKt$main$1 implements lambda/Button$OnClickListener{
public final static Llambda/FunctionalInterfaceTestKt$main$1; INSTANCE
//...
}
//内部类2
final class lambda/FunctionalInterfaceTestKt$main$2 implements lambda/Button$OnClickListener{
public final static Llambda/FunctionalInterfaceTestKt$main$2; INSTANCE
//...
}
//main函数
// access flags 0x19
public final static main([Ljava/lang/String;)V
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 0
LDC "args"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 10 L1
NEW lambda/Button
DUP
INVOKESPECIAL lambda/Button.<init> ()V
ASTORE 1
L2
LINENUMBER 11 L2
ALOAD 1
GETSTATIC lambda/FunctionalInterfaceTestKt$main$1.INSTANCE : Llambda/FunctionalInterfaceTestKt$main$1;
CHECKCAST lambda/Button$OnClickListener
INVOKEVIRTUAL lambda/Button.setOnClickListener (Llambda/Button$OnClickListener;)V
从中可以看出,它会新建2个内部类,内部类会暴露一个INSTANCE实例供外界使用。
也就是说传递lambda参数多少次,就会生成多少个内部类
但是不管这个main方法调用多少次,一个setOnClickListener,都只会有一个内部类对象,因为暴露出来的INSTANCE是一个常量
我们再来调整一下lambda体内的实现方式:
fun main(args: Array<String>) {
val button = Button()
var count = 0
button.setOnClickListener {
println("click ${++count}")
}
button.setOnClickListener {
println("click ${++count}")
}
}
也就是lambda体里面使用了外部变量了,再来看下反编译后的Java代码:
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Button button = new Button();
final IntRef count = new IntRef();
count.element = 0;
button.setOnClickListener((OnClickListener)(new OnClickListener() {
public final void click() {
StringBuilder var10000 = (new StringBuilder()).append("click ");
IntRef var10001 = count;
++count.element;
String var1 = var10000.append(var10001.element).toString();
System.out.println(var1);
}
}));
button.setOnClickListener((OnClickListener)(new OnClickListener() {
public final void click() {
StringBuilder var10000 = (new StringBuilder()).append("click ");
IntRef var10001 = count;
++count.element;
String var1 = var10000.append(var10001.element).toString();
System.out.println(var1);
}
}));
}
从中发现,每次调用setOnClickListener方法的时候都会new一个新的内部类对象
由此,我们做一个小结:
- 一个lambda对应一个内部类
- 如果lambda体里没有使用外部变量,则调用方法时只会有一个内部类对象
- 如果lambda体里使用了外部变量,则每调用一次该方法都会新建一个内部类对象
lambda表达式赋值给变量
lambda除了可以当作参数进行传递,还可以吧lambda赋值给一个变量:
//定义一个lambda,赋值给一个变量
val sum = { x: Int, y: Int, z: Int ->
x + y + z
}
fun main(args: Array<String>) {
//像调用方法一样调用lambda
println(sum(12, 10, 15))
}
//控制台输出:37
接下来分析来其实现原理,反编译查看其对应的Java代码:
public final class LambdaToVariableTestKt {
@NotNull
private static final Function3 sum;
@NotNull
public static final Function3 getSum() {
return sum;
}
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
int var1 = ((Number)sum.invoke(12, 10, 15)).intValue();
System.out.println(var1);
}
static {
sum = (Function3)null.INSTANCE;
}
}
其对应的Java代码是看不到具体的细节的,而且还是会有null.INSTANCE
的情况,但是我们还是可以看到主体逻辑。
但由于class字节篇幅很大,就不贴出来了,通过我们上面的分析,INSTANCE
是一个常量,在这里也是这样的:
首先会新建一个内部类,该内部类实现了接口kotlin/jvm/functions/Function3
,为什么是Function3
因为我们定义的lambda只有3个参数。
所以lambda有几个参数对应的Function几,最多支持22个参数,也就是Function22。我们把这类接口称之为FunctionN
。
然后内部类实现了接口的invoke方法,lambda体的代码就是invoke方法体。
这个内部类会暴露一个实例常量INSTANCE
,供外界使用。
如果把上面kotlin的代码放到一个类里,然后在lambda体里使用外部的变量,那么每调用一次sum也会创建一个新的内部类对象,上面我们对lambda的小结在这里依然是有效的。
上面setOnClickListener的例子,我们传了两个lambda参数,生成了两个内部类,我们也可以把监听事件的lambda赋值给一个变量:
val button = Button()
val listener = Button.OnClickListener {
println("click event")
}
button.setOnClickListener(listener)
button.setOnClickListener(listener)
这样对于OnClickListener接口,只会有一个内部类。
从这个例子中我们发现,className{}
这样的格式也能创建一个对象,这是因为接口OnClickListener
是SAM interface,只有一个抽象函数的接口。
编译器会生成一个SAM constructor,这样便于把一个lambda表达式转化成一个functional interface实例对象。
至此,我们又学到了另一种创建对象的方法。
做一个小结,在Kotlin中常规的创建对象的方式(除了反射、序列化等):
- 类名后面接括号,格式:className()
- 创建内部类对象,格式:object : className
- SAM constructor方式,格式:className{}
高阶函数
由于高阶函数和lambda表达式联系比较紧密,而且不介绍高阶函数,lambda有些内容无法讲,所以在高阶函数这部分,还继续分析lambda表达式。
高阶函数的定义
如果某个函数是以另一个函数作为参数或者返回值是一个函数,我们把这样的函数称之为高阶函数
比如 kotlin库里的filter函数就是一个高阶函数:
//Kotlin library filter function
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>
//调用高阶函数filter,直接传递lambda表达式
list.filter { person ->
person.age > 18
}
filter函数定义部分predicate: (T) -> Boolean
格式有点像lambda,感觉又不是,但是传参的时候又可以传递lambda表达式。
弄清这个问题之前,我们先来介绍下 function type
,function type 格式如下:
名称 : (参数) -> 返回值类型
- 冒号左边是函数类型的名字
- 冒号右边是参数
- 尖括号右边是返回值
比如:predicate: (T) -> Boolean
predicate就是名字,T泛型就是参数,Boolean就是返回值类型
高阶函数是以另一个函数作为参数或者其返回值是一个函数,也可以说高阶函数是参数是function type或者返回值是function type
在调用高阶函数的时候,我们可以传递lambda,这是因为编译器会把lambda推导成function type
高阶函数原理分析
我们定义一个高阶函数到底定义了什么?我们先来定义一个简单的高阶函数:
fun process(x: Int, y: Int, operate: (Int, Int) -> Int) {
println(operate(x, y))
}
编译后代码如下:
public static final void process(int x, int y, @NotNull Function2 operate) {
Intrinsics.checkParameterIsNotNull(operate, "operate");
int var3 = ((Number)operate.invoke(x, y)).intValue();
System.out.println(var3);
}
我们又看到了FunctionN
接口了,上面介绍把lambda赋值给一个变量的时候讲到了FunctionN
接口
发现高阶函数的function type
编译后也会变成FunctionN
,所以能把lambda作为参数传递给高阶函数也是情理之中了
这个高阶函数编译后的情况,我们再来看下调用高阶函数的情况:
//调用高阶函数,传递一个lambda作为参数
process(a, b) { x, y ->
x * y
}
//编译后的字节码:
GETSTATIC higher_order_function/HigherOrderFuncKt$main$1.INSTANCE : Lhigher_order_function/HigherOrderFuncKt$main$1;
CHECKCAST kotlin/jvm/functions/Function2
INVOKESTATIC higher_order_function/HigherOrderFuncKt.process (IILkotlin/jvm/functions/Function2;)V
发现会生成一个内部类,然后获取该内部类实例,这个内部类实现了FunctionN。介绍lambda的时候,我们说过了lambda会编译成FunctionN
如果lambda体里使用了外部变量,那每次调用都会创建一个内部类实例,而不是INSTANCE
常量,这个也在介绍lambda的时候说过了。
再探lambda表达式
lambda表达式参数和function type参数
除了filter,还有常用的forEach也是高阶函数:
//list里是Person集合
//遍历list集合
list.forEach {person ->
println(person.name)
}
我们调用forEach函数的传递lambda表达式,lambda表达式的参数是person,那为什么参数类型是集合里的元素Person,而不是其他类型呢?比是集合类型?
到底是什么决定了我们调用高阶函数时传递的lambda表达式的参数是什么类型呢?
我们来看下forEach源码:
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
发现里面对集合进行for循环,然后把集合元素作为参数传递给 action (function type)
所以,调用高阶函数时,lambda参数是由function type的参数决定的
lambda receiver
我们再看下kotlin高阶函数apply
,它也是一个高阶函数,调用该函数时lambda参数是调用者本身this
:
list.apply {//lambda参数是this,也就是List
println(this)
}
我们看下apply函数的定义:
public inline fun <T> T.apply(block: T.() -> Unit): T
发现apply函数的的 function type
有点不一样,block: T.() -> Unit
在括号前面有个 T.
调用这样的高阶函数时,lambda参数是 this
,我们把这个this称之为 lambda receiver
把这类lambda称之为带有接受者的lambda表达式(lambda with receiver
)
这样的lambda在编写代码的时候提供了很多便利,调用所有关于 this
对象的方法 ,都不需要 this.
,直接写方法即可,如下面的属于StringBuilder的append方法:
除了apply,函数with、run的lambda参数都是this
:
public inline fun <T> T.apply(block: T.() -> Unit): T
public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T, R> with(receiver: T, block: T.() -> R): R
它们三者都能完成彼此的功能:
//apply
fun alphabet2() = StringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}
//with
fun alphabet() = with(StringBuilder()) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know alphabet!").toString()
}
//run
fun alphabet3() = StringBuilder().run {
for (c in 'A'..'Z') {
append(c)
}
append("\nNow I know the alphabet!")
}
高阶函数let、with、apply、run总结
1) let函数一般用于判断是否为空
//let函数的定义
public inline fun <T, R> T.let(block: (T) -> R): R {
return block(this)
}
//let的使用
message?.let { //lambda参数it是message
val result = it.substring(1)
println(result)
}
2) with是全局函数,apply是扩展函数,其他的都一样
3) run函数的lambda是一个带有接受者的lambda,而let不是,除此之外功能差不多
public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T, R> T.let(block: (T) -> R): R
所以let能用于空判断,run也可以:
高阶函数的优化
通过上面我们对高阶函数原理的分析:在调用高阶函数的时候 ,会生成一个内部类。
如果这个高阶函数被程序中很多地方调用了,那么就会有很多的内部类,那么程序员的体积就会变得不可控了。
而且如果调用高阶函数的时候,lambda体里使用了外部变量,则会每次创建新的对象。
所以需要对高阶函数进行优化下。
上面我们在介绍kotlin内置的一些的高阶函数如let、run、with、apply,它们都是内联函数,使用inline
关键字修饰
内联inline是什么意思呢?就是在调用inline函数的地方,编译器在编译的时候会把内联函数的逻辑拷贝到调用的地方。
依然以在介绍高阶函数原理那节介绍的process函数为例:
//使用inline修饰高阶函数
inline fun process(x: Int, y: Int, operate: (Int, Int) -> Int) {
println(operate(x, y))
}
fun main(args: Array<String>) {
val a = 11
val b = 2
//调用inline的高阶函数
process(a, b) { x, y ->
x * y
}
}
//编译后对应的Java代码:
public static final void main(@NotNull String[] args) {
int a = 11;
int b = 2;
int var4 = a * b;
System.out.println(var4);
}
Kotlin泛型
要想掌握Kotlin泛型,需要对Java的泛型有充分的理解。掌握Java泛型后 ,Kotlin的泛型就很简单了。
所以我们先来看下Java泛型相关的知识点:
Java泛型:不变性(invariance)、协变性(covariance)、逆变性(contravariance)
我们先定义两个类:Plate、Food、Fruit
//定义一个`盘子`类
public class Plate<T> {
private T item;
public Plate(T t) {
item = t;
}
public void set(T t) {
item = t;
}
public T get() {
return item;
}
}
//食物
public class Food {
}
//水果类
public class Fruit extends Food {
}
然后定义一个takeFruit()方法
private static void takeFruit(Plate<Fruit> plate) {
}
然后调用takeFruit方法,把一个装着苹果的盘子传进去:
takeFruit(new Plate<Apple>(new Apple())); //泛型之不变
发现编译器报错,发现装着苹果的盘子竟然不能赋值给装着水果的盘子,这就是泛型的不变性(invariance
)
这个时候就要引出泛型的协变性
了
1) 协变性
假设我就要把一个装着苹果的盘子赋值给一个装着水果的盘子呢?
我们来修改下takeFruit方法的参数(? extends Fruit
):
private static void takeFruit(Plate<? extends Fruit> plate) {
}
然后调用takeFruit方法,把一个装着苹果的盘子传进去:
takeFruit(new Plate<Apple>(new Apple())); //泛型的协变
这个时候编译器不报错了,而且你不仅可以把装着苹果的盘子放进去,还可以把任何继承了Fruit
类的水果都能放进去:
//包括自己本身Fruit也可以放进去
takeFruit(new Plate<Fruit>(new Fruit()));
takeFruit(new Plate<Apple>(new Apple()));
takeFruit(new Plate<Pear>(new Pear()));
takeFruit(new Plate<Banana>(new Banana()));
在Java中把? extends Type
类似这样的泛型,称之为上界通配符(Upper Bounds Wildcards)
为什么叫上界通配符?因为Plate<? extends Fruit>
,可以存放Fruit和它的子类们,最高到Fruit类为止。所以叫上界通配符
好,现在编译器不报错了,我们来看下takeFruit方法体里的一些细节:
private static void takeFruit(Plate<? extends Fruit> plate) {
//plate5.set(new Fruit()); //编译报错
//plate5.set(new Apple()); //编译报错
Fruit fruit = plate5.get(); //编译正常
}
发现takeFruit()
的参数plate的set方法不能使用了,只有get方法可以使用。如果我们需要调用set方法呢?
这个时候就需要引入泛型的逆变性
了
2) 逆变性
修改下泛型的形式(extends
改成super
):
private static void takeFruit(Plate<? super Fruit> plate){
plate.set(new Apple()); //编译正常
//Fruit fruit = plate.get(); //编译报错
//Fruit pear = plate.get(); //编译报错
}
发现set方法可以用了,但是get方法失效了。我们把类似? super Type
这样的泛型,称之为下界通配符(Lower Bounds Wildcards)
在介绍上界通配符(extends)的时候,我们知道上界通配符的泛型可以存放该类型的和它的子类们
。
那么,下界通配符(super)顾名思义就是能存放该类型和它的父类们
。所以对于 Plate<? super Fruit>
只能放进 Fruit 和 Food。
我们在回到刚刚说到的set和get方法:set方法的参数是该泛型;get方法的返回值是该泛型
也就是说上界通配符(extends),只允许获取(get),不允许修改(set)。可以理解为只生产(返回给别人用),不消费。
下界通配符(super),只允许修改(set),不允许获取(get)。可以理解为只消费(set方法传进来的参数可以使用了),不生产。
可以总结为:PECS(Producer Extends, Consumer Super)
3) 泛型小结
- 上界通配符的泛型可以存放
该类型的和它的子类们
,下界通配符能存放该类型和它的父类们
- PECS(Producer Extends, Consumer Super)
上界通配符一般用于读取,下界通配符一般用于修改。比如Java中Collections.java的copy方法:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
dest参数只用于修改
,src参数用于读取
操作,只读(read-only)
通过泛型的协变逆变来控制集合是只读
,还是只改
。使得程序代码更加优雅。
Kotlin泛型的协变、逆变
掌握了Java的泛型,Kotlin泛型就简单很多了,大体上是一致的,但还有一些区别。我们挨个的来介绍下:
1) Kotlin 协变
关于泛型的不变性
,Kotlin和Java都是一致的。比如 List<Apple>
不能赋值给 List<Fruit>
。
我们来看下Kotlin协变:
fun takeFruit(fruits: List<Fruit>) {
}
fun main(args: Array<String>) {
val apples: List<Apple> = listOf(Apple(), Apple())
takeFruit(apples)
}
编译器不会报错,为什么可以把List<Apple>
赋值给List<Fruit>
,根据泛型不变性 ,应该会报错的。
不报错的原因是这里的List不是java.util.List而是Kotlin里的List:
//kotlin Collection
public interface List<out E> : Collection<E>
//Java Collection
public interface List<E> extends Collection<E>
发现Kotlin的List泛型多了out关键字,这里的out关键相当于java的extends通配符
所以不仅可以把List<Apple>
赋值给List<Fruit>
,Fruit的子类都可以:
fun main(args: Array<String>) {
val foods: List<Food> = listOf(Food(), Food())
val fruits: List<Fruit> = listOf(Fruit(), Fruit())
val apples: List<Apple> = listOf(Apple(), Apple())
val pears: List<Pear> = listOf(Pear(), Pear())
//takeFruit(foods) 编译报错
takeFruit(fruits)
takeFruit(apples)
takeFruit(pears)
}
2) Kotlin逆变
out
关键字对应Java中的 extends
关键字,那么java的 super
关键字对应kotlin的 in
关键字
关于逆变Kotlin中的排序函数 sortedWith
,就用到了in关键字:
public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T>
//声明3个比较器
val foodComparator = Comparator<Food> { e1, e2 ->
e1.hashCode() - e2.hashCode()
}
val fruitComparator = Comparator<Fruit> { e1, e2 ->
e1.hashCode() - e2.hashCode()
}
val appleComparator = Comparator<Apple> { e1, e2 ->
e1.hashCode() - e2.hashCode()
}
//然后声明一个集合
val list = listOf(Fruit(), Fruit(), Fruit(), Fruit())
//Comparator声明成了逆变(contravariant),这和Java的泛型通配符super一样的
//所以只能传递Fruit以及Fruit父类的Comparator
list.sortedWith(foodComparator)
list.sortedWith(fruitComparator)
//list.sortedWith(appleComparator) 编译报错
3) Kotlin和Java在协变性、逆变性的异同点
Java中的上界通配符extends和下界通配符super,这两个关键字非常形象
extends表示 只要继承
了这个类包括其本身都能存放
super表示 只要是这个类的父类
包括其本身都能存放
同样的Kotlin中out和in关键字也很想象,这个怎么说呢?
在介绍Java泛型的时候说过,上界通配符extends只能get(后者只能做出参,这就是out),不能set(意思就是不能参数传进来)。所以只能出参(out)
下界通配符super只能set(意思就是可以入参,这就是in),不能get。所以只能入参(in)
Kotlin和Java只是站在不同的角度来看这个问题而已。可能kotlin的in和out更加简单明了,不用再记什么PECS(Producer Extends, Consumer Super)
缩写了
除了关键字不一样,另一方面,Java和Kotlin关于泛型定义的地方也不一样。
在介绍Java泛型的时候,我们定义通配符的时候都是在方法上,比如:
void takeExtendsFruit(Plate<? extends Fruit> plate)
虽然Java支持在类上使用? extends Type
,但是不支持? super Type
,并且在类上定义了? extends Type
,对该类的方法是起不到只读、只写
约束作用的。
我们把Java上的泛型变异称之为:use-site variance
,意思就是在用到的地方定义变异
在Kotlin中,不仅支持在用到的地方定义变异,还支持在定义类的时候声明泛型变异(declaration-site variance
)
比如上面的排序方法sortedWith
就是一个use-site variance
:
public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T>
再比如kotlin list,它就是declaration-site variance
,它在声明List类的时候,定义了泛型协变
这个时候会对该List类的方法产生约束:泛型不能当做方法入参,只能当做出参,如:
public interface List<out E> : Collection<E> {
public operator fun get(index: Int): E
public fun listIterator(): ListIterator<E>
public fun listIterator(index: Int): ListIterator<E>
public fun subList(fromIndex: Int, toIndex: Int): List<E>
public fun indexOf(element: @UnsafeVariance E): Int
//省略其他代码
}
比如get、subList等方法泛型都是作为出参返回值的,我们也发现indexOf方法的参数竟然是泛型E,不是说只能当做出参,不能是入参吗?
这里只是为了兼容java的list的api,所以加上了注解@UnsafeVariance
(不安全的协变),编译器就不会报错了。
例如我们自己定义一个MyList接口,不加@UnsafeVariance
编译器就会报错了:
Kotlin泛型擦除和具体化
Kotlin和Java的泛型只在编译时有效,运行时会被擦除(type erasure)。例如下面的代码就会报错:
//Error: Cannot check for instance of erased type: T
//fun <T> isType(value: Any) = value is T
Kotlin提供了一种泛型具体化的技术,它的原理是这样的:
我们知道泛型在运行时会擦除,但是在inline函数中我们可以指定泛型不被擦除,
因为inline函数在编译期会copy到调用它的方法里,所以编译器会知道当前的方法中泛型对应的具体类型是什么,
然后把泛型替换为具体类型,从而达到不被擦除的目的,在inline函数中我们可以通过reified关键字来标记这个泛型在编译时替换成具体类型
如下面的代码就不会报错了:
inline fun <reified T> isType(value: Any) = value is T
泛型具体化的应用案例
我们在开发中,常常需要把json字符串解析成java bean对象,但是我们不是知道json可以解析成什么对象,通常我们通过泛型来做。
但是我们在最底层把这个不知道的类封装成泛型,在具体运行的时候这个泛型又被擦除了,从而达不到代码重用的最大化。
比如下面一段代码,请求网络成功后把json解析(反射)成对象,然后把对象返回给上层使用:
从上面代码可以看出,CancelTakeoutOrderRequest
我们写了5遍.
那么我们对上面的代码进行优化下,上面的代码只要保证Type对象那里使用是具体的类型就能保证反射成功了
把这个wrapCallable方法在包装一层:
再看下优化后的cancelTakeoutOrder
方法,发现CancelTakeoutOrderRequest
需要写2遍:
我们在使用Kotlin的泛型具体换,再来优化下:
因为泛型具体化是一个内联函数,所以需要把requestRemoteSource方法体积变小,所以我们包装一层:
再看下优化后的cancelTakeoutOrder
方法,发现CancelTakeoutOrderRequest
需要写1遍就可以了:
Kotlin集合
Kotlin中的集合底层也是使用Java集合框架那一套。在上层又封装了一层可变集合和不可变集合接口。
声明可变集合
声明不可变集合
通过Kotlin提供的api可以方便的创建各种集合,但是同时需要搞清楚该API创建的集合底层到底是对应Java的哪个集合。
Kotlin集合常用的API
1) all、any、count、find、firstOrNull、groupBy函数
2) filter、map、flatMap、flatten函数
案例分析:list.map(Person::age).filter { it > 18 }
虽然 list.map(Person::age).filter { it > 18 } 代码非常简洁,我们要知道底层做了些什么?反编译代码如下:
发现调用map和filter分别创建了一个集合,也就是整个操作创建了两个2集合。
延迟集合操作之Sequences
根据上面的分析,list.map(Person::age).filter { it > 18 }
会创建两个集合,本来常规操作一个集合就够了,Sequence就是就是为了避免创建多余的集合的问题。
val list = listOf<Person>(Person("chiclaim", 18), Person("yuzhiqiang", 15),
Person("johnny", 27), Person("jackson", 190),
Person("pony", 85))
//把filter函数放置前面,可以有效减少map函数的调用次数
list.asSequence().filter { person ->
println("filter---> ${person.name} : ${person.age}")
person.age > 20
}.map { person ->
println("map----> ${person.name} : ${person.age}")
person.age
}.forEach {
println("---------符合条件的年龄 $it")
}
为了提高效率,我们把filter调用放到了前面,并且加了一些测试输出:
filter---> chiclaim : 18
filter---> yuzhiqiang : 15
filter---> johnny : 27
map----> johnny : 27
---------符合条件的年龄 27
filter---> jackson : 190
map----> jackson : 190
---------符合条件的年龄 190
filter---> pony : 85
map----> pony : 85
---------符合条件的年龄 85
从这个输出日志我们可以总结出Sequence的原理:
集合的元素有序的经过filter操作,如果满足filter条件,再经过map操作。
而不会新建一个集合存放符合filter条件的元素,然后在创建一个集合存放map的元素
Sequence的原理图如下所示:
需要注意的是,如果集合的数量不是特别大,并不建议使用Sequence的方式来进行操作。我们来看下Sequence<T>.map
函数
public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
return TransformingSequence(this, transform)
}
它是一个高阶函数,但是它并没有内联,为啥没有内容因为,它把transform传递给了TransformingSequence,并没有直接使用transform,所以不能内联。
根据上面我们对高阶函数的分析,如果一个高阶函数没有内联,每调用一次该函数都会创建内部类。
除此之外还有一点也需要注意,下面一段代码实际上不会执行:
list.asSequence().filter { person ->
person.age > 20
}.map { person ->
person.age
}
只有用到了该Sequence里的元素才会触发上面的操作,比如后面调用了forEach、toList等操作。
对Sequence做一个小结:
- 如果集合的数据量很大啊,可以使用集合操作的延迟Sequence
- Sequence的filter、map等扩展还是是一个非inline的高阶函数
- 集合的Sequence只有调用forEach、toList等操作,才会触发对集合的操作。有点类似RxJava。
Koltin和Java交互的一些问题
1) Kotlin和Java交互上关于空的问题
例如我们用Kotlin定义了一个接口:
interface UserView {
fun showFriendList(list: List<User>)
}
然后在Java代码里调用了该接口的方法:
public class UserPresenter {
public void getLocalFriendList() {
List<User> friends = getFriendList();
friendView.showFriendList(friends);
}
}
看上去是没什么问题,但是如果getFriendList()方法返回null,那么程序就会出现异常了,因为我们定义UserView的showFriendList方法时规定参数不能为空
如果在运行时传递null进去,程序就会报异常,因为Kotlin会为每个定义不为null的参数加上非空检查:
Intrinsics.checkParameterIsNotNull(list, "list");
而且这样的问题,非常隐蔽,不会再编译时报错,只会在运行时报错。
2) 关于Kotlin基本类型初始化问题
比如我们在某个类里定义了一个Int变量:
private var mFrom:Int
默认mFrom是一个空,而不是像我们在Java中定义的是0
在用的时候可能我们直接判断mFrom是不是0了,这个时候可能就会有问题了。
所以建议,一般基本类型定义为0,特别是定义bean类的时候。这样也不用考虑其为空的情况,也可以利用Kotlin复杂类型的API的便捷性。
3) Kotlin泛型具体化无法被Java调用
如果我们定义了一个inline函数,且使用了泛型具体化,该方法不能被Java调用。反编译后发现该方法是私有的。只能Kotlin代码自己调用。
4) Kotlin间接访问Java default class
这个问题是码上开学分享Kotlin、Jetpack的微信群里成员发现的问题:
class JavaPackagePrivate{
}
public class JavaPublic extends JavaPackagePrivate {
}
public class JavaClassForTestDefault {
public void test(JavaPackagePrivate b){
}
}
然后在Kotlin中调用JavaClassForTestDefault.test方法:
fun main(args: Array<String>) {
JavaClassForTestDefault().test(JavaPublic())
}
Exception in thread "main" java.lang.IllegalAccessError: tried to access class visibility_modifier.modifier_class.JavaPackagePrivate...
在Kotlin看来,test方法的参数类型JavaPackagePrivate是package-private(default),也就是包内可见。Kotlin代码中在其他包内调用该方法,Kotlin就不允许了。
要么Kotlin代码和JavaPackagePrivate包名一样,要么使用Java代码来调用这样的API
总结
本文介绍了关于Kotlin的很多相关的知识点:从Kotlin的基本数据类型、访问修饰符、类和接口、lambda表达式、Kotlin泛型、集合、高阶函数等都做了详细的介绍。
如果掌握这些技术点,在实际的开发中基本上能够满足需要。除此之外,像Kotlin的协程、跨平台等,本文没有涉及,这也是今后重点需要研究的地方。
Kotlin在实际的使用过程中,还是很明显的感觉到编码效率的提升、代码的可读性提高。
可能一行Kotlin代码,可以抵得上以前Java的好几行代码。也不是说代码越少越好,但是我们要知道这几行简短的代码底层在做什么。
这也需要开发者对Kotlin代码底层为我们做了什么有一个比较好的了解。对那些不是很熟悉的API最好反编译下代码,看看到底是怎么实现的。
这样下来,对Kotlin的各种语法糖就不会觉得神秘了,对我们写的Kotlin代码也更加自信。