Kotlin学习记录

泛型

  • 对比Java

和 Java 不同, Kotlin 始终要求类型实参要么被显式地说明,要么能被编译器推导出来。(因为Java有历史包袱,1.5才引入泛型)

  • 泛型类型约束

约束上界 : < T : Number> T sum(List < T> list)

  • 擦除和实化类型参数

JVM 上的泛型一般是通过类型擦除实现的,泛型类实例的类型实参在运行时是不保留的。

var items = listOf("lhr","lnx",2)

/**
 * 运行时泛型被擦除了,无法判断List的类型
 */
fun main(args: Array<String>) {
    if (items is List<String>){

        //...
    }
}

声明带实化类型参数的函数(基于inline内联函数)

Kotlin 有特殊的语法结构可以允许你在函数体中使用具体的类型泛型实参,但只有 inline 函数可以。

/**
 * 内联函数 reified声明了类型参数T不会在运行时被擦除
 */
inline fun <reified T> Iterable<*>.filterIsInstance():List<T>{
    val destination= mutableListOf<T>()
    /*
    对指定实参进行检测存储返回。
    因为生成的字节码引用了具体类,而不是类型参数,它不会被运行时发生的类型参数擦除影响 。
     */
    for(element in this){
       if (element is T){
           destination.add(element)
       }
    }
    return destination;
}

fun main(args: Array<String>) {
    val list = listOf("123", "456", 999)
    println(list.filterIsInstance<String>())
}

泛型协变、逆变和不变

概念

假设Orange类是Fruit类的子类,Crate<T> 是一个泛型类

type variance(型变):指我们是否允许对参数类型进行子类型转换
invariance(不型变):也就是说,Crate<Orange> 和 Crate<Fruit> 之间没有关系
covariance(协变):也就是说,Crate<Orange> 是 Crate<Fruit> 的子类型[Java Crate<Orange> 是 Crate<? extends Fruit> 的子类型]
contravariance(逆变):也就是说,Crate<Fruit> 是 Crate<Orange> 的子类型。[Crate<Fruit> 是 Crate<? super Orange> 的子类型]

image

Java

Java处理型变的做法概括起来是:Java中的泛型类在正常使用时是不型变的,要想型变必须在使用处通过通配符进行(称为使用处型变)。

协变

List<? extends Fruit> fruits = new ArrayList<>();
//编译错误:不能添加任何类型的对象
//fruits.add(new Orange());
//fruits.add(new Fruit());
//fruits.add(new Object());
//我们知道,返回值肯定是Fruit
Fruit f = fruits.get(0);

fruits的类型是List<? extends Fruit>,代表Fruit类型或者从Fruit继承的类型的List,fruits可以引用诸如Fruit或Orange这样类型的List,然后向上转型为了List<? extends Fruit>。我们并不关心fruits具体引用的是ArrayList<Orange>(),还是ArrayList<Fruit>(),对于类型 List<? extends Fruit> 我们所能知道的就是:调用一个返回Fruit的方法是安全的,因为你知道,这个List中的任何对象至少具有Fruit类型。

我们之所以可以安全地将 ArrayList<Orange> 向上转型为 List<? extends Fruit>,是因为编译器限制了我们对于 List<? extends Fruit> 类型部分方法的调用。例如void add(T t)方法,以及一切参数中含有 T 的方法(称为消费者方法)。因为这些方法可能会破坏类型安全,只要限制这些方法的调用,就可以安全地将 ArrayList<Orange> 转型为 List<? extends Fruit>

协变:通过限制对于消费者方法的调用,使得像 List<? extends Fruit> 这样的类型成为单纯的“生产者”,以保证运行时的类型安全

逆变

List<Object> objs = new ArrayList<>();
objs.add(new Object());
List<? super Fruit> canContainFruits = objs;
//没有问题,可以写入Fruit类及其子类
canContainFruits.add(new Orange());
canContainFruits.add(new Banana());
canContainFruits.add(new Fruit());
//无法安全地读取,canContainFruits完全可能包含Fruit基类的对象,比如这里的Object
//Fruit f = canContainFruits.get(0);

