变量声明
val
—声明只读变量var
—声明可读写变量
在kotlin中 val
声明的是只读变量,但是不是常量,这个说法比较有意思,和java有区别,比如,val声明一个变量,可以定义它的get方法:
class X {
val b: Int
get() {
return (Math.random() * 100).toInt()
}
}
它并不是一个常量,要定义一个真正意义上的常量,必须使用const
, const
只能修饰基本类型,且必须初始化
object KotlinVars2 {
//编译时常量
const val b = 3
}
这样定义了一个编译时常量,等价于java的 static final int
val
跟java的final
一样,可以不指定初始值,但是必须要在后面的某个地方初始化它:
val c: Int
if (a == 3) {
c = 4
} else {
c = 5
}
建议:尽可能使用 val 来声明不可变引用,让程序的含义更加清晰稳定。
kotlin基本数据类型
- 变量声明:
var a: Int = 2
val b: String = "Hello Kotlin"
特点是冒号后面跟上类型
- 变量声明—类型自动推导:
var a = 2
val b = "Hello Kotlin"
这种方式可以省略类型,由kotlin自动推导
- 长整型写法,必须以大写 L 结尾,与java不同:
// val c = 12345678910l // compile error.
val c = 12345678910L // ok
- Float是后面加小写的
f
, 不写f
的小数就是Double类型的:
val d = 3.0 // Double, 3.0f Float
val float1: Float = 1f
- 与java不同,kotlin中所有类型转换必须显示调用方法:
val e: Int = 10
//val f: Long = e // implicitness not allowed
val f: Long = e.toLong()
- 字符串支持模板变量引用:
val j = "I❤️China"
println("Value of String 'j' is: $j") // no need brackets
println("Length of String 'j' is: ${j.length}") // need brackets
System.out.printf("Length of String 'j' is: %d\n", j.length)
===
比较引用,==
比较值:
val k = "Today is a sunny day."
val m = String("Today is a sunny day.".toCharArray())
println(k === m) //比较引用
println(k == m) //比较值
val n = """
<!doctype html>
<html>
<head>
<meta charset="UTF-8"/>
<title>Hello World</title>
</head>
<body>
<div id="container">
<H1>Hello World</H1>
<p>This is a demo page.</p>
</div>
</body>
</html>
""".trimIndent()
println(n)
集合相关
集合类型
集合框架的创建
- 只有可变的才能添加删除元素:
val intList: List<Int> = listOf(1, 2, 3, 4) //不可变list 不能添加删除
val intList2: MutableList<Int> = mutableListOf(1, 2, 3, 4)//可变list 可以添加删除
val map: Map<String, Any> = mapOf("name" to "benny", "age" to 20)
val map2: Map<String, Any> = mutableMapOf("name" to "benny", "age" to 20)
- 集合遍历:
//集合遍历 for-i
for (i in 0..10) {
println(i)
}
//集合遍历 for-in
for (e in list) {
println(e)
}
var i = 0
while (i++ < 10) {
println(i)
}
do {
println("Hello")
} while (false)
//集合遍历 for-each
list.forEach {
if (it == 2) return@forEach //continue操作
println(it)
}
- 集合遍历可以直接通过
+
或-
号来添加/删除元素:
//直接仿java的创建方式也可以
val stringList = ArrayList<String>()
for (a in 'a'..'z'){
stringList.add("char: $a")
}
for (a in stringList){
println("item====${a}")
}
//kotlin集合添加元素,可以直接+,等价于add方法
for (i in 0 .. 10){
stringList += "num: $i"
}
//kotlin集合删除元素,可以直接-,等价于remove方法
for (i in 0 .. 10){
stringList -= "num: $i"
}
- 与java不同,可以直接通过
[ ]
下标索引赋值取值:
stringList[5] = "HelloWorld"
val valueAt5 = stringList[5]
- map也可以直接用
[ ]
来存取:
val hashMap = HashMap<String, Int>()
hashMap["Hello"] = 10
println(hashMap["Hello"])
- 特殊集合类型:Pair表示一对值, Triple表示一个三值集合
val pair = "Hello" to "Kotlin"
val pair = Pair("Hello", "Kotlin")
val first = pair.first
val second = pair.second
val (x, y) = pair
val triple = Triple("x", 2, 3.0)
val first = triple.first
val second = triple.second
val third = triple.third
val (x, y, z) = triple
数组类型
kotlin中的数组类型跟java相对应,其中基本类型和装箱类型的写法也是不一样的,装箱类型的都带有泛型参数,基本类型直接是一个类
数组的创建
- 数组的长度:
val a = IntArray(5)
println(a.size) //same with the Collections(e.g. List)
val b = ArrayList<String>()
println(b.size)
比java好的地方:不需要.length和.size区分了,集合长度也是.size
- 引用数组值
val d = arrayOf("Hello", "World")
d[1] = "Kotlin"
println("${d[0]}, ${d[1]}")
- 数组遍历:
val e = floatArrayOf(1f, 3f, 5f, 7f)
//普通遍历
for (element in e) {
println(element)
}
println("====================")
//遍历支持箭头函数
e.forEach { element ->
println(element)
}
println("====================")
//一个参数可以省略箭头 it是某个元素
e.forEach {
println(it)
}
println("====================")
显然forEach
的遍历方式最方便
- 判断某个元素是否在数组中:
if(1f in e){
println("1f exists in variable 'e'")
}
- 判断某个元素不在数组中:
if(1.2f !in e){
println("1.2f not exists in variable 'e'")
}
区间类型
- kotlin中的区间类型,java没有
- 开区间
- 倒序区间
- 区间步长
- 可数区间 即离散区间才能打印出来,连续区间是不能打印的
//闭区间
val intRange = 1..10 // [1, 10]
val charRange = 'a'..'z'
val longRange = 1L..100L
val floatRange = 1f .. 2f // [1, 2]
val doubleRange = 1.0 .. 2.0
//打印可数的区间 离散区间
println(intRange.joinToString())
//对于不可数的区间,这样不能打印出来 连续区间
println(floatRange.toString())
- 可数的离散区间才支持步长设置:
//可数的离散区间才支持步长设置
val intRangeWithStep = 1..10 step 2
val charRangeWithStep = 'a'..'z' step 2
val longRangeWithStep = 1L..100L step 5
println(intRangeWithStep.joinToString())
println("====================")
//半闭半开区间
val intRangeExclusive = 1 until 10 // [1, 10)
val charRangeExclusive = 'a' until 'z'
val longRangeExclusive = 1L until 100L
println(intRangeExclusive.joinToString())
println("====================")
//倒序区间
val intRangeReverse = 10 downTo 1 // [10, 9, ... , 1]
val charRangeReverse = 'z' downTo 'a'
val longRangeReverse = 100L downTo 1L
println(intRangeReverse.joinToString())
println("====================")
- 遍历离散区间跟数组一样:
for (element in intRange) {
println(element)
}
intRange.forEach {
println(it)
}
- 判断一个值是否在连续区间中:
if (3.0 in doubleRange) {
println("3.0 in range 'doubleRange'")
}
if (3.0 !in doubleRange) {
println("3.0 not in range 'doubleRange'")
}
- 判断一个值是否在离散区间中:
if (12 !in intRange) {
println("12 not in range 'intRange'")
}
if (12 in intRange) {
println("12 in range 'intRange'")
}
- kotlin中的
for-i
循环便捷写法:
val array = intArrayOf(1, 3, 5, 7)
for(i in array.indices){
println(array[i])
}
//麻烦点的写法
for (i in 0 until array.size) {
println(array[i])
}
集合、数组、区间这三种类型他们的遍历和判断是否在集合中都是一样的方法,使用for-in语法。
函数定义
其中函数返回值为Unit
可以省略 即void
类型省略,跟java也是一样的。
- 函数的引用
函数引用的写法感觉比较奇怪,它是函数名前面加两个冒号,函数的引用类似C语言中的函数指针,可用于函数传递。
左边冒号后面的函数类型可以写,也可以省略掉,简写:
其中等号右侧冒号前面有类名的是类对象的方法引用,在调用的时候也要传对象实例才行:
val foo = Foo();
h(foo, "qq", 1);
- 变长参数:kotlin中函数变长参数类型使用
vararg
修饰
fun multiParameters(vararg ints: Int){
println(ints.contentToString())
}
这时ints实际上是一个IntArray,可以进行遍历操作等。这个变长参数类型很奇怪,看上去是var和arg两个单词的合并。
listOf(1,2,3) //listOf也是通过变长参数实现的
- 函数默认参数:
fun defaultParameter(x: Int, y: String, z: Long = 0L){
TODO()
}
//默认参数省略
defaultParameter(5, "Hello")
- 如果默认参数不是最后一个,必须使用具名参数:
fun defaultParameter(x: Int = 5, y: String, z: Long = 0L){
TODO()
}
defaultParameter(y = "Hello")
- 函数参数可以是另一个函数:
fun test(p: (Foo, String, Long) -> Any){
//p(Foo(), "Hello", 3L)//调用
}
val x = Foo::bar
test(x)
- 多参数返回值:
fun multiReturnValues(): Triple<Int, Long, Double> {
return Triple(1, 3L, 4.0)
}
val (a, b, c) = multiReturnValues()
val r = a + b
val r1 = a + c
实际就是返回了一个Triple对象,它可以持有多个值而已
表达式
- 在kotlin里面分支判断都是表达式而不是语句,在java中是语句,这个是kotlin与java的最大不同
var c: Int
if (a == 3) {
c = 4
} else {
c = 5
}
//可以直接表达式赋值
c = if (a == 3) 4 else 5
由于是表达式,所以分支判断都可以直接赋值为一个变量,c = if (a == 3) 4 else 5
,因此kotlin里面没有三目运算符,是因为本身表达式就支持。类似的when
、try-catch
全部可以赋值
c = when (a) {
0 -> 5
1 -> 100
else -> 20
}
var x: Any = Any()
c = when {
x is String -> x.length
x == 1 -> 100
else -> 20
}
c = when(val input = readLine()){
null -> 0
else -> input.length
}
val b = 0
try {
c = a / b
}catch (e: Exception){
e.printStackTrace()
c = 0
}
c = try {
a / b
} catch (e: ArithmeticException){
2
} catch (e: Exception) {
e.printStackTrace()
0
}
c = try {
a / b
} catch (e: ArithmeticException){
2
} catch (e: Exception) {
e.printStackTrace()
0
}
这样可以直接赋值确实比java方便了许多!when
代替了java的switch-case
,写法也更简洁了
运算符重载
kotlin支持运算符重载,类似C++,kotlin中的 ==
、+
、>
、[]
、包括函数调用符号 ()
都是kotlin中内置好的重载运算符,参考:https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators
在IDE当中点击对应的符号可以直接跳转到对应的实现。
运算符重载例子:复数运算操作
import java.lang.IndexOutOfBoundsException
//复数
class Complex(var real: Double, var image: Double){
override fun toString() = "$real + ${image}i"
}
operator fun Complex.plus(other: Complex): Complex {
return Complex(this.real + other.real, this.image + other.image)
}
operator fun Complex.plus(other: Double): Complex {
return Complex(this.real + other, this.image)
}
operator fun Complex.plus(other: Int): Complex {
return Complex(this.real + other, this.image)
}
operator fun Complex.minus(real: Double): Complex {
return Complex(this.real - real, this.image)
}
operator fun Complex.get(index: Int): Double = when(index){
0 -> this.real
1 -> this.image
else -> throw IndexOutOfBoundsException()
}
fun main() {
val c1 = Complex(3.0, 4.0)
val c2 = Complex(2.0, 2.0)
println(c1 + 2.0)
println(c1 + c2)
println(c1 + 3)
println(c1 - 3.0)
println(c1[0])
println(c1[1])
println(c1[2])
}
运算符重载例子2:实现字符串的加减乘除
operator fun String.minus(right: Any?) = this.replaceFirst(right.toString(), "")
operator fun String.times(right: Int): String {
return (1..right).joinToString(""){ this }
}
operator fun String.div(right: Any?): Int {
val right = right.toString()
return this.windowed(right.length, 1, transform = {
it == right
}).count { it }
}
fun main() {
val value = "HelloWorld World"
println(value - "World")
println(value * 2)
val star = "*"
println("*" * 20)
println(value / 3)
println(value / "l")
println(value / "ld")
}
重载运算符的定义特点就是类定义扩展方法,方法名和运算符对应的描述,可以到前面的网址上查,然后方法前面加 operator
关键字。
定义hascode和euqals方法:
class Person(val age: Int, val name: String){
override fun equals(other: Any?): Boolean {
val other = (other as? Person)?: return false
return other.age == age && other.name == name
}
override fun hashCode(): Int {
return 1 + 7 * age + 13 * name.hashCode()
}
}
与java一样,如果是添加到哈希一类的数据结构中,必须重写equals和hascode方法,如果equals方法判断相同,则认为是同一个对象。
val persons = HashSet<Person>()
(0..5).forEach {
persons += Person(20, "Benny")
}
println(persons.size) //打印出1
lambda表达式
- kotlin里面的lambdas表达式是一个匿名函数的语法糖,因此它的类型其实就是对应的函数类型
- java中的lambdas表达式其实是接口类型的一个语法糖,二者不同
//函数类型是 () -> Unit
val func: () -> Unit = fun() {
println("Hello") //最后一行是函数的返回值
}
val lambda: () -> Unit = {
println("Hello")
}
//函数类型是 (Int) -> String
val f1 = { p: Int ->
println(p)
"Hello" //最后一行是函数的返回值
}
println(f1(1))
kotlin中lambda表达式中最后一行的类型就是函数的返回类型
中缀表达式
kotlin中比较奇怪的一种写法
val map = mutableMapOf(
"Hello" to 2,
"World" to 3
)
2 to 3
2.to(3)
println("HelloWorld" rotate 5)
infix fun String.rotate(count: Int): String {
val index = count % length
return this.substring(index) + this.substring(0, index)
}
本质还是类的扩展方法,前面加 infix
关键字,可能是为了实现更加语义化的书写方式
class Book
class Desk
infix fun Book.on(desk: Desk){
}
val book = Book()
val desk = Desk()
book on desk
高阶函数
高阶函数简单来说就是函数的参数可传递另一个函数,常见于forEach
表达式:
val intArray = IntArray(5){ it + 1 }
intArray.forEach {
println(it)
}
intArray.forEach(::println)
intArray.forEach {
println("Hello $it")
}
例子:定义一个函数打印方法的耗时
fun cost(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
println("${System.currentTimeMillis() - start}ms")
}
//返回值是一个lambda表达式,也是高阶函数
fun fibonacci(): () -> Long {
var first = 0L
var second = 1L
return {
val next = first + second
val current = first
first = second
second = next
current
}
}
调用:
cost {
val fibonacciNext = fibonacci()
for (i in 0..10) {
println(fibonacciNext())
}
}
显然在java中这样是做不到的,必须每个方法里面前后去打印一下才行。。
内联函数
添加inline
关键字的函数标记为内联函数,内联函数的特点是,代码会被直接插入到调用处,编译的代码反编译结果就是代码直接插入到调用处的效果。内联函数效率高一些。
val intArray = IntArray(5){ it + 1 }
intArray.forEach {
println(it)
}
forEach在kotlin中的实现就是内联函数:
public inline fun IntArray.forEach(action: (Int) -> Unit): Unit {
for (element in this) action(element)
}
高阶函数搭配内联函数使用更加高效:
inline fun cost(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
println(System.currentTimeMillis() - start)
}
cost {
println("Hello")
}
编译后等价于:
val start = System.currentTimeMillis()
println("Hello")
println(System.currentTimeMillis() - start)
内联函数的返回:
val ints = intArrayOf(1, 2, 3, 4)
ints.forEach {
if(it == 3) return@forEach
println("Hello $it")
}
其实是跳过3并不是返回,等价于:
for (element in ints) {
if(element == 3) continue
println("Hello $element")
}
- nonLocalReturn 返回调用的方法,下面直接返回main方法
inline fun nonLocalReturn(block: () -> Unit){
block()
}
fun main() {
nonLocalReturn {
return
}
}
- 禁止non-local-return,使用
crossinline
关键字
public inline fun IntArray.forEach(crossinline action: (Int) -> Unit): Unit {
for (element in this) action(element)
}
- 内联属性:
var pocket: Double = 0.0
var money: Double
inline get() = pocket
inline set(value) {
pocket = value
}
内联函数的限制:
- 内联函数只能访问对应类的 public 成员
- 内联函数的参数不能被存储(赋值给变量)
- 内联函数的参数只能传递给其他内联函数参数
扩展函数
标准库中的常用扩展函数:let、run、also、apply、use
fun main() {
val person = Person("benny", 20)
person.let(::println)
person.run(::println)
val person2 = person.also {
it.name = "hhh" //it是当前的对象
}
val person3 = person.apply {
name = "xxx" //this是当前对象
}
File("build.gradle").inputStream().reader().buffered().use {
println(it.readLines())
}
}
在IDE中点击打开 use
方法的实现源码:
@InlineOnly
@RequireKotlin("1.2", versionKind = RequireKotlinVersionKind.COMPILER_VERSION, message = "Requires newer compiler version to be inlined correctly.")
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
when {
apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
this == null -> {}
exception == null -> close()
else ->
try {
close()
} catch (closeException: Throwable) {
// cause.addSuppressed(closeException) // ignored here
}
}
}
}
可以发现它基本上实现了我们java的try-catch-finally
结构,包括最终流它也默认帮你关闭了,你都不需要关心了,可以说用起来真的省事多了!IO读写再也不用那么麻烦了,比java又臭又长的写法真是方便太多!
集合变换操作符
集合的变换操作
val list = listOf(1, 2, 3, 4)
list.filter { it % 2 == 0 }
list.flatMap {
0 until it
}
.joinToString().let(::println)
list.asSequence()
.flatMap {
(0 until it).asSequence()
}
.joinToString().let(::println)
val newList = list.flatMap {
ArrayList<String>(it)
}
- sequence的概念
sequence类似java8里面的stream流,或者RxJava里面的流概念
val list = listOf(1, 2, 3, 4)
list.asSequence()
.filter {
println("filter: $it")
it % 2 == 0
}.map {
println("map: $it")
it * 2 + 1
}.forEach {
println("forEach: $it")
}
上面代码list
调用asSequence
后每个元素会依次调用filter
和map
, 不加asSequence
每个元素会先调用filter
再调用map
。
加asSequence
最后不加forEach
的话,不会有输出,不加asSequence
的话,去掉forEach
也会有输出。即加上asSequence
变成了流一样。
asSequence
被称为懒序列,使用asSequence
性能会优化一些,因为每个元素只需要走一遍每个操作,而不是每个操作中将每个元素走一遍。
集合的聚合操作
sum
val list = listOf(1, 2, 3, 4)
val s = list.sum()
println("list.sum() $s")//10
val list = listOf(1, 2, 3, 4)
//acc是上次的计算结果,初始值为StringBuffer(),返回值跟初始值类型一样
val foldStrBuf = list.fold(StringBuffer()){
acc, i -> acc.append(i)
}
println("list.fold() $foldStrBuf")//1234
reduce
val list = listOf(1, 2, 3, 4)
val r = list.reduce() { acc, i ->
acc + i
}
println("list.reduce() $r")//10
fold和reduce有点递归的意思在里面,每次的结果都是基于上次的结果。
zip
val list = listOf(1, 2, 3, 4)
val array = arrayOf(2, 2)
val z = list.zip(array) { a: Int, b: Int ->
a * b
}
z.forEach {
println(it)
} // 2 4
val array2 = arrayOf("x", "y")
val z2 = list.zip(array2) { a: Int, b: String ->
"$a$b"
}
z2.forEach {
println(it)
} // 1x 2y
看源码,zip其实就是将两个集合遍历执行某个操作,只不过最终集合大小是以最小长度的那个集合为准:
public inline fun <T, R, V> Iterable<T>.zip(other: Array<out R>, transform: (a: T, b: R) -> V): List<V> {
val arraySize = other.size
val list = ArrayList<V>(minOf(collectionSizeOrDefault(10), arraySize))
var i = 0
for (element in this) {
if (i >= arraySize) break
list.add(transform(element, other[i++]))
}
return list
}
集合变换应用例子:
统计文本文件中非空格字符出现的次数
import java.io.File
fun main() {
File("build.gradle").readText() // 1. read file
.toCharArray() // 2.
//.filter{ !it.isWhitespace() } // 3. filter white space
.filterNot(Char::isWhitespace) // 等价上面一行
.groupBy { it } //分组
.map {
it.key to it.value.size
}.let {
println(it)
}
}
SAM转换
- Java 的 SAM:
- Kotlin 的 SAM:
- Kotlin的匿名内部类:
val executor: ExecutorService = Executors.newSingleThreadExecutor()
//匿名内部类的写法
executor.submit(object : Runnable {
override fun run() {
println("run in executor.")
}
})
//匿名内部类简写
executor.submit(Runnable {
println("run in executor.")
})
//匿名内部类简写
executor.submit { println("run in executor.") }
kotlin中SAM目前只支持只有一个方法的java接口
fun submitRunnable(runnable: Runnable){
runnable.run()
}
submitRunnable {
println("Hello")
}
kotlin中SAM不支持只有一个方法的kotlin接口, 但是可以直接定义一个函数参数
下面这样写法是不行的:
interface Invokable {
fun invoke()
}
fun submit(invokable: Invokable) {
invokable.invoke()
}
//报错
submit {
println("Hello")
}
下面这样写法是可行的:
typealias FunctionX = ()->Unit
//函数参数传递一个lambda表达式
fun submit(block: FunctionX){
block()
}
//等价这种直接传lambda表达式的写法
//fun submit(()->Unit){
//
//}
//这样是可以的
submit {
println("Hello啊啊啊")
}
- SAM转换支持对比:
一个例子,添加和移除监听的正确kotlin写法:
public class EventManager {
interface OnEventListener {
void onEvent(int event);
}
private HashSet<OnEventListener> onEventListeners = new HashSet<>();
public void addOnEventListener(OnEventListener onEventListener){
this.onEventListeners.add(onEventListener);
}
public void removeOnEventListener(OnEventListener onEventListener){
this.onEventListeners.remove(onEventListener);
}
}
使用上面的java类:
fun main() {
val eventManager = EventManager()
//匿名内部类的写法
val onEvent = EventManager.OnEventListener { event -> println("onEvent $event") }
//等价上面的写法
val onEvent2 = object : EventManager.OnEventListener{
override fun onEvent(event: Int) {
println("onEvent $event")
}
}
// DO NOT use this.
//错误的写法,这样还是一个函数类型,传到removeOnEventListener方法里不能移除,
// 还是会调用方法创建一个对象
// val onEvent3 = { event: Int ->
// println("onEvent $event")
// }
eventManager.addOnEventListener(onEvent)
eventManager.removeOnEventListener(onEvent)
}
DSL: 领域特定语言
如sql语言、gradle中的groovy语言等,kotlin可以方便的实现这些语言的写法
例子: 通过拼接操作生成一个html文件
import java.io.File
interface Node {
fun render(): String
}
class StringNode(val content: String): Node {
override fun render(): String {
return content
}
}
class BlockNode(val name: String): Node {
val children = ArrayList<Node>()
val properties = HashMap<String, Any>()
override fun render(): String {
return """<$name ${properties.map { "${it.key}='${it.value}'" }.joinToString(" ")}>${children.joinToString(""){ it.render() }}</$name>"""
}
operator fun String.invoke(block: BlockNode.()-> Unit): BlockNode {
val node = BlockNode(this)
node.block()
this@BlockNode.children += node
return node
}
operator fun String.invoke(value: Any) {
this@BlockNode.properties[this] = value
}
operator fun String.unaryPlus(){
this@BlockNode.children += StringNode(this)
}
}
fun html(block: BlockNode.() -> Unit): BlockNode {
val html = BlockNode("html")
html.block()
return html
}
fun BlockNode.head(block: BlockNode.()-> Unit): BlockNode {
val head = BlockNode("head")
head.block()
this.children += head
return head
}
fun BlockNode.body(block: BlockNode.()-> Unit): BlockNode {
val head = BlockNode("body")
head.block()
this.children += head
return head
}
fun main() {
//变量后面跟东西相当于传递一个lambda表达式
val htmlContent = html {
head {
"meta" { "charset"("UTF-8") } //字符串后面跟东西相当于运算符重载 invoke
}
body {
"div" {
"style"(
"""
width: 200px;
height: 200px;
line-height: 200px;
background-color: #C9394A;
text-align: center
""".trimIndent()
)
"span" {
"style"(
"""
color: white;
font-family: Microsoft YaHei
""".trimIndent()
)
+"Hello HTML DSL!!"
}
}
}
}.render()
File("Kotlin.html").writeText(htmlContent)
}
这个例子主要有两点:
- 一个是如果是变量后面跟东西相当于传递一个lambda表达式,那定义的时候其实就是定义一个高阶函数来实现;
- 二是如果字符串后面跟东西相当于运算符重载
invoke
,跟{}
相当于参数是一个ambda表达式,跟()
就是普通参数,定义String类的扩展函数即可实现。
operator fun String.invoke(block: BlockNode.()-> Unit): BlockNode {
val node = BlockNode(this)
node.block()
this@BlockNode.children += node
return node
}
operator fun String.invoke(value: Any) {
this@BlockNode.properties[this] = value
}
+"Hello HTML DSL!!"
这种也是字符串的运算符重载:
operator fun String.unaryPlus(){
this@BlockNode.children += StringNode(this)
}
字符串前面后面跟操作符好像基本都是运算符重载
另外扩展方法中如果想访问除了自身以外的其他Receiver的话,只需将扩展方法定义到对应的类内部即可,如上面的String相关扩展方法直接定义到了BlockNode类的内部,就可以引用BlockNode类的成员属性来使用了。
Kotlin中的几个特殊符号 ( ‘?.‘ ‘?:‘ ‘!!‘ ‘as?‘ ‘?‘ ) 含义
?.
安全调用符
if (foo != null){
return foo.bar()
}else{
return null
}
?:
as?
!!
?
类构造器
init 块
- 构造器内省写var的属性,可以防止类内初始化,类似java中的大括号初始化,该初始化会跟构造函数一起执行。
init
块中可以直接访问到构造方法中的参数。init
块可以有多个,可以分开写,init
块最终会合并执行。
副构造函数,后面可以指定调用主构造函数或其他副构造函数,类似java的构造函数重载,但是kotlin不推荐定义很多副构造函数(会增加复杂度),还是推荐定义一个主构造器。
- 主构造器默认参数:
- 构造同名的工厂函数,函数名也可以与类的名字不同,工厂方法比构造函数更具有表现力,因为它可以通过名字知道类是如何构造出来的
class Person(var age: Int, var name: String = "unknown") {
override fun equals(other: Any?) = (other as? Person)?.name?.equals(name) ?: false
override fun hashCode() = name.hashCode()
}
val persons = HashMap<String, Person>()
//函数名也可以与类的名字不同
fun Person(name: String): Person {
return persons[name] ?: Person(1, name).also { persons[name] = it }
}
类似String有常见的工厂方法调用:
fun main() {
val str = String()//调用的是构造函数
val str1 = String(charArrayOf('1', '2'))//调用的是工厂函数 实际是一个函数指向了String的构造函数
}
可以自己给系统String类添加工厂方法,一般写成函数名跟类名一样
fun String(ints: IntArray): String {
return ints.contentToString()
}
其实就是一个函数返回类型为对应的类
类的可见性
- 与java不同,kotlin中啥也不写,默认就是
public
的,而java中不写默认是default
包内可见 - kotlin中多一个限制可见性的
internal
关键字,去掉了default
关键字 - 对于
protected
, java是包内可见,而kotlin是类内可见,这点不同,当然子类肯定都是可见的,kotlin中protected
只能用来修饰类的成员,不能用来修饰类和顶级声明
internal VS default
internal
这个关键字比较有意思,可以在kotlin中用作模块化隔离可见性
比如在一个模块中声明:
//internal只在模块内可见,模块外的kotlin访问不到
internal class CoreApiKotlinA {
//指定JvmName使java也不能访问该方法
@JvmName("%abcd")
internal fun a(){
println("Hello A")
}
}
而在依赖它的模块中使用它会报错:
//kotlin中访问其他模块的internal的类会报错 不可见
val coreApiKotlinA = CoreApiKotlinA()
coreApiKotlinA.a()
但是通过java代码却可以使用,此时可以通过internal
方法上添加@JvmName("xxx")
注解指定一个非法的java变量可达到java不能调用的目的,但实际上java是能看到的,只不过不能打出来合法的方法名而已。
//java是可以访问internal的kotlin类的,但是可以指定JvmName使java也不能访问对应方法
CoreApiKotlinA coreApiKotlinA = new CoreApiKotlinA();
coreApiKotlinA.%abcd();
构造器的可见性
属性的可见性
- get可见性必须和属性可见性一致, 不能定义public属性的get为private
- set可见性不能大于属性的可见性, 不能定义private属性的set为public
顶级声明的可见性
- 顶级声明指文件内直接定义的属性、函数、类等
- 顶级声明不支持 protected
- 顶级声明被 private 修饰表示文件内可见
延迟初始化方案
lateinit 的注意事项:
- lateinit 会让编译器忽略变量的初始化,不支持 Int 等基本数据类型
- 开发者必须能够在完全确定变量值的生命周期下使用 lateinit
- 不要在复杂的逻辑中使用lateinit,它只会让你的代码更加脆弱
使用 lazy:
lazy
是比较推荐的延迟初始化方式,实际上它是一个属性代理
接口代理
接口代理其实就是可以把一些没必要实现的接口方法隐藏起来不去实现,方便一些,而不用每一个接口都要写一下。其中by
关键字右边的就是实际代理类对象,它是构造函数中的一个属性,by
关键字左边的是代理类对象实现的接口。
例子:
//超级数组 同时支持list和map接口,通过接口代理的方式不必实现list和map接口的所有方法
class SuperArray<E>(
private val list: MutableList<E?> = ArrayList(),
private val map: MutableMap<Any, E> = HashMap()
) : MutableList<E?> by list, MutableMap<Any, E> by map {
override fun isEmpty() = list.isEmpty() && map.isEmpty()
override val size: Int
get() = list.size + map.size
override fun clear() {
list.clear()
map.clear()
}
override operator fun set(index: Int, element: E?): E? {
if (list.size <= index) {
repeat(index - list.size + 1) {
list.add(null)
}
}
return list.set(index, element)
}
override fun toString(): String {
return """List: [$list]; Map: [$map]"""
}
}
fun main() {
val superArray = SuperArray<String>()
val superArray2 = SuperArray<String>()
superArray += "Hello"
superArray["Hello"] = "World"
superArray2[superArray] = "World"
superArray[1] = "world"
superArray[4] = "!!!"
println(superArray)
println(superArray2)
}
属性代理 - lazy
lazy
属性代理 代理了Person实例的firstName的getter
方法,实际是一个函数 传递一个lambda表达式
class Person(val name: String){
//lazy属性代理 代理了Person实例的firstName的getter方法
// 实际是一个函数 传递一个lambda表达式
val firstName by lazy {
name.split(" ")[0]
}
val lastName by lazy {
name.split(" ")[1]
}
}
observable代理属性,监听set值变化:
class StateManager {
//observable代理属性,监听set值变化
var state: Int by Delegates.observable(0) {
property, oldValue, newValue ->
println("State changed from $oldValue -> $newValue")
}
}
自定义代理属性:
class Foo {
val x: Int by X()
var y: Int by X()
}
class X {
operator fun getValue(thisRef: Any?, property: KProperty<*>): Int {
return 2
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, i: Int) {
}
}
其中getValue和setValue方法的参数写法是固定的
调用:
fun main() {
val stateManager = StateManager()
stateManager.state = 3
stateManager.state = 4
println(Foo().x)
}
自定义实例:读取Config.properties中的配置项
Config.properties文件中一般是key-value的配置
author=xxxx
version=1.0
desc=This is a demo.
class PropertiesDelegate(private val path: String, private val defaultValue: String = ""){
private lateinit var url: URL
private val properties: Properties by lazy {
val prop = Properties()
url = try {
javaClass.getResourceAsStream(path).use {
prop.load(it)
}
javaClass.getResource(path)
} catch (e: Exception) {
try {
ClassLoader.getSystemClassLoader().getResourceAsStream(path).use {
prop.load(it)
}
ClassLoader.getSystemClassLoader().getResource(path)!!
} catch (e: Exception) {
FileInputStream(path).use {
prop.load(it)
}
URL("file://${File(path).canonicalPath}")
}
}
prop
}
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return properties.getProperty(property.name, defaultValue)
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
properties.setProperty(property.name, value)
File(url.toURI()).outputStream().use {
properties.store(it, "Hello!!")
}
}
}
abstract class AbsProperties(path: String){
protected val prop = PropertiesDelegate(path)
}
class Config: AbsProperties("Config.properties"){
var author by prop
var version by prop
var desc by prop
}
fun main() {
val config = Config()
println(config.author)
config.author = "Bennyhuo"
println(config.author)
}
其实主要还是实现getValue
和setValue
方法就可以,需要注意的是签名写法,setValue
最后一个参数是设置的值的类型,而getValue
的返回值就是对应获取的值的类型。
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
}
只要定义好了getValue
和setValue
方法,然后就可以通过by
操作符去代理了
使用形式:var
变量 by
【实现getValue
和setValue
的类】的实例()
Kotlin单例
只需要类前面添加object
关键字即可,object
定义类等价于java的恶汉式的单例模式
object Singleton {
var x: Int = 2
fun y(){ }
init {
//object不能定义构造函数,但可以定义init块
}
}
object不能定义构造函数,但可以定义init
块
使用:
fun main() {
Singleton.x
Singleton.y()
}
@JvmStatic
和 @JvmField
object Singleton {
@JvmField var x: Int = 2 //生成java的静态成员 不会生成getter和setter方法
@JvmStatic fun y(){ } //生成java的静态方法
}
- 静态成员
@JvmStatic
object
修饰的类内部方法相当于静态方法,但是伪静态,也就是内部会生成一个静态的成员对象,对类的方法调用实际上是调用的内部静态成员对象的方法,只有在方法上添加@JvmStatic
才会真正的生成静态方法。
- 不生成 gettter 和 setter
@JvmField
普通kotlin类(非单例)中使用使用JvmField
和JvmStatic
:
class Foo {
//普通类中使用JvmField和JvmStatic
companion object {
@JvmField var x: Int = 2
@JvmStatic fun y(){ }
}
}
注意,加的注解@JvmField
和@JvmStatic
只针对java平台的可用,其他平台并不可行。
单例的object类仍可以继承类:
object Singleton: Runnable{
override fun run() {
}
}
内部类
在kotlin
中类内部的class
前面不写修饰符默认就是 静态内部类
,class前面写 inner
修饰符才是java中的 普通内部类
,与java一样,普通内部类会持有外部类的对象引用。
class Outer {
//普通内部类 与java一样会持有外部类的引用
inner class Inner
//默认静态内部类
class StaticInner
}
fun main() {
val inner = Outer().Inner()
val staticInner = Outer.StaticInner()
}
object类内部的object类默认是静态的 不存在非静态的情况 不能定义成inner
object OuterObject {
//内部object默认是静态的 不存在非静态的情况 不能定义成inner
object StaticInnerObject
}
匿名内部类
fun main() {
//匿名内部类
object : Cloneable {
}
}
其实就是object 省略类名直接实现接口。
匿名内部类 可用继承多个接口,java不行:
fun main() {
// 类型是混合类型:Cloneable & Runnable
val runnableCloneable = object : Cloneable, Runnable {
override fun run() {
}
}
}
实现多个接口时,它的类型是多个接口类型的混合类型。
Local class :
fun main() {
//本地函数
fun localFunc(){
println("Hello")
}
//对应Java的local class
class LocalClass: Cloneable, Runnable{
override fun run() {}
}
}
可以对比java的local class实现,其实就是在静态函数的内部定义一个类:
public class JavaInnerClasses {
public static void main(String... args) {
class LocalClass implements Cloneable, Runnable {
@Override
public void run() { }
}
}
}
说实话,写了这么多年代码,未曾这么干过。。
数据类
kotlin中提供一个data
关键字,data
修饰的类就是一个数据类,对标java的bean类:
data class Book(val id: Long, val name: String, val author: Person)
data class Person(val id: Long, val name: String, val age: Int)
与java的bean类相比,kotlin的data类不能被继承,并且属性要全部写到构造函数当中,没有无参的构造函数。确实简便了许多!
并且编译器会为data类生成了一些好用的方法:
val book = Book(0, "Kotlin in Action", Person(1, "Dmitry", 40))
//编译器生成的方法 copy component1
book.copy()
val id = book.component1()
val name = book.component2()
val author = book.component3()
其中copy()
和 component1()
等都是编译器自动生成的,component
方法的意义是方便解构赋值的使用:
// 解构赋值,对应的字段是通过component方法获取的
val (id, name, author) = book
关于解构:
//解构
val pair = "Hello" to "World"
val (hello, world) = pair
自己实现解构方法:
class Student(var id: Int, var name: String, var sex: Char) {
// component 不能写错
operator fun component1(): Int = id
operator fun component2(): String = name
operator fun component3(): Char = sex
operator fun component4(): String = "KT Study OK"
}
fun main() {
val stu = Student(4545, "Derry", 'M')
val (n1, n2, n3, n4) = stu
}
Kotlin的数据类内部其实会生成上面的三个component
方法:
data class User(val id: Int, val name: String, val sex: Char)
fun main() {
val u = User(1, "Derry", 'M')
val (v1, v2, v3) = u.copy()
println("v1: $v1, v2: $v2, v3: $v3")
}
例如上面代码翻译后就会看到:
data 不能被继承,那么为啥不能有子类呢?
可以先看一下kotlin为data类生成的对应的java类是什么样的,查看方式,doule shift键,然后Actions中输入kotlin Bytecode显示:
点击Decompile即可查看对应生成的java代码
以下是Book的数据类对应生成的java代码
public final class Book {
private final long id;
@NotNull
private final String name;
@NotNull
private final Person author;
public final long getId() {
return this.id;
}
@NotNull
public final String getName() {
return this.name;
}
@NotNull
public final Person getAuthor() {
return this.author;
}
public Book(long id, @NotNull String name, @NotNull Person author) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(author, "author");
super();
this.id = id;
this.name = name;
this.author = author;
}
public final long component1() {
return this.id;
}
@NotNull
public final String component2() {
return this.name;
}
@NotNull
public final Person component3() {
return this.author;
}
@NotNull
public final Book copy(long id, @NotNull String name, @NotNull Person author) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(author, "author");
return new Book(id, name, author);
}
// $FF: synthetic method
public static Book copy$default(Book var0, long var1, String var3, Person var4, int var5, Object var6) {
if ((var5 & 1) != 0) {
var1 = var0.id;
}
if ((var5 & 2) != 0) {
var3 = var0.name;
}
if ((var5 & 4) != 0) {
var4 = var0.author;
}
return var0.copy(var1, var3, var4);
}
@NotNull
public String toString() {
return "Book(id=" + this.id + ", name=" + this.name + ", author=" + this.author + ")";
}
public int hashCode() {
long var10000 = this.id;
int var1 = (int)(var10000 ^ var10000 >>> 32) * 31;
String var10001 = this.name;
var1 = (var1 + (var10001 != null ? var10001.hashCode() : 0)) * 31;
Person var2 = this.author;
return var1 + (var2 != null ? var2.hashCode() : 0);
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Book) {
Book var2 = (Book)var1;
if (this.id == var2.id && Intrinsics.areEqual(this.name, var2.name) && Intrinsics.areEqual(this.author, var2.author)) {
return true;
}
}
return false;
} else {
return true;
}
}
}
除了类名方法名前面添加了final
关键字不允许继承以外,还生成了许多方法,其中有重写hashCode()
和equals()
方法,所以如果有一个类继承了data类,可能导致属性变化,从而导致hashCode()
和equals()
方法的结果不一致。
如果有子类的话,会发生什么?
如何合理的使用 data class :
data类的属性最好全部为基本类型或者其他data类型,保持它的纯净性。
另外,有方法可以破除data
类的不可继承性,也有网友在吐槽kotlin的这个设计,觉得它不好,其实如果你想用一个可以继承到类,只需要把data
关键字去掉,建一个普通类就好了。kotlin这样设计肯定是想保持它的纯洁性,如果可继承,只会变的更复杂。
枚举类
kotlin里面的枚举类跟java差不多
enum class State {
Idle, Busy
}
//枚举定义构造函数 同java
enum class State1(val id: Int) {
Idle(0), Busy(1)
}
enum class Color {
White, Red, Green, Blue, Yellow, Black
}
fun main() {
State.Idle.name // Idle
State.Idle.ordinal // 0
val state = State.Idle
//枚举全部值
val value = when (state) {
State.Idle -> { 0 }
State.Busy -> { 1 }
}
//枚举创建区间
val colorRange = Color.White .. Color.Green
val color = Color.Blue //Blue不在区间内
println(color in colorRange)
}
枚举类不可继承其他类,因为枚举类有父类是enum
,但是可以实现接口:
enum class State: Runnable{
Idle, Busy{
override fun run() {
println("For Busy State.")
}
};
override fun run() {
println("For Every State.")
}
}
fun main() {
State.Idle.run()
State.Busy.run()
}
枚举类可以定义扩展函数:
fun State.successor(): State? {
return State.values().let {
if (ordinal + 1 >= it.size) null
else it[ordinal + 1]
}
}
fun State.predecessor(): State? {
return State.values().let {
if (ordinal - 1 < 0) null
else it[ordinal - 1]
}
}
fun main() {
println(State.Idle.successor())
println(State.Busy.successor())
}
密封类
- 密封类是一种特殊的抽象类
- 密封类的子类必须定义在与自身相同的文件中
- 密封类的子类的个数是有限的
其实就是一个只能在同一个文件中定义子类的抽象类。定义方式是在类的前面加sealed
关键字
sealed class PlayerState
object Idle : PlayerState()
class Playing(val song: Song) : PlayerState() {
fun start() {}
fun stop() {}
}
class Error(val errorInfo: ErrorInfo) : PlayerState() {
fun recover() {}
}
完整的示例:控制播放器播放状态的例子
data class Song(val name: String, val url: String, var position: Int)
data class ErrorInfo(val code: Int, val message: String)
object Songs {
val StarSky = Song("Star Sky", "https://fakeurl.com/321144.mp3", 0)
}
sealed class PlayerState
object Idle : PlayerState()
class Playing(val song: Song) : PlayerState() {
fun start() {}
fun stop() {}
}
class Error(val errorInfo: ErrorInfo) : PlayerState() {
fun recover() {}
}
class Player {
var state: PlayerState = Idle
fun play(song: Song) {
this.state = when (val state = this.state) {
Idle -> {
Playing(song).also(Playing::start)
}
is Playing -> {
state.stop()
Playing(song).also(Playing::start)
}
is Error -> {
state.recover()
Playing(song).also(Playing::start)
}
}
}
}
fun main() {
val player = Player()
player.play(Songs.StarSky)
}
注意其中的when
表达式的使用,可见它可以用来替代枚举类做类似的功能,子类的个数也是全部可枚举的。跟枚举类有相似之处。
密封类 VS 枚举类:
内联类
内联类的概念:
- 内联类是对某一个类型的包装
- 内联类是类似于 Java 装箱类型的一种类型
- 内联类编译器会尽可能使用被包装的类型进行优化
内联类的限制:
- 主构造器必须有且仅有一个只读属性
- 不能定义有 backing-field 的其他属性
- 被包装类型不能是泛型类型
- 不能继承父类也不能被继承
简而言之,内联类是一种类型的包装类,类前面加inline
关键字,构造器只能有一个参数,不能继承类,不能被继承,不能作为内部类。类似于java的Integer、Double这种,但有不同。
内联类虽然不能继承类或被继承,但是可以实现接口。
//只能有方法不能有属性
inline class BoxInt(val value: Int): Comparable<Int> {
override fun compareTo(other: Int)
= value.compareTo(other)
operator fun inc(): BoxInt {
return BoxInt(value + 1)
}
}
BoxInt会做编译优化,只有在需要的时候才使用包装类 多数情况下直接使用Int
,可以反编译字节码生成的java代码查看。另外,还有个特点就是它只能有方法不能有属性。
内联类也可以用来模拟枚举类,与枚举相比内存占用小,但使用方式类似
inline class State(val ordinal: Int) {
companion object {
val Idle = State(0)
val Busy = State(1)
}
fun values() = arrayOf(Idle, Busy)
val name: String
get() = when (this) {
State.Idle -> "Idle"
State.Busy -> "Busy"
else -> throw IllegalArgumentException()
}
}
inline class Color(val value: UInt) {
companion object {
val Red = Color(0xFFFF0000u)
val Green = Color(0xFF00FF00u)
val Blue = Color(0xFF0000FFu)
}
fun values() = arrayOf(Red, Green, Blue)
val name: String
get() = when (this) {
Red -> "Red"
Green -> "Green"
Blue -> "Blue"
else -> throw IllegalArgumentException()
}
}
fun main() {
State.Busy
Color.Blue
var boxInt = BoxInt(5)
if(boxInt < 10){
println("value is less than 10")
}
val newValue = boxInt.value * 200
println(newValue)
boxInt++
println(boxInt)
}
typealias VS inline class:
Json解析
Kotlin中解析Json有那么几种方式:
- Gson
- Moshi
- kotlinx.serialization
其中Gson是原来经常使用的Google的解析库,用这个在kotlin中使用已经不适合,会有很多情况不支持,如空类型 默认值等。然后就是moshi,这个是square出品的,还有就是Kotlin自带的kotlinx.serialization,不过kotlin自带的居然不支持java类。显然对于android开发者来说,如果你使用kotlin一般都是java和kotlin混合使用的,所以首选的还是moshi这个库。如果是纯kotlin的话,还是选官方自带的比较好。
框架对比:
这里补充一个比较不错的库:klaxon 这个也是用来解析json的kotlin库,貌似支持的功能也比较丰富,后面有空再详细了解一下吧。这里先看一下moshi。
moshi的话,使用可以看官网:https://github.com/square/moshi
这里有一篇更详细的中文使用介绍:新一代Json解析库Moshi使用及原理解析
这里主要简单记录一下moshi的使用方式,首先gradle需要添加依赖:
dependencies {
...
//moshi
implementation("com.squareup.moshi:moshi:1.11.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.11.0")
}
还需要应用kotlin的apt插件,否则kapt()方法找不到
apply plugin: 'kotlin-kapt'
moshi官方提供了两种方式,一是反射方式,二是通过注解处理器,可以选其一,也可以都用。我上面的依赖是采用的注解处理器的方式,因为采用反射方式的话,需要引入一个额外的依赖库:
implementation("com.squareup.moshi:moshi-kotlin:1.11.0")
但是使用反射库会依赖导入一个2.5M大小的jar包,这么大。。这还得了!所以我们肯定不会选这种方式了。。。
然后就是代码,只需要在data class上面添加注解:
@JsonClass(generateAdapter = true)
data class Person(val name: String, val age: Int = 18)
序列化和反序列化:
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(Person::class.java)
val json = jsonAdapter.toJson(Person("张三", 25))
//val json = jsonAdapter.toJson(Person("张三"))
Log.e(TAG, "json : ${json}") //{"name":"张三", "age": 25}
val person = jsonAdapter.fromJson("""{"name":"张三"}""")
Log.e(TAG, "name: ${person?.name}") //张三
Log.e(TAG, "age: ${person?.age}") //18
还有很多高级的用法,具体参考官网介绍。
上面的data类给定了默认值,moshi在序列化和反序列化时,是识别这个值的,如果没有给定值就采用默认值。
moshi更好的支持空类型安全,如果把上面的数据类的默认参数去掉:
@JsonClass(generateAdapter = true)
data class Person(val name: String, val age: Int)
//这行会直接报空类型异常
val person = jsonAdapter.fromJson("""{"name":"张三"}""")
Log.e(TAG, "name: ${person?.name}")
Log.e(TAG, "age: ${person?.age}")
moshi进行json反序列化为KClass时,如果filed是Nullable类型,则可以填入Null,如果是NonNull类型,则在填入Null时会立即抛出异常,将NPE风险暴露在早期,如果是java的话,则会到调用 person.name 的时候才会暴露。
也就是说,如果后台接口少返回了我们定义的data类中的非空类型的属性字段,在生成对象的时候就会报异常,而不是具体使用的时候。
moshi还支持属性延时初始化:
@JsonClass(generateAdapter = true)
data class PersonWithInits(val name: String, val age: Int){
val firstName by lazy {
name.split(" ")[0]
}
//@Transient
val lastName = name.split(" ")[1]
}
fun main(){
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(PersonWithInits::class.java)
println(jsonAdapter.toJson(PersonWithInits("Hello world", 18))) // {"name":"Hello world","age":18}
val personWithInits = jsonAdapter.fromJson("""{"name":"Hello world","age":20, "lastName":"tom"}""")
println(personWithInits?.firstName) // Hello
println(personWithInits?.lastName) // world firstName和lastName是以name为准
}
最后记录一个AS插件 JsonToKotlinClass ,类似于原来的GsonFormat插件,可以类似的根据Json字符串生成Kotlin的 data class
。
泛型
泛型约束:
fun <T : Comparable<T>> maxOf(a: T, b: T): T {
return if (a > b) a else b
}
fun main() {
val max = maxOf("Hello", "World")
println(max) //输出World
}
多个约束:
多个泛型参数:
多个泛型参数,可以每个泛型都有约束:
fun <T, R> callMax(a: T, b: T): R
where T : Comparable<T>, T : () -> R, //T有两个约束
R : Number { //R有一个约束
return if (a > b) a() else b()
}
类似的还有Map类,它的K 和V泛型都进行了约束:
class Map<K, V> where K : Serializable, V : Comparable<V>
协变
协变点:
协变点举例:
协变小结:
- 子类 Derived 兼容父类 Base
- 生产者
Producer<Derived>
兼容Producer<Base>
- 存在协变点的类的泛型参数必须声明为协变或不变
- 当泛型类作为泛型参数类实例的生产者时用协变
简而言之就是用out
关键字修饰的泛型就是协变, 返回值为协变泛型类型的称为协变点。
例子:
interface Book
interface EduBook : Book
class BookStore<out T : Book> {
fun getBook(): T {
TODO()
}
}
fun covariant(){
val eduBookStore: BookStore<EduBook> = BookStore<EduBook>()
val bookStore: BookStore<Book> = eduBookStore
val book: Book = bookStore.getBook() //书店只能获取普通的书
val eduBook : EduBook = eduBookStore.getBook() //教辅书店只能获取教辅书籍
}
逆变
逆变点:
逆变小结:
- 子类 Derived 兼容父类 Base
- 生产者
Producer<Base>
兼容Producer<Derived>
- 存在逆变点的类的泛型参数必须声明为逆变或不变
- 当泛型类作为泛型参数类实例的消费者时用逆变
简而言之就是用in
关键字修饰的泛型就是逆变,作为函数输入参数的泛型称为逆变点。逆变点主要是指输入的参数类型,是消费者。并且消费者的继承关系跟协变是相反的。
例子:
open class Waste
class DryWaste : Waste()
class Dustbin<in T : Waste> {
fun put(t: T) {
TODO()
}
}
fun contravariant(){
val dustbin: Dustbin<Waste> = Dustbin<Waste>()
val dryWasteDustbin: Dustbin<DryWaste> = dustbin
val waste = Waste()
val dryWaste = DryWaste()
//普通垃圾桶可以放入任何垃圾
dustbin.put(waste)
dustbin.put(dryWaste)
//干垃圾桶只能放入干垃圾,不能放普通垃圾
// dryWasteDustbin.put(waste)
dryWasteDustbin.put(dryWaste)
}
星投影
*
可用在变量类型声明的位置*
可用以描述一个未知的类型*
所替换的类型在协变点返回泛型参数上限类型,在逆变点接收泛型参数下限类型
协变点:
星投影在所有逆变点的下限类型是Nothing, 因此不能用在属性或函数上。
星投影的适用范围:
说白了只是一个描述符,可以简写泛型参数而已。
fun main() {
val queryMap: QueryMap<*, *> = QueryMap<String, Int>()
queryMap.getKey()
queryMap.getValue()
val f: Function<*, *> = Function<Number, Any>()
//f.invoke()
if (f is Function) {
(f as Function<Number, Any>).invoke(1, Any())
}
maxOf(1, 3)
HashMap<String, List<*>>()
//endregion
val hashMap: HashMap<*, *> = HashMap<String, Int>()
//hashMap.get()
}
class QueryMap<out K : CharSequence, out V : Any> {
fun getKey(): K = TODO()
fun getValue(): V = TODO()
}
fun <T : Comparable<T>> maxOf(a: T, b: T): T {
return if (a > b) a else b
}
class Function<in P1, in P2> {
fun invoke(p1: P1, p2: P2) = Unit
}
泛型擦除(伪泛型)
泛型实现原理:
泛型实现对比:
Java与Kotlin实现机制一样,在运行时擦除真正的类型,C#则会真的生成一个类型去执行。
内联特化
内联特化在调用的地方会替换到调用处,因此这时类型是确定的了,即已经特化成某个具体类型。通过fun前面的关键字 inline 和泛型参数T前面的 reified 参数两个来指定泛型参数在调用处实例化。
内联特化实际应用:
inline fun <reified T> genericMethod(t: T){
//val t = T()
val ts = Array<T>(3) { TODO() }
val jclass = T::class.java
val list = ArrayList<T>()
if(list is List<*>){
println(list.joinToString())
}
}
class Person(val age: Int, val name: String)
inline fun <reified T> Gson.fromJson(json: String): T = fromJson(json, T::class.java)
fun main() {
val gson = Gson()
val person2: Person = gson.fromJson("""{"age":18,"name":"Bennyhuo"}""")
val person3 = gson.fromJson<Person>("""{"age":18,"name":"Bennyhuo"}""")
}
实例:模仿的Self Type
typealias OnConfirm = () -> Unit
typealias OnCancel = () -> Unit
private val EmptyFunction = {}
open class Notification(
val title: String,
val content: String
)
class ConfirmNotification(
title: String,
content: String,
val onConfirm: OnConfirm,
val onCancel: OnCancel
) : Notification(title, content)
interface SelfType<Self> {
val self: Self
get() = this as Self //当前类型强转成Self类型
}
//泛型添加约束只能传子类
open class NotificationBuilder<Self: NotificationBuilder<Self>>: SelfType<Self> {
protected var title: String = ""
protected var content: String = ""
fun title(title: String): Self {
this.title = title
return self //返回接口的常量属性即可,运行时就是当前子类实际类型
}
fun content(content: String): Self {
this.content = content
return self
}
open fun build() = Notification(this.title, this.content)
}
class ConfirmNotificationBuilder : NotificationBuilder<ConfirmNotificationBuilder>() {
private var onConfirm: OnConfirm = EmptyFunction
private var onCancel: OnCancel = EmptyFunction
fun onConfirm(onConfirm: OnConfirm): ConfirmNotificationBuilder {
this.onConfirm = onConfirm
return this
}
fun onCancel(onCancel: OnCancel): ConfirmNotificationBuilder {
this.onCancel = onCancel
return this
}
override fun build() = ConfirmNotification(title, content, onConfirm, onCancel)
}
fun main() {
ConfirmNotificationBuilder()
.title("Hello")
.onCancel {
println("onCancel")
}.content("World")
.onConfirm {
println("onConfirmed")
}
.build()
.onConfirm()
}
如果不定义SelfType类型,则子类在调用ConfirmNotificationBuilder().title(“Hello”)之后不能再继续调用子类的onCancel 方法,因为返回的是父类型,但是实际运行时这个类型是子类型。
实例: 基于泛型实现 Model 实例的注入
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KProperty
abstract class AbsModel {
init {
Models.run { this@AbsModel.register() }
}
}
class DatabaseModel : AbsModel() {
fun query(sql: String): Int = 0
}
class NetworkModel : AbsModel() {
fun get(url: String): String = """{"code": 0}"""
}
class SpModel : AbsModel() {
init {
Models.run { register("SpModel2") }
}
fun hello() = println("HelloWorld")
}
object Models {
private val modelMap = ConcurrentHashMap<String, AbsModel>()
fun AbsModel.register(name: String = this.javaClass.simpleName) {
modelMap[name] = this
}
//String扩展函数
fun <T: AbsModel> String.get(): T {
return modelMap[this] as T
}
}
fun initModels() {
DatabaseModel()
NetworkModel()
SpModel()
}
//object单例
object ModelDelegate {
operator fun <T: AbsModel> getValue(thisRef: Any, property: KProperty<*>): T {
return Models.run {
property.name.capitalize().get() //利用了String的扩展函数
}
}
}
class MainViewModel {
//利用属性代理by by左边指定类型推导泛型
val databaseModel: DatabaseModel by ModelDelegate
val networkModel: NetworkModel by ModelDelegate
val spModel: SpModel by ModelDelegate
val spModel2: SpModel by ModelDelegate
// val compileKotlin: KotlinCompile by tasks //gradle这种写法类似,tasks也是属性代理
// val compileTestKotlin: KotlinCompile by tasks
}
fun main() {
initModels()
val mainViewModel = MainViewModel()
mainViewModel.databaseModel.query("select * from mysql.user").let(::println)
mainViewModel.networkModel.get("https://www.imooc.com").let(::println)
mainViewModel.spModel.hello()
mainViewModel.spModel2.hello()
}
反射
反射的依赖:
反射常用数据结构:
反射常用数据结构:Kotlin vs Java
基本上和java是一一对应的,其中KType表示的是未擦除的泛型,KClass获取的是实际运行时的类型,不带泛型参数的。
Kotlin使用反射的话,唯一一点不好的就是需要引入一个体积非常大的库:
dependencies {
// kotlin反射库,大小2.5M左右
implementation "org.jetbrains.kotlin:kotlin-reflect:1.4.20"
}
没错,有2.5M,不过编译后的大小还能接受
kotlin获取KClass通过两个冒号
var cls: KClass<String> = String::class
cls.java // 转成java的Class<String>
cls.java.kotlin // 再转回到kotlin的KClass
// 获取定义在类中的属性(直接写在类当中的)
val property = cls.declaredMemberProperties.firstOrNull()
KClass是不带泛型的类,typeOf能拿到具体的泛型实际类型:
val mapCls = Map::class
println(mapCls) // 输出class kotlin.collections.Map
val mapType = typeOf<Map<String, Int>>() // 拿到 KType
mapType.arguments.forEach { // 拿到 KType 中的每个参数泛型的类型
println(it) // 输出 kotlin.String 和 kotlin.Int
}
拿到KClass之后可以通过KClass的方法获取各种其他属性了
一个简单的示例:
open class S(val sex : Boolean) {
fun superFun() {
}
}
class A(val name : String, sex: Boolean) : S(sex) {
fun String.hello(){
}
fun foo() {
}
var age : Int = 0;
}
fun A.test() {
}
上面的class A继承了一个类,并且类的内部有其他类定义的扩展方法,本身也定义了一个扩展方法。
KClass提供了很多方法获取类的属性和方法,但是有一些区别,方法比较多,可以看一下区别:
fun main() {
// 能得到:age, name, foo(), (String.)hello(), equals(), hashCode(), toString(), sex, superFun()
println(A::class.members) 获取所有的成员属性和方法,包括其他类的扩展方法,包括父类的方法,但不包括构造方法
// 能得到:foo(), (String.)hello(), equals(), hashCode(), toString(), superFun()
println(A::class.functions) 获取所有的方法, 包括扩展方法,包括父类的
// 能得到:age, name, sex
println(A::class.memberProperties) 获取所有的成员属性 非扩展, 包括父类
// 能得到:foo(), equals(), hashCode(), toString(), superFun()
println(A::class.memberFunctions) 获取所有的成员方法 非扩展, 包括父类
// 能得到:(String.)hello()
println(A::class.memberExtensionFunctions) 获取所有的扩展方法, 包括父类
// 能得到:[]
println(A::class.memberExtensionProperties) 获取所有的扩展属性, 包括父类
// 能得到:age, name
println(A::class.declaredMemberProperties) 获取到所有定义的属性 当前类
// 能得到:foo()
println(A::class.declaredMemberFunctions) 获取到所有定义的方法(普通方法,非扩展方法,非静态方法)当前类
// 能得到:age, name, foo(), (String.)hello()
println(A::class.declaredMembers) 获取到所有定义的成员包括属性和方法(普通方法和扩展方法)当前类
// 能得到:foo(), (String.)hello()
println(A::class.declaredFunctions) 获取到所有定义的方法(普通方法和扩展方法) 如果是java类可以获取父类的方法
// 能得到:(String.)hello()
println(A::class.declaredMemberExtensionFunctions) 获取定义在当前类的扩展方法
// 能得到:[]
println(A::class.declaredMemberExtensionProperties) 获取定义在当前类的扩展属性
}
可以看出以declared开头的方法基本上只能获取当前类的属性和方法,不带declared开头的方法则同时可以获取到父类的相关属性和方法。
还有一点需要注意的是,这里kotlin里面所指的扩展属性和扩展方法一般是指直接写在当前类中的其他类的扩展方法,如上面的A里面的String.hello()方法。如果是A类在某个地方定义的扩展方法是获取不到的,如上面的A.test()方法。这点跟java有点不一样。
nestedClasses
获取内部类
B::class.nestedClasses//获取内部类
objectInstance
获取object单例的实例,如果不是单例类则返回可能为null
B::class.objectInstance?.hello() //获取object实例
A::class.objectInstance?.foo() //如果类不是一个object, 则返回null
类内部的其他类如何获取外部类的实例对象:
class A {
fun String.hello(){
this 表示当前String对象
this@A 表示外部当前的class A对象
}
}
java也是一样,内部类获取外部类的实例时需要通过,A.this
获取
获取泛型实参
1.获取接口某个方法的返回值类型的泛型参数
interface Api {
fun getUsers(): List<UserDTO>
}
获取上面 Api 接口的 getUsers() 返回类型的泛型参数类 UserDTO
有几种方式,第一种是根据name来比较判断找到对应的方法:
//获取到 Api的getUsers() 方法 通过 filter
val functions = Api::class.declaredFunctions.filter { it.name == "getUsers" }
val getUsers : KFunction<*> = functions.get(0)
getUsers.returnType.arguments.forEach {
println("getUser的返回值泛型:${it}")
}
//获取函数的返回值参数的泛型类型UserDTO 通过 first
Api::class.declaredFunctions.first { it.name == "getUsers" }
.returnType.arguments.forEach {
println("getUser的返回值泛型:${it}")
}
还可以直接通过函数引用获取 Api::getUsers
得到的就是一个KFunction
Api::getUsers.returnType.arguments.forEach {
println("getUser的返回值泛型2:${it}")
}
显然这种方式最简单了。
还可以通过java的反射方式来获取:
//Api::class.java是获取到对应java的class Class<Api> 然后可以调用java的反射方法获取泛型类型
Api::class.java.getDeclaredMethod("getUsers")
.genericReturnType.safeAs<ParameterizedType>()?.actualTypeArguments?.forEach {
println(it)
}
//safeAs是定义的一个Any的扩展方法
fun <T> Any.safeAs(): T? {
return this as? T
}
//safeAs扩展方法可以简写下面的代码,等价于上面的代码
(Api::class.java.getDeclaredMethod("getUsers")
.genericReturnType as ParameterizedType).actualTypeArguments?.forEach {
println(it)
}
只能说java的方式也可以,但是这种也太麻烦了。。还是全部用kotlin的方法吧,不然得各种强转各种判空?.
2.获取接口类的泛型
abstract class SuperType<T> {
//kotlin反射方法获取
val typeParameter by lazy {
//this是实际运行子类型, supertypes拿到父类型,first是第一个父类型即SuperType,arguments获取到泛型参数列表,只有一个可以first(),
// first()方法返回的是KTypeProjection,KTypeProjection.type才返回KType
this::class.supertypes.first().arguments.first().type!!
}
//java反射方法获取
val typeParameterJava by lazy {
this.javaClass.genericSuperclass.safeAs<ParameterizedType>()!!.actualTypeArguments.first()
}
}
open class SubType : SuperType<String>()
获取上面 SubType类实现的SuperType接口类的泛型:
val subType = SubType()
subType.typeParameter.let(::println) // kotlin.String
subType.typeParameterJava.let(::println) // class java.lang.String java获取的永远是java类型的描述
关键代码就是这句:this::class.supertypes.first().arguments.first().type
这里的话主要注意这个this运行时是实际的子类型(OO多态),所以最后是可以直接强转的。
上面代码是只有一个父类,如果有多个父类,会有问题,需要修改一下:
abstract class SuperType<T> {
val typeParameter2 by lazy {
//实际中,如果子类是open可继承的可能还会有子类,具体要看使用的类
//此时需要找到合适的父类再操作,可以根据名字去比较,这里示例直接判断不是空的
this::class.allSupertypes.first { it.arguments.isNotEmpty() }.arguments.first().type!!
//等价上面
//this::class.allSupertypes.filter{ it.arguments.isNotEmpty()}.first().arguments.first().type!!
}
}
open class SubType : SuperType<String>()
class SubType2: SubType()
获取上面 SubType2类的父类实现的SuperType接口类的泛型:
val subType2 = SubType2()
subType2.typeParameter2.let(::println) // kotlin.String
实例:为数据类实现 DeepCopy
fun <T : Any> T.deepCopy(): T {
//是数据类data class 才拷贝
if(!this::class.isData){
return this
}
//primaryConstructor获取主构造器,因为执行到这里的是数据类肯定有主构造器,所以!!强转,不用判空
return this::class.primaryConstructor!!.let {
primaryConstructor ->
primaryConstructor.parameters.map { parameter ->
//(this::class as KClass<T>)逆变转协变
val value = (this::class as KClass<T>).memberProperties.first { it.name == parameter.name } //成员属性名和构造函数的参数名相等
.get(this)
//classifier先转成KClass,然后判断是否是数据类
if((parameter.type.classifier as? KClass<*>)?.isData == true){
parameter to value?.deepCopy() //如果value是数据类继续调用value的deepCopy()方法深拷贝 递归
} else {
parameter to value // 如果value不是数据类直接返回,(K to V)是返回一个Pair对象
}
}.toMap() //Pair集合转Map集合
.let(primaryConstructor::callBy) //callBy调用构造函数构造对象, callBy需要一个Map<KParameter, Any?>参数就是当前的map对象
}
}
调用测试代码:
data class Person(val id: Int, val name: String, val group: Group)
data class Group(val id: Int, val name: String, val location: String)
fun main() {
val person = Person(
0,
"hello",
Group(
0,
"Kotliner.cn",
"China"
)
)
val copiedPerson = person.copy()
val deepCopiedPerson = person.deepCopy()
println(person === copiedPerson) //false
println(person === deepCopiedPerson) //false
println(person.group === copiedPerson.group) //true for shallow copy.
println(person.group === deepCopiedPerson.group) //false
println(deepCopiedPerson)
}
上面的例子中主要有几点需要注意的:
this::class.isData
判断是否是数据类 data classthis::class.primaryConstructor
获取主构造器,因为是数据类一定有主构造器,所以可以强转!!
primaryConstructor.parameters
let里面调用当前primaryConstructor对象的parameters获取所有的构造器参数this::class as KClass<T>
逆变转协变,否则this.class返回一个协变点out T, 而get()方法接受一个逆变点,会报错memberProperties.first { it.name == parameter.name }
数据类的特点是构造器的参数名和成员的属性名相等parameter.type.classifier as? KClass<*>
type参数需要调用classifier先转成KClass然后再判断是否是数据类parameter to value?.deepCopy()
如果value是数据类继续调用value的deepCopy()方法深拷贝,这里是一个递归调用,K to V 是返回的一个 Pair对象.toMap()
将Pair集合转Map集合.let(primaryConstructor::callBy)
调用主构造器,callsBy是KCallable接口的方法,KFunction是KCallable的子类,因此所有的KFunction都可以调用callBy,callBy接受一个map参数正好就是let前面返回的结果。
这个例子有一个完整的开源库代码 KotlinDeepCopy 是由大神Bennyhuo所写的,但是貌似目前没什么issue, 慎用,可以当案例学习一下。
实例:实现 Model 映射
这个例子是实现一个拷贝工作,将一个对象里的字段赋值给另一个对象里面的同名字段,跟深拷贝的例子有点相似
//任意对象转其他对象(成员属性名相同)
inline fun <reified From : Any, reified To : Any> From.mapAs(): To {
//所有成员转成Map集合再调用下面的Map转对象方法即可
return From::class.memberProperties.map { it.name to it.get(this) }
.toMap().mapAs()
}
//Map转对象(成员属性名相同) 默认只处理数据类
inline fun <reified To : Any> Map<String, Any?>.mapAs(): To {
//primaryConstructor反射调用主构造器
return To::class.primaryConstructor!!.let {
it.parameters.map {
parameter ->
// this[parameter.name] 有可能为null, 如果目标对象To的构造器参数类型可以接受null类型就直接返回一个null 否则抛异常
parameter to (this[parameter.name] ?: if(parameter.type.isMarkedNullable) null
else throw IllegalArgumentException("${parameter.name} is required but missing."))
}.toMap()
.let(it::callBy)//callBy调用主构造器构造出一个To类型的对象
}
}
第一个方法的实现实际上是调用第二个方法的,所以只需实现第二个方法即可,这里依然是先获取主构造器To::class.primaryConstructor
,获取了主构造器之后拿到它的参数列表进行map操作,map里面依然是返回当前参数 parameter to value
,to 操作符左边的是To对象的也就是目标对象, to 操作符右边的是当前调用.mapAs的map对象,因此通过this[parameter.name]访问它里面的同名参数的value值,但是这个值可能为null, 不为null就返回 ?: 左边它自身,为null还需一个处理就是如果To类型即目标类的构造函数的这个当前参数可接受可空类型 ,就直接传null, 否则抛异常。
调用测试代码:
data class UserVO(val login: String, val avatarUrl: String)
data class UserDTO(
var id: Int,
var login: String,
var avatarUrl: String,
var url: String,
var htmlUrl: String
)
fun main() {
val userDTO = UserDTO(
0,
"world",
"https://ccccccc",
"https://ddddddddd",
"https://eeeeeeeeeee"
)
val userVO: UserVO = userDTO.mapAs()
println(userVO)
val userMap = mapOf(
"id" to 0,
"login" to "hello",
"avatarUrl" to "https://aaaaaaa",
"url" to "https://bbbbbbbb"
)
val userVOFromMap: UserVO = userMap.mapAs()
println(userVOFromMap)
}
实例:可释放对象引用的不可空类型
这个例子主要是模仿了一个Android当中释放bitmap对象赋值为null的场景,在kotlin当中如果你定义了一个 var bitmap: Bitmap, 然后在onDestroy方法里面将其置为null, 但是这样写bitmap=null是不行的,因为定义的时候是一个不可空类型,这就矛盾了。
fun <T : Any> releasableNotNull() = ReleasableNotNull<T>()
class ReleasableNotNull<T: Any>: ReadWriteProperty<Any, T> {
private var value: T? = null
override fun getValue(thisRef: Any, property: KProperty<*>): T {
return value ?: throw IllegalStateException("Not initialized or released already.")
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
this.value = value
}
fun isInitialized() = value != null
fun release() {
value = null
}
}
inline val KProperty0<*>.isInitialized: Boolean
get() {
//允许反射获取
isAccessible = true
//this.getDelegate()获取属性代理的实例
return (this.getDelegate() as? ReleasableNotNull<*>)?.isInitialized()
?: throw IllegalAccessException("Delegate is not an instance of ReleasableNotNull or is null.")
}
fun KProperty0<*>.release() {
isAccessible = true
(this.getDelegate() as? ReleasableNotNull<*>)?.release()
?: throw IllegalAccessException("Delegate is not an instance of ReleasableNotNull or is null.")
}
class Bitmap(val width: Int, val height: Int)
class Activity {
private var bitmap by releasableNotNull<Bitmap>()
fun onCreate(){
//::bitmap省略了this, 默认绑定了当前对象为receiver
println(this::bitmap.isInitialized)
println(this::bitmap.isInitialized)
bitmap = Bitmap(1920, 1080)
println(::bitmap.isInitialized)
}
fun onDestroy(){
println(::bitmap.isInitialized)
::bitmap.release()
println(::bitmap.isInitialized)
}
}
fun main() {
val activity = Activity()
activity.onCreate()
activity.onDestroy()
}
这个例子中主要利用了属性代理,然后有两个比较特殊的定义分别实现了属性代理接口KProperty0
的扩展属性KProperty0<*>.isInitialized
和扩展方法KProperty0<*>.release()
。
KProperty0
表示没有receiver(其实是绑定当前调用对象this作为receiver了)KProperty1
表示有1个receiver, KProperty2
表示有2个receiver。
isAccessible = true
允许反射操作,跟java一样也要设置一个accessible为true。
this.getDelegate()
获取的是当前属性代理接口的实际代理对象,而 this.getDelegate() as? ReleasableNotNull<*>
这个是转换成实际类型,然后调用实际类型的相关属性或方法即可,当然这个对象可能为null或者不是一个ReleasableNotNull类型的,这时需要抛异常。
注解
注解定义:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Api(val url: String)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Path(val name: String = "")
@Target(AnnotationTarget.FUNCTION)
annotation class Get(val name: String)
使用annotation
关键字修饰calss前面,比java的 @interface
更人性化,其中@Retention
有三种:
public enum class AnnotationRetention {
/** Annotation isn't stored in binary output */
SOURCE,
/** Annotation is stored in binary output, but invisible for reflection */
BINARY,
/** Annotation is stored in binary output and visible for reflection (default retention) */
RUNTIME
}
分别表示作用时机是在源码级、编译期、还是运行时,跟java基本类似。
@Target
指定限定标注对象,取值如下:
public enum class AnnotationTarget {
/** Class, interface or object, annotation class is also included */
CLASS,
/** Annotation class only */
ANNOTATION_CLASS,
/** Generic type parameter (unsupported yet) */
TYPE_PARAMETER,
/** Property */
PROPERTY,
/** Field, including property's backing field */
FIELD,
/** Local variable */
LOCAL_VARIABLE,
/** Value parameter of a function or a constructor */
VALUE_PARAMETER,
/** Constructor only (primary or secondary) */
CONSTRUCTOR,
/** Function (constructors are not included) */
FUNCTION,
/** Property getter only */
PROPERTY_GETTER,
/** Property setter only */
PROPERTY_SETTER,
/** Type usage */
TYPE,
/** Any expression */
EXPRESSION,
/** File */
FILE,
/** Type alias */
@SinceKotlin("1.1")
TYPEALIAS
}
添加参数:
注解类的参数是有限的,必须是能在编译期确定的类型。
简单使用:
@Api("https://api.github.com")
interface GitHubApi {
@Get("/users/{name}")
fun getUser(@Path name: String): User
}
class User
内置注解:
第一个标注注解的注解主要是指前面的@Retention
和@Target
之类的,是写在注解类上的注解。
标准库的通用注解:
Java虚拟机相关注解:
@file:JvmName("KotlinAnnotations")
@file:JvmMultifileClass
package com.bennyhuo.kotlin.annotations.builtins
import java.io.IOException
@Volatile
var volatileProperty: Int = 0
@Synchronized
fun synchronizedFunction(){
}
val lock = Any()
fun synchronizedBlock(){
synchronized(lock) {
}
}
@Throws(IOException::class)
fun throwException(){
}
像 @Synchronized
@Throws
注解都是比较好用的,替代java的相应关键字,比较人性化了。其中 @file:JvmName("KotlinAnnotations")
和 @file:JvmMultifileClass
比较有意思,能让多个文件中的kotlin代码最终生成到一个类里面,假如还有一个文件如下:
@file:JvmName("KotlinAnnotations")
@file:JvmMultifileClass
package com.bennyhuo.kotlin.annotations.builtins
fun hello(){
}
那经过编译之后,这个文件会和上面的文件合并到一起,生成到一个kotlin类文件当中。
实例:仿 Retrofit 反射读取注解请求网络
data class User(
var login: String,
var location: String,
var bio: String)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Api(val url: String)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Path(val url: String = "")
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Get(val url: String = "")
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class PathVariable(val name: String = "")
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Query(val name: String = "")
@Api("https://api.github.com")
interface GitHubApi {
@Api("users")
interface Users {
@Get("{name}")
fun get(name: String): User
@Get("{name}/followers")
fun followers(name: String): List<User>
}
@Api("repos")
interface Repos {
@Get("{owner}/{repo}/forks")
fun forks(owner: String, repo: String)
}
}
object RetroApi {
const val PATH_PATTERN = """(\{(\w+)\})"""
val okHttp = OkHttpClient()
val gson = Gson()
val enclosing = {
cls: Class<*> ->
var currentCls: Class<*>? = cls
sequence {
while(currentCls != null){
// enclosingClass获取下一个class
// yield将对象添加到正在构建的sequence序列当中
currentCls = currentCls?.also { yield(it) }?.enclosingClass
}
}
}
//内联特化
inline fun <reified T> create(): T {
val functionMap = T::class.functions.map{ it.name to it }.toMap() //【函数名,函数本身】的Pair转成map
val interfaces = enclosing(T::class.java).takeWhile { it.isInterface }.toList() //拿到所有接口列表
println("interfaces= $interfaces")// 输出 [GitHubApi$Users, GitHubApi]
//foldRight从interfaces序列的右边开始拼
val apiPath = interfaces.foldRight(StringBuilder()) {
clazz, acc ->
// 拿到每个接口类的Api注解的url参数值,如果url参数为空,则使用类名作为url值
acc.append(clazz.getAnnotation(Api::class.java)?.url?.takeIf { it.isNotEmpty() } ?: clazz.name)
.append("/")
}.toString()
println("apiPath= $apiPath") // https://api.github.com/users/
//动态代理
return Proxy.newProxyInstance(RetroApi.javaClass.classLoader, arrayOf(T::class.java)) {
proxy, method, args ->
//所有函数中的抽象函数 即接口的方法
functionMap[method.name]?.takeIf { it.isAbstract }?.let {
function ->
//方法的参数
val parameterMap = function.valueParameters.map {
//参数名和参数的值放在一起
it.name to args[it.index - 1] //valueParameters包含receiver 因此需要index-1来对应args
}.toMap()
println("parameterMap= $parameterMap") //{name=bennyhuo}
//{name} 拿到Get注解的参数 如果注解参数不为空就使用注解参数,如果为空使用方法名称
val endPoint = function.findAnnotation<Get>()!!.url.takeIf { it.isNotEmpty() } ?: function.name
println("endPoint= $endPoint") //{name}/followers
//正则找到endPoint中的所有符合"{owner}/{repo}/forks"其中{xxx}的结果
val compiledEndPoint = Regex(PATH_PATTERN).findAll(endPoint).map {
matchResult ->
println("matchResult.groups= ${matchResult.groups}") // [MatchGroup(value={name}, range=0..5), MatchGroup(value={name}, range=0..5), MatchGroup(value=name, range=1..4)]
println("matchResult.groups1.range= ${matchResult.groups[1]?.range}") // 0..5
println("matchResult.groups2.value= ${matchResult.groups[2]?.value}") // name
matchResult.groups[1]!!.range to parameterMap[matchResult.groups[2]!!.value]
}.fold(endPoint) {
acc, pair ->
//acc的初始值就是endPoint即{name}/followers
println("acc= ${acc}") // {name}/followers
println("pair= ${pair}") // (0..5, bennyhuo) pair是一个 range to name
acc.replaceRange(pair.first, pair.second.toString()) // 把{name}/followers中的0到5的位置的字符串{name}替换成bennyhuo
}
println("compiledEndPoint= ${compiledEndPoint}") //bennyhuo/followers
//拼接api和参数
val url = apiPath + compiledEndPoint
println("url ==== $url")
println("*****************")
okHttp.newCall(Request.Builder().url(url).get().build()).execute().body()?.charStream()?.use {
gson.fromJson(JsonReader(it), method.genericReturnType)//返回json的解析结果
}
}
} as T
}
}
fun main() {
//interface com.bennyhuo.kotlin.annotations.eg.GitHubApi
//println("enclosingClass=${GitHubApi.Users::class.java.enclosingClass}")
val usersApi = RetroApi.create<GitHubApi.Users>()
val user = usersApi.get("bennyhuo")
val followers = usersApi.followers("bennyhuo").map { it.login }
println("user ====== $user")
println("followers ======== $followers")
}
这个例子还是有点复杂,不太好理解,有些方法没接触过不知道啥意思,这里加了很多打印方法,把结果打印输出一下,这样能知道具体是代表的啥,就好理解一点了。
实例:注解加持反射版 Model 映射
这个例子是在前面反射一节实现的model映射例子的基础上,通过添加注解方式处理那些字段名称不是相同风格的情况,比如两个对象中的avatar_url
和 avatarUrl
的相互映射。
//不写默认是RUNTIME
//@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class FieldName(val name: String)
@Target(AnnotationTarget.CLASS)
annotation class MappingStrategy(val klass: KClass<out NameStrategy>)
interface NameStrategy {
fun mapTo(name: String): String
}
//下划线转驼峰
object UnderScoreToCamel : NameStrategy {
// html_url -> htmlUrl
override fun mapTo(name: String): String {
//先转成字符数组,然后fold操作
return name.toCharArray().fold(StringBuilder()) { acc, c ->
when (acc.lastOrNull()) { //上一次的acc不是空
'_' -> acc[acc.lastIndex] = c.toUpperCase() //上一次结果的最后一个字符是下划线就把下划线位置替换成当前字符的大写字母
else -> acc.append(c) // 否则直接拼接
}
//返回acc
acc
}.toString()
}
}
//驼峰转下划线
object CamelToUnderScore : NameStrategy {
override fun mapTo(name: String): String {
//先转成字符数组,然后fold操作
return name.toCharArray().fold(StringBuilder()) { acc, c ->
when {
c.isUpperCase() -> acc.append('_').append(c.toLowerCase()) //如果是大写字母直接拼一个下划线再拼上小写
else -> acc.append(c)
}
//返回acc
acc
}.toString()
}
}
//使用定义的策略注解,驼峰转下划线
@MappingStrategy(CamelToUnderScore::class)
data class UserVO(
val login: String,
//@FieldName("avatar_url") //这种是单个字段上面添加注解,只能一个一个添加
val avatarUrl: String,
var htmlUrl: String
)
data class UserDTO(
var id: Int,
var login: String,
var avatar_url: String,
var url: String,
var html_url: String
)
fun main() {
val userDTO = UserDTO(
0,
"Bennyhuo",
"https://avatars2.githubusercontent.com/u/30511713?v=4",
"https://api.github.com/users/bennyhuo",
"https://github.com/bennyhuo"
)
val userVO: UserVO = userDTO.mapAs()
println(userVO)
val userMap = mapOf(
"id" to 0,
"login" to "Bennyhuo",
"avatar_url" to "https://api.github.com/users/bennyhuo",
"html_url" to "https://github.com/bennyhuo",
"url" to "https://api.github.com/users/bennyhuo"
)
val userVOFromMap: UserVO = userMap.mapAs()
println(userVOFromMap)
}
inline fun <reified From : Any, reified To : Any> From.mapAs(): To {
return From::class.memberProperties.map { it.name to it.get(this) }
.toMap().mapAs()
}
inline fun <reified To : Any> Map<String, Any?>.mapAs(): To {
return To::class.primaryConstructor!!.let {
it.parameters.map { parameter ->
parameter to (this[parameter.name]
// let(this::get)等价于let{this[it]} userDTO["avatar_url"]
?: (parameter.annotations.filterIsInstance<FieldName>().firstOrNull()?.name?.let(this::get))
// 拿到UserVO类的注解MappingStrategy的kclass即CamelToUnderScore,它是一个object calss, objectInstance获取实例,然后调用mapTo把avatarUrl转成avatar_url,最后调用userDTO["avatar_url"]
?: To::class.findAnnotation<MappingStrategy>()?.klass?.objectInstance?.mapTo(parameter.name!!)?.let(this::get)
?: if (parameter.type.isMarkedNullable) null
else throw IllegalArgumentException("${parameter.name} is required but missing."))
}.toMap().let(it::callBy)
}
}
这里如果注解上不写@Retention(AnnotationRetention.RUNTIME)
默认就是运行时类型。
下面两种写法是等价的:
parameter.annotations.filterIsInstance<FieldName>()
parameter.findAnnotation<FieldName>()
下面两种写法是等价的:
let(this::get)
let{
this[it]
}
mapAs()方法中做了几件事:
- 尝试直接从当前Map中获取To对象的同名参数值,
- 尝试从To对象的字段上面的注解来获取需要转换的参数名,再根据名字获取Map中的值
- 尝试获取To对象的类注解得到处理类,调用处理类方法驼峰转下划线,再根据名字获取Map中的值
- 以上大招都没有获取到,如果To对象的字段可接受空值,就赋值null, 否则就抛异常
驼峰和下划线转换那里稍微有点绕。。
实例:注解处理器版 Model 映射
Java编译过程:
这个例子会用到一些著名的代码生成库:
- 生成Java代码:JavaPoet
- 生成Kotlin代码:KotlinPoet
上面两个都是square公司出品的开源库,JakeWharton大神的杰作,这个例子中主要用到了KotlinPoet 。
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "com.squareup:kotlinpoet:1.4.3"
implementation "com.bennyhuo.aptutils:aptutils:1.7.1"
implementation project(":apt:annotations")
}
注解声明:
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS)
annotation class ModelMap
这里不需要在运行时保留注解,编译就会生成代码了,因此使用的是AnnotationRetention.BINARY
注解生成代码:
package com.bennyhuo.kotlin.annotations.apt.compiler
import com.bennyhuo.aptutils.AptContext
import com.bennyhuo.aptutils.logger.Logger
import com.bennyhuo.aptutils.types.ClassType
import com.bennyhuo.aptutils.types.asKotlinTypeName
import com.bennyhuo.aptutils.types.packageName
import com.bennyhuo.aptutils.types.simpleName
import com.bennyhuo.aptutils.utils.writeToFile
import com.bennyhuo.kotlin.annotations.apt.ModelMap
import com.squareup.kotlinpoet.*
import javax.annotation.processing.*
import javax.lang.model.SourceVersion
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.TypeElement
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
//必须指定注解的类型
@SupportedAnnotationTypes("com.bennyhuo.kotlin.annotations.apt.ModelMap")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class ModelMapProcessor: AbstractProcessor() {
override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv)
AptContext.init(processingEnv)
}
//fun Sample.toMap() = mapOf("a" to a, "b" to b)
//fun <V> Map<String, V>.toSample() = Sample(this["a"] as Int, this["b"] as String)
override fun process(annotations: MutableSet<out TypeElement>, roundEnv: RoundEnvironment): Boolean {
roundEnv.getElementsAnnotatedWith(ModelMap::class.java)
.forEach {
element ->
element.enclosedElements.filterIsInstance<ExecutableElement>()
.firstOrNull { it.simpleName() == "<init>" }
?.let {
val typeElement = element as TypeElement
FileSpec.builder(typeElement.packageName(), "${typeElement.simpleName()}\$\$ModelMap") //$$转义
.addFunction(
FunSpec.builder("toMap")
.receiver(typeElement.asType().asKotlinTypeName())
.addStatement("return mapOf(${it.parameters.joinToString {""""${it.simpleName()}" to ${it.simpleName()}""" }})")//mapOf("a" to a, "b" to b)
.build()
)
.addFunction(
FunSpec.builder("to${typeElement.simpleName()}")
.addTypeVariable(TypeVariableName("V"))
.receiver(MAP.parameterizedBy(STRING, TypeVariableName("V")))
.addStatement(
"return ${typeElement.simpleName()}(${it.parameters.joinToString{ """this["${it.simpleName()}"] as %T """ } })", //Sample(this["a"] as Int, this["b"] as String) %T是模板字符串 用后面的参数替换
*it.parameters.map { it.asType().asKotlinTypeName() }.toTypedArray()
)
.build()
)
.build().writeToFile()
}
}
return true
}
}
这是注解处理器的模块,然后新建一个模块来使用它:
gradle中必须通过kapt
的方式来依赖注解处理器的库:
plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.kapt'
}
group 'com.bennyhuo.kotlin'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
jcenter()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
kapt project(":apt:compiler")
implementation project(":apt:annotations")
testCompile group: 'junit', name: 'junit', version: '4.12'
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
注意注解处理器仅在编译器起作用,而注解库的依赖方式是在运行时,也就是会参与编译到字节码中的。
最后使用代码:
import com.bennyhuo.kotlin.annotations.apt.ModelMap
fun main() {
val sample = Sample(0, "1")
val map = sample.toMap();
println(map)
val sample2 = map.toSample2()
println(sample2)
}
@ModelMap
data class Sample(val a: Int, val b: String)
//fun Sample.toMap() = mapOf("a" to a, "b" to b)
//fun <V> Map<String, V>.toSample() = Sample(this["a"] as Int, this["b"] as String)
@ModelMap
data class Sample2(val a: Int, val b: String)
使用前先build一下,这样注解处理器会在下面的目录下生成kotlin的代码文件:
可以看到每个添加了@ModelMap
的data类都会生成一个文件 ,打开看一下内容:
package com.bennyhuo.kotlin.annotations.sample
import kotlin.Int
import kotlin.String
import kotlin.collections.Map
fun Sample.toMap() = mapOf("a" to a, "b" to b)
fun <V> Map<String, V>.toSample() = Sample(this["a"] as Int , this["b"] as String )
package com.bennyhuo.kotlin.annotations.sample
import kotlin.Int
import kotlin.String
import kotlin.collections.Map
fun Sample2.toMap() = mapOf("a" to a, "b" to b)
fun <V> Map<String, V>.toSample2() = Sample2(this["a"] as Int , this["b"] as String )
基本就是前面期望的结果了
后面有空再把JavaPoet的使用拿出来学习一下吧
kotlin编译器插件对开发者的要求比较高,需要熟悉字节码的api。
AllOpen插件的处理逻辑:
常见的kotlin提供的插件:
kotlin插件还能编译js文件
kotlin 与 java 代码互调的一些注意事项
新建一个Kotlin文件,直接在里面写一个方法,编译器会生成一个 类名 + Kt 结尾的 java
类文件,并将该方法生成为该java
类的静态方法:
// MyUtils.kt
fun show(info: String) {
println(info)
}
上面代码会生成类似下面Java代码:
直接在Java中调用的话,找到以 Kt 结尾的Java类进行静态方法调用即可:
MyUtilsKt.show("hello");
但是如果Kotlin文件中的方法是写在一个类里面的,java调用的时候必须创建一个类实例进行调用:
// MyUtils.kt
class MyUtils {
fun show(info: String) {
println(info)
}
}
// java 代码中调用方式
new MyUtils().show("hello");
另外需要注意: in
在 Kotlin 中是 关键字,Java 中如果有名字为 in
的变量名,在 Kotlin 中使用时需要写成 `in` 的方式来调用。
Kotlin 和 Java 互调的另一个坑:Kotlin 无法判断来自 Java 平台的可空性,所以最靠谱的方法是使用一个可空类型来接收来自 Java 的变量。例如,Java 中返回值为 String
类型的方法,在 Kotlin 中使用时,最好使用 var str : String ?
可空类型来接受,避免空指针风险。
// java 代码
public class JavaStudent {
public static String in = "hello";
public String getName() {
return null; // java 返回的 null,kotlin 中是无法感知的
}
}
// kotlin 代码中调用
fun main() {
println(JavaStudent.`in`)
val student = JavaStudent()
val name : String? = student.name // 要用可空类型接受Java方法返回的值
println(name?.length)
}
Kotlin 中的只读集合并不是真的不可变,当跨平台的时候,它就可以被其他平台的语言所修改,例如,当我们在Kotlin中调用下面这个bar
方法的时候:
fun bar(list: List<Int>) {
foo(list)
}
而这里调用的这个foo
方法是用Java的代码定义的:
public static List<Int> foo(List<Int> list) = {
for (int i = 0; i < list.size(); i++) {
list[i] = list[i] * 2;
}
return list;
}
所以传入bar
方法中的list
就会被foo
方法改变:
val list = listOf(1, 2, 3, 4)
bar(list)
println(list)
>>> [2, 4, 6, 8]
所以,当我们与Java进行互操作的时候就要考虑到这种情况。
关于Class
类名的传递:
- 如果方法参数的泛型是Java的类,必须传
Java类名::class.java
- 如果方法参数的泛型是Kotlin的类,直接传
Kotlin类型::class
即可
例如:
fun show(clazz: Class<JavaStudent>) {
}
fun show(clazz: KClass<KtStudent>) {
}
fun main() {
show(JavaStudent::class.java)
show(KtStudent::class)
}
Kotlin中使用Java的Callback
写法:
// java 代码
public interface JavaCallback {
public void show(String info);
}
public class JavaManager {
private JavaCallback javaCallback;
public void setCallback(JavaCallback callback) {
javaCallback = callback;
}
}
// kotlin 代码中调用
fun main() {
val javaManager = JavaManager()
// 第一种写法
javaManager.setCallback {
println(it)
}
// 第二种写法
javaManager.setCallback(object : JavaCallback {
override fun show(info: String?) {
println(info)
}
})
// 第三种写法
val javaCallback = JavaCallback {
println(it)
}
javaManager.setCallback(javaCallback)
}
建议使用第一种lambda的写法方式。
而 Kotlin 中使用 Kotlin 的 Callback
写法,只能使用object
的方式传,不能像调用 java 的callback
那样灵活:
interface KTCallback {
fun show(name: String)
}
class KTManager {
fun setCallback(callback: KTCallback) {
callback.show("aaa")
}
}
fun main() {
KTManager().setCallback(object : KTCallback {
override fun show(name: String) {
println(name)
}
})
// 或者这样
val callback = object : KTCallback {
override fun show(name: String) {
println(name)
}
}
KTManager().setCallback(callback)
// 但这样写不行
// KTManager().setCallback {
// println(it)
// }
}
示例:Handler
的Callback
和Thread
都是Java的类,可以多种写法
fun main() {
Handler(Looper.getMainLooper(), object : Handler.Callback {
override fun handleMessage(msg: Message): Boolean {
// ...
return true
}
})
Handler(Looper.getMainLooper()) {
// ...
true
}
Thread {
println("这里是Runnbale对象的run方法被执行")
}.start()
}
一个方法接受到可变参数传给另一个方法时,需要前面加上*
号:
fun foo(vararg students : Student) {
foo2(*students)
}
fun foo2(vararg students : Student) {
}
另外需要注意的一点是,Java 并不支持主动指定一个函数是否是内联函数,所以在 Kotlin 中声明的普通内联函数可以在Java中调用,因为它会被当作一个常规函数;而用reified
来实例化的参数类型的内联函数则不能在 Java 中调用,因为它永远是需要内联的。
如何将Kotlin方法与Java方法隔离
通常Java代码中可以直接调用kotlin的方法,但是可以通过以下方式使kotlin方法不被Java方法识别,只能被kotlin调用:
fun `222show`() {
}
fun `4323545422666`() {
}
fun main() {
`222show`()
`4323545422666`()
}
其中 4323545422666
这种写法可以向第三方调用者隐藏函数名意图,然后自己找个小本本记录下来 每个数字的含义,只有自己人知道具体含义。