从Java角度深刻理解Kotlin

前言

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

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

  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包装类型。github

在 Kotlin 中是没有这样的区分的,例如对于整型来讲只有 Int 这一个类型,Int 是一个类(姑且把它当装包装类型),咱们能够说在 Kotlin 中在编译前只有包装类型,为何说是编译前呢?由于编译时会根据状况把这个整型( Int )是编译成 Java 中的 int 仍是 Integer。 那么是根据哪些状况来编译成基本类型仍是包装类型呢,后面会讲到。咱们先来看下 Kotlin和 Java 数据类型对比:json

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中的基本类型仍是包装类型。下面以整型为例,其余的数据类型同理。api

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 那一套,要单独设计一套这样的数据类型呢?bash

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([var/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 就是用来解决这个问题的。若是有新的类型分支且没有处理编译器就会报错。

sealed-class.png

当 when 判断的是 sealed class,那么不须要加 else 默认分支,若是有新的子类,编译器会经过编译报错的方式提醒开发者添加新分支,从而保证逻辑的完整性和正确性

须要注意的是,当 when 判断的是 sealed class,千万不要添加 else 分支,不然有新类编译器也不会提醒

sealed class 其实是一个抽象类且不能被继承,构造方法是私有的。

object 关键字用法汇总

除了上面咱们介绍的,object 关键字定义单例类外,object 关键字还有如下几种用法:

1) companion object

咱们把 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 的定义:

lambda.png

总的来讲,主要有 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 方法,invoke 方法体里的代码就是 lambda 体的代码逻辑。

这个内部类会暴露一个实例常量 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,它格式以下:

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

  1. 冒号左边是 function type 的名字
  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 方法:

lambda-receiver.png

除了 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 也能够:

let-run.png

高阶函数的优化

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

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

并且若是调用高阶函数的时候,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. 上界通配符的泛型能够存放该类型的和它的子类们,下界通配符能存放该类型和它的父类们

generic-.png

  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 类的方法产生约束:泛型不能当作方法入参,只能当作出参。Kotlin 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 编译器就会报错了:

generic-out.png

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

泛型具体换应用01

从上面代码能够看出,CancelTakeoutOrderResponse 咱们写了 5 遍.

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

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

wrap.png

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

泛型具体换应用02

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

由于泛型具体化是一个内联函数,因此须要把 requestRemoteSource 方法体积变小,因此咱们包装一层:

wrap2.png

再看下优化后的 cancelTakeoutOrder 方法,发现 CancelTakeoutOrderResponse 须要写 1 遍就能够了:

泛型具体换应用03

Kotlin 集合

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

下面是 Kotlin 封装的可变集合和不可变集合接口:

接口 是否可变 所在文件
List 不可变 Collections.kt
MutableList 可变 Collections.kt
Set 不可变 Collections.kt
MutableSet 可变 Collections.kt
Map 不可变 Collections.kt
MutableMap 可变 Collections.kt

声明可变集合

声明可变集合

声明不可变集合

声明不可变集合

经过 Kotlin 提供的 API 能够方便的建立各类集合,可是同时须要搞清楚该 API 建立的集合底层究竟是对应 Java 的哪一个集合。

Kotlin 集合经常使用的 API

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

collection-api.png

2) filter、map、flatMap、flatten 函数

collection-api2.png

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

虽然 list.map(Person::age).filter { it > 18 } 代码很是简洁,咱们要知道底层作了些什么?反编译代码以下:

collection-api3.png

发现调用 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 的原理:

集合的元素有序的通过 filte r操做,若是知足 filter 条件,再通过 map 操做。

而不会新建一个集合存放符合 filter 条件的元素,而后在建立一个集合存放 map 的元素

Sequence 的原理图以下所示:

Sequence.png

须要注意的是,若是集合的数量不是特别大,并不建议使用 Sequence 的方式来进行操做。咱们来看下 Sequence<T>.map 函数

public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
    return TransformingSequence(this, transform)
}
复制代码

它是一个高阶函数,可是它并无内联,为啥没有内联?由于它把 transform 传递给了 TransformingSequence,而后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》是能够一读再读的书,每次都会有新的收获。能够根据书中的章节,深刻研究其背后相关的东西。


下面是个人公众号,干货文章不错过,有须要的能够关注下,有任何问题能够联系我:

公众号:  chiclaim

Document Reference

  1. 《Kotlin In Action》
  2. www.zhihu.com/question/20…
  3. stackoverflow.com/questions/3…
相关文章
相关标签/搜索