//总是可以读取为Object,然而这并没有太多意义
Object o = canContainFruits.get(1);

canContainFruits的类型是List<? super Fruit>,代表Fruit类型或者Fruit基类型的List,canContainFruits可以引用诸如Fruit或Object这样类型的List,然后向上转型为了List<? super Fruit>。对于List中的 T get(int pos) 方法,当指定类型是 “? super Fruit” 时,get方法的返回类型就变成了 “? super Fruit”,也就是说,返回类型可能是Fruit或者任意Fruit的基类型,我们不能确定,因此编译器拒绝调用任何返回类型为 T 的方法(除非我们只是读取为Object类)

逆变:编译器限制了我们对于 List<? super Fruit> 类型部分方法的调用。例如T get(int pos)方法,以及一切返回类型为 T 的方法(称为生产者方法),因为我们不能确定这些方法的返回类型,只要限制这些方法的调用,就可以安全地将 ArrayList<Object> 转型为 List<? super Fruit>。这就是所谓的逆变,通过限制对于生产者方法的调用,使得像 List<? super Fruit> 这样的类型成为单纯的“消费者”。

总结

extends限定了通配符类型的上界,所以我们可以安全地从其中读取;而super限定了通配符类型的下界,所以我们可以安全地向其中写入。

Kotlin

Kotlin中的泛型类在定义时即可标明型变类型(协变或逆变,当然也可以不标明,那就是不型变的),在使用处可以直接型变(称为声明处型变)。

协变

使用out修饰符,表明类型参数 T 在泛型类中仅作为方法的返回值,不作为方法的参数,因此,这个泛型类是个协变的。回报是,使用时Source<Orange>可以作为Source<Fruit>的子类型。

abstract class Source<out T> {
    abstract fun nextT(): T
}

fun demo(oranges: Source<Orange>) {
    val fruits: Source<Fruit> = oranges // 没问题,因为 T 是一个 out-参数,Source<T>是协变的
    val oneFruit: Fruit = fruits.nextT() //可以安全读取
}

逆变

使用in修饰符,表明类型参数 T 在泛型类中仅作为方法的参数,不作为方法的返回值,因此,这个泛型类是个逆变的。回报是,使用时Comparable<Number>可以作为Comparable<Double>的子类型。

//kotlin
abstract class Comparable<in T> {
    abstract fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    val y: Comparable<Double> = x // OK!逆变,Comparable<Number>可以作为Comparable<Double>的子类型
    y.compareTo(1.0) //1.0 拥有类型 Double
}

总结

out和in修饰符是自解释的。out代表泛型类中,类型参数 T 只能存在于方法的返回值中,即是作为输出,因此,泛型类是生产者/协变的;in代表泛型类中,类型参数T只能存在于方法的参数中,即是作为输入,因此,泛型类是消费者/逆变的。

注解

  • 注解实参需要在编译期就是己知的,所以你不能引用任意的属性作为实参。要把属性当作注解实参使用,你需要用const修饰符标记它,来告知编译器这个属性是编译期常量。
  • Kotlin 允许你对任意的表达式应用注解,而不仅仅是类和函数的声明及类型。
  • 用注解控制JavaAPI
//volatile 多线程可见性
@volatile
//Strictfp 多平台浮点数可预测性
@Strictfp
//改变由 Kotlin 生成的 Java 方法或字段的名称 
@JvmName 
//能被用在对象声明或者伴生对象的方法上,把它们暴露成Java 的静态方法
@JvmStatic
//指导 Kotlin 编译器为带默认参数值的函数生成多个重载
@JvmOverloads
//应用于一个属性,把这个属性暴露成一个没有访问器的公有 Java 字段。
@JvmField 
  • 注解使用
//声明注解
//@Target 元注解
@Target(AnnotationTarget.PROPERTY)
annotation class xxx

DSL

关于DSL

  • DSL

DSL 也就是 Domain Specific Language 的简称,是指为某些特定领域(domain)设计的专用语言。最常见的 DSL 就是 SQL和正则表达式。和通用编程语言相反,DSL大部分是声明式的(声明式语言描述了想要的结果并将执行细节留给了解释它的引擎)。

  • 内部DSL

