转载

从Java角度深入理解Kotlin

前几个月,在组内分享了关于Kotlin相关的内容。但由于PPT篇幅的原因,有些内容讲的也不是很详细。另外本人也参与了扔物线组织的码上开学,该社区主要用于分享 KotlinJetpack 相关的技术, 如果您对Kotlin或者Jetpack使用上有想要分享的地方,也欢迎您一起来完善该社区,欢迎联系扔物线。

为了方便大家对本文有一个大概的了解,先总的说下本文主要讲Kotlin哪些方面的内容(下面的目录和我在组内分享时PPT目录是类似的):

  1. Kotlin数据类型、访问修饰符
    1. Kotlin和Java数据类型对比
    2. Kotlin和Java访问修饰符对比
  2. Kotlin中的Class和Interface
    1. Kotlin中声明类的几种方式
    2. Kotlin中interface原理分析
  3. lambda表达式
    1. lambda初体验
    2. 定义lambda表达式
    3. Member Reference
    4. 常用函数let、with、run、apply分析
    5. lambda原理分析
  4. 高阶函数
    1. 高阶函数的定义
    2. 高阶函数的原理分析
    3. 高阶函数的优化
  5. Kotlin泛型
    1. Java泛型:不变、协变、逆变
    2. Kotlin中的协变、逆变
    3. Kotlin泛型擦除和具体化
  6. Kotlin集合
    1. kotlin集合创建方式有哪些
    2. kotlin集合的常用的函数
    3. Kotlin集合Sequence原理
  7. Koltin和Java交互的一些问题
  8. 总结

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) 访问修饰符小结

  1. 如果不写访问修饰符,在Java中是default修饰符(package-private);在Kotlin中是public的
  2. internal访问修饰符是Kotlin独有,只能在模块内能访问的到
  3. protected修饰类的时候,不管是java和kotlin都只能放到内部类上
  4. 在Kotlin中,要继承protected类,要么子类在同一内部类名下;要么该类的的外部类和protected类的外部类有继承关系
  5. 在Kotlin中,继承protected类,子类也必须是protected的
  6. 在Kotlin中,对于protected修饰符,去掉了同包名能访问的特性
  7. 如果某个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 。这种方式声明一个类的主要做了一下几件事:

  1. 会生成一个构造方法,参数就是括号里的那些参数
  2. 会根据括号的参数生成对应的属性
  3. 会根据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就是用来解决这个问题的。如果有新的类型分支且没有处理编译器就会报错。

从Java角度深入理解Kotlin

当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...")
    }
}

复制代码
  1. 如果使用val来定义,而没有使用const 那么该属性是一个私有常量
  2. 如果使用const和val来定义则是一个公共常量
  3. 如果使用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种用法:

  1. 定义单例类,格式为:object className
  2. 定义友元对象,格式为:companion object
  3. 创建匿名内部类对象,格式为: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的定义:

从Java角度深入理解Kotlin

总的来说,主要有3点:

  1. lambda总是放在一个花括号里 ({})
  2. 箭头左边是lambda参数 (lambda parameter)
  3. 箭头右边是lambda体 (lambda body)

我们再来看上面简单的lambda实例:

button.setOnClickListener{view -> //view是lambda参数
    //lambda体
    //todo something
}
复制代码

lambda表达式与Java的functional interface

上面的 OnClickListener 接口和Button类是定义在Java中的。

该接口只有一个抽象方法,在Java中这样的接口被称作 functional interfaceSAM (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当作参数传递,需要满足两个条件:

  1. 该Java方法的参数类型是 functional interface (只有一个抽象方法)
  2. 该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一个新的内部类对象

由此,我们做一个小结:

  1. 一个lambda对应一个内部类
  2. 如果lambda体里没有使用外部变量,则调用方法时只会有一个内部类对象
  3. 如果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中常规的创建对象的方式(除了反射、序列化等):

  1. 类名后面接括号,格式:className()
  2. 创建内部类对象,格式:object : className
  3. 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 格式如下:

名称 : (参数) -> 返回值类型

  1. 冒号左边是函数类型的名字
  2. 冒号右边是参数
  3. 尖括号右边是返回值

比如: 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方法:

从Java角度深入理解Kotlin

除了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也可以:

从Java角度深入理解Kotlin

高阶函数的优化

通过上面我们对高阶函数原理的分析:在调用高阶函数的时候 ,会生成一个内部类。

如果这个高阶函数被程序中很多地方调用了,那么就会有很多的内部类,那么程序员的体积就会变得不可控了。

而且如果调用高阶函数的时候,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) 泛型小结

  1. 上界通配符的泛型可以存放 该类型的和它的子类们 ,下界通配符能存放 该类型和它的父类们
从Java角度深入理解Kotlin
  1. 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 编译器就会报错了:

从Java角度深入理解Kotlin

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解析(反射)成对象,然后把对象返回给上层使用:

从Java角度深入理解Kotlin

从上面代码可以看出, CancelTakeoutOrderRequest 我们写了5遍.

那么我们对上面的代码进行优化下,上面的代码只要保证Type对象那里使用是具体的类型就能保证反射成功了

把这个wrapCallable方法在包装一层:

从Java角度深入理解Kotlin

再看下优化后的 cancelTakeoutOrder 方法,发现 CancelTakeoutOrderRequest 需要写2遍:

从Java角度深入理解Kotlin

我们在使用Kotlin的泛型具体换,再来优化下:

因为泛型具体化是一个内联函数,所以需要把requestRemoteSource方法体积变小,所以我们包装一层:

从Java角度深入理解Kotlin

再看下优化后的 cancelTakeoutOrder 方法,发现 CancelTakeoutOrderRequest 需要写1遍就可以了:

从Java角度深入理解Kotlin

Kotlin集合

Kotlin中的集合底层也是使用Java集合框架那一套。在上层又封装了一层可变集合和不可变集合接口。

声明可变集合

从Java角度深入理解Kotlin

声明不可变集合

从Java角度深入理解Kotlin

通过Kotlin提供的api可以方便的创建各种集合,但是同时需要搞清楚该API创建的集合底层到底是对应Java的哪个集合。

Kotlin集合常用的API

1) all、any、count、find、firstOrNull、groupBy函数

从Java角度深入理解Kotlin

2) filter、map、flatMap、flatten函数

从Java角度深入理解Kotlin

案例分析:list.map(Person::age).filter { it > 18 }

虽然 list.map(Person::age).filter { it > 18 } 代码非常简洁,我们要知道底层做了些什么?反编译代码如下:

从Java角度深入理解Kotlin

发现调用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的原理图如下所示:

从Java角度深入理解Kotlin

需要注意的是,如果集合的数量不是特别大,并不建议使用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做一个小结:

  1. 如果集合的数据量很大啊,可以使用集合操作的延迟Sequence
  2. Sequence的filter、map等扩展还是是一个非inline的高阶函数
  3. 集合的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代码也更加自信。

最后,《kotlin in action》是可以一读再读的书,每次都会有新的收获。可以根据书中的章节,深入研究其背后相关的东西。

Document Reference

  1. kotlin in action
  2. www.zhihu.com/question/20…
  3. stackoverflow.com/questions/3…
原文  https://juejin.im/post/5c2ba4255188256ec3600d97
正文到此结束
Loading...