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> 的子类型]
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)
}