前几个月,在组内分享了关于Kotlin相关的内容。但由于PPT篇幅的原因,有些内容讲的也不是很详细。另外本人也参与了扔物线组织的码上开学,该社区主要用于分享 Kotlin
和 Jetpack
相关的技术, 如果您对Kotlin或者Jetpack使用上有想要分享的地方,也欢迎您一起来完善该社区,欢迎联系扔物线。
为了方便大家对本文有一个大概的了解,先总的说下本文主要讲Kotlin哪些方面的内容(下面的目录和我在组内分享时PPT目录是类似的):
为什么要讲下Kotlin数据类型和访问修饰符修饰符呢?因为Kotlin的数据类型和访问修饰符和Java的还是有些区别的,所以单独拎出来说一下。
我们知道,在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中的基本类型还是包装类型。下面以整型为例,其他的数据类型同理。
?
),则编译后是包装类型 //因为可以为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,那么编译后时是包装类型,反之就是基本类型。
//集合泛型 //集合里的元素都是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}; } 复制代码
从上面的例子中,关于集合泛型编译后是包装类型在Java中也是一样的。如果想要声明的数组编译后是基本类型的数组,需要使用Kotlin为我们提供的方法:
//会编译成一个int[] fun getAge5(): IntArray { return intArrayOf(170, 180, 190) } 当然,除了intArrayOf,还有charArrayOf、floatArrayOf等等,就不一一列举了。 复制代码
我们都知道,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和Java在修饰类和修饰类成员的异同点:
表格一:类访问修饰符:
类访问修饰符 | Java可访问级别 | Kotlin可访问级别 |
---|---|---|
public | 均可访问 | 均可访问 |
protected | 同包名 | 同包名也不可访问 |
internal | 不支持该修饰符 | 同模块内可见 |
default | 同包名下可访问 | 相当于public |
private | 当前文件可访问 | 当前文件可访问 |
表格二:类成员访问修饰符:
成员修饰符 | Java可访问级别 | Kotlin可访问级别 |
---|---|---|
public | 均可访问 | 均可访问 |
protected | 同包名或子类可访问 | 只有子类可访问 |
internal | 不支持该修饰符 | 同模块内可见 |
default | 同包名下可访问 | 相当于public |
private | 当前文件可访问 | 当前文件可访问 |
通过以上两个表格,有几点需要讲一下。
internal修饰符意思是只能在当前模块访问,出了当前模块不能被访问。
需要注意的是,如果A类是internal修饰,B类继承A类,那么B类也必须是internal的,因为如果kotlin允许B类声明成public的,那么A就间接的可以被其他模块的类访问。
也就是说在Kotlin中,子类不能放大父类的访问权限。类似的思想在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 } 复制代码
如果protected修饰类成员,在Java中可以被同包名或子类可访问;在Kotlin中只能被子类访问。
这个比较简单就不赘述了
虽然Kotlin的数据类型和访问修饰符比较简单,还是希望大家能够动手写些demo验证下,这样可能会有意想不到的收获。你也可以访问我的 github 上面有比较详细的测试demo,有需要的可以看下。
在实际的开发当中,经常需要去新建类。在Kotlin中有如下几种声明类的方式:
这种方式和Java类似,通过class关键字来声明一个类。不同的是,这个类是public final的,不能被继承。
class Person 编译后: public final class Person { } 复制代码
这种方式和上面一种方式多加了一组括号,代表构造函数,我们把这样的构造函数称之为 primary constructor 。这种方式声明一个类的主要做了一下几件事:
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代码块里的逻辑只有可能被执行一次。
该种方式和上面是等价的,只是多加了constructor关键字而已
不在类名后直接声明构造函数 ,在类的里面再声明构造函数。我们把这样的构造函数称之为 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
新建bean类的时候,常常需要声明equals、hashCode、toString等方法,我们需要写很多代码。在Kotlin中,只需要在声明类的时候前面加data关键字就可以完成这些功能。
节省了很多代码篇幅。需要注意的是,那么哪些属性参与equals、hashCode、toString方法呢? primary constructor构造函数里的参数,都会参与equals、hashCode、toString方法里。
这个也比较简单,大家可以利用kotlin插件,查看下反编译后的代码即可。由于篇幅原因,在这里就不贴出来了。
这种方法声明的类是一个单例类,以前在Java中新建一个单例类,需要写一些模板代码,在Kotlin中一行代码就可以了(类名前加上 object
关键字)
在Kotlin中object关键字有很多用法,等介绍完了kotlin新建类方式后,单独汇总下object关键字的用法。
在Kotlin中内部类默认是静态的(Java与此相反),不持有外部类的引用,如:
class OuterClass { //在Kotlin中内部类默认是静态的,不持有外部类的引用 class InnerStaticClass{ } //如果要声明非静态的内部类,需要加上inner关键字 inner class InnerClass{ } } 编译后代码如下: class OuterClass { public static final class InnerStaticClass { } public final class InnerClass { } } 复制代码
当我们使用when语句通常需要加else分支,如果添加了新的类型分支,忘记了在when语句里进行处理,遇到新分支,when语句就会走else逻辑
sealed class就是用来解决这个问题的。如果有新的类型分支且没有处理编译器就会报错。
当when判断的是sealed class,那么不需要加else默认分支,如果有新的子类,编译器会通过编译报错的方式提醒开发者添加新分支,从而保证逻辑的完整性和正确性
需要注意的是,当when判断的是sealed class,千万不要添加else分支,否则有新类编译器也不会提醒
sealed class 实际上是一个抽象类且不能被继承,构造方法是私有的。
除了上面我们介绍的,object关键字定义单例类外,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...") } } 复制代码
虽然只是一个关键字的差别,但是最终编译出的结果还是有细微的差别的,在开发中注意下就可以了。
为什么叫友元对象,我们来看下上面代码编译之后对应的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) } 复制代码
如下面的例子,创建一个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") } }) 复制代码
我们都知道,在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就派上用场了
在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最基本的语法开始,引用一段Kotlin in Action中对lambda的定义:
我们再来看上面简单的lambda实例:
button.setOnClickListener{view -> //view是lambda参数 //lambda体 //todo something } 复制代码
上面的 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定义了方法想要像上面一样,把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,赋值给一个变量 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实例对象。
至此,我们又学到了另一种创建对象的方法。
由于高阶函数和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的时候说过了。
除了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)
我们再看下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函数的定义 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) } 复制代码
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泛型,需要对Java的泛型有充分的理解。掌握Java泛型后 ,Kotlin的泛型就很简单了。
所以我们先来看下Java泛型相关的知识点:
我们先定义两个类: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
)
这个时候就要引出泛型的 协变性
了
假设我就要把一个装着苹果的盘子赋值给一个装着水果的盘子呢?
我们来修改下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方法呢?
这个时候就需要引入泛型的 逆变性
了
修改下泛型的形式( 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)
该类型的和它的子类们
,下界通配符能存放 该类型和它的父类们
上界通配符一般用于读取,下界通配符一般用于修改。比如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)
通过泛型的协变逆变来控制集合是 只读
,还是 只改
。使得程序代码更加优雅。
掌握了Java的泛型,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) } 复制代码
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) 编译报错 复制代码
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和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中的集合底层也是使用Java集合框架那一套。在上层又封装了一层可变集合和不可变集合接口。
通过Kotlin提供的api可以方便的创建各种集合,但是同时需要搞清楚该API创建的集合底层到底是对应Java的哪个集合。
虽然 list.map(Person::age).filter { it > 18 } 代码非常简洁,我们要知道底层做了些什么?反编译代码如下:
发现调用map和filter分别创建了一个集合,也就是整个操作创建了两个2集合。
根据上面的分析, 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等操作。
例如我们用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"); 复制代码
而且这样的问题,非常隐蔽,不会再编译时报错,只会在运行时报错。
比如我们在某个类里定义了一个Int变量:
private var mFrom:Int 复制代码
默认mFrom是一个空,而不是像我们在Java中定义的是0
在用的时候可能我们直接判断mFrom是不是0了,这个时候可能就会有问题了。
所以建议,一般基本类型定义为0,特别是定义bean类的时候。这样也不用考虑其为空的情况,也可以利用Kotlin复杂类型的API的便捷性。
如果我们定义了一个inline函数,且使用了泛型具体化,该方法不能被Java调用。反编译后发现该方法是私有的。只能Kotlin代码自己调用。
这个问题是码上开学分享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代码也更加自信。
最后,《kotlin in action》是可以一读再读的书,每次都会有新的收获。可以根据书中的章节,深入研究其背后相关的东西。