与有着自己独立语法的外部 DSL 不同,内部 DSL是用通用编程语言编写的程序的一部分,使用了和通用编程语言完全一致的语法。实际上,内部 DSL 不是完全独立的语言,而是使用主要语言的特定方式,同时保留具有独立语法的 DSL 的主要优点。 [使用Kotlin 语言开发的,解决特定领域问题,具备独特代码结构的 API]

  • DSL结构

DSL 通常会依赖在其他上下文中也会广泛使用的语言特性,比如说中缀调用和运算符重载。DSL与API不同的显著特征是结构/文法。 与API的命令查询不同, DSL 的方法调用存在于由 DSL 文法定义的 更大的结构中。

  • Kotlin DSL

Kotiin 的 DSL 是完全静态类型的[编译时错误的检测,以及更好的 IDE 支持]

Kotlin DSL使用

利用带接收者的lambda和扩展函数实现HTML Table Sample DSL

open class Tag(val name: String) {

    /**
     * 保存所有嵌套标签
     */
    private val children = mutableListOf<Tag>()

    protected fun <T : Tag> doInit(child: T, init: T. () -> Unit) {
        //调用作为参数的lambda,初始化并保存子标签
        child.init()
        children.add(child)
    }

    override fun toString() = "<$name>${children.joinToString("")} </$name>"
}

fun table(init: TABLE.() -> Unit) = TABLE().apply(init)

class TABLE : Tag("table") {
    /**
     * 创建并初始化 TR 标签的示例并添加到TABLE 的子标签中
     */
    fun tr(init: TR.() -> Unit) = doInit(TR(), init)
}

class TR : Tag("tr") {
    /**
     * 添加TD标签的一个实例到 TR 的子标签中
     */
    fun td(init: TD.() -> Unit) = doInit(TD(), init)
}

class TD : Tag("td")

fun main(args: Array<String>) {
    fun createTable() =
            table {
                tr {
                    td { }
                    td { }
                }
            }
    //<table><tr><td> </td><td> </td> </tr> </table>
    println(createTable())
}

关于带接收者的lambda与扩展函数

让我们使用一个结构来构建 API。当函数被调用的时候需要提供这个对象,它在函数体内是可用的。实际上 ,一个扩展函数类型描述了一个可以被当作扩展函数来调用的代码块。

调用扩展函数类型lamdba时,可以像调用一个扩展函数那样调用 lambda,而不是将对象作为参数传递给 lambda。

/**
 * action: StringBuilder.() -> Unit 定义接收者为StringBuilder的lambda
 * StringBuilder.() -> Unit 扩展函数
 */
fun buildString(action: StringBuilder.() -> Unit):String{
    val sb = StringBuilder()
    //传递一个 StringBuilder 实例作为lambda的接收者
    sb.action()
    return sb.toString()
}

var s = buildString {
    append("are you ok")
}

invoke约定

invoke约定允许把自定义类型的对象当作函数一样调用。(类似于运算符重载)

class Person(val name: String){
    /**
     * 重载invoke
     */
    operator fun invoke(no: String){
        println("$name - $no ")
    }
}

fun main(args: Array<String>) {
    val person = Person("lhr")
    person("test")//实际上调用的是person.invoke

}

中缀调用 DSL清晰的语法

object start

infix fun String.should(x: start): StartWrapper = StartWrapper(this)

class StartWrapper(val value: String) {
    infix fun with(prefix: String): Unit =
            if (value.startsWith(prefix)) {
            } else {
                throw AssertionError("String does not start with $prefix: $value")
            }

}

fun main(args: Array<String>) {
    "kotlin" should start with "ko"
}

基本数据类型扩展

val Int.days: Period
    //this 引用数字常量值
    get() = Period . ofDays (this)

val Period.ago:LocalDate
    get() = LocalDate.now() - this

fun main(args: Array<String>) {
    //获取一天前日期
    println(1.days.ago)
}