2018最新Kotlin基础视频教程上线了

从5月份开始制作这个视频,历时两个多月,终于在网易云课堂与大家见面了…

视频特色

  • 在前期课程中,使用普通编辑器进行编码,治疗你的IDE依赖症
  • 所有课程均使用live coding的方式授课,保持与你的频率一致
  • 每节课程课后都准备了相应的习题供大家巩固练习
  • 课程覆盖面广,线下效果持续跟踪
  • 项目实战部分是一个非常有意思的小项目,使用控制台完成类QQ聊天室功能(支持文字消息、文件发送、文件下载等功能)

课程目录

  • Kotlin基础环境配置、变量与空值安全
  • 一等公民函数基础语法
  • 高阶函数、lambda表达式与匿名函数
  • 面向对象知识点(类、接口、继承、扩展等)
  • Kotlin语言中多样的class类型、对象以及枚举
  • 强大的集合与控制流处理
  • 操作符重载
  • 更安全的泛型处理
  • 代理模式及其应用
  • 协程简介
  • 析构、注解与异常处理
  • Kotlin与Java互通的那些事儿
  • 控制台版本QQ聊天室项目实战

一些感想

这是我第一次真正尝试录制视频教程,种种原因,导致整个视频的录制、剪辑加发布审核一共花掉了两个多月,将近三个月。而且还是离职在家,全职的情况下。

即便如此,事实上,这个视频依然没有达到我的要求。但我想,第一次永远是青涩的,想要一步到位永远不过是痴人说梦。我坚信下一个视频我会做的更好。因此,也希望大家对这个视频提出宝贵的意见。

视频介绍看这里

在Kotlin交流群中,不少同学希望我能够出露脸视频。为了满足大家的愿望,我在课程的开始部分录制了一段真人解说视频。如果你不想看文字介绍的话,请移步到这里:http://study.163.com/course/courseLearn.htm?courseId=1005686004&share=2&shareId=400000000535034#/learn/video?lessonId=1052950023&courseId=1005686004

如何观看这个视频教程

目前,该视频教程仅上线了网易云课堂,同学们可以移步至这里观看:http://study.163.com/course/introduction.htm?courseId=1005686004&share=2&shareId=400000000535034

课程问题解惑

如果你在使用Kotlin视频教程的时候遇到了任何问题,欢迎来我的Kotlin语言交流群里面提问。只要是关于视频课程的内容,一定有问必答。如果你没有购买这个视频教程,也可以添加这个QQ交流群,群里的小伙伴都非常热情。如果遇到了Kotlin语言问题,也可以在群里提问,同样会有问必答。

视频课程资料

课程源码:https://github.com/yuanhoujun/kotlin-video-tutorial

习题答案:关注微信公众号【欧阳锋工作室】,回复“Kotlin视频教程”索取

Kotlin语言交流群

唯一官方Kotlin语言交流群:329673958,期待您的加入。

那些年,我们看不懂的那些Kotlin标准函数

Kotlin标准库中提供了一套用于常用操作的函数。最近,在我的Kotlin交流群中有人再次问到了关于这些函数的用法。今天,让我们花一点时间,一起看一下这些函数的用法。

Ready go >>>

注:这里所说的标准函数主要来自于标准库中在文件Standard.kt中的所有函数。

run#1

1
2
3
4
5
6
7
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

contract部分主要用于编译器上下文推断,这里我们忽略掉这部分代码。

观察源码发现,run方法仅仅是执行传入的block表达式并返回执行结果而已(block是一个lambda表达式)。

因此,如果你仅仅需要执行一个代码块,可以使用该函数

看一个例子:

1
2
3
4
5
6
7
8
9
val x = run {
println("Hello, world")
return@run 1
}
println(x)

// 执行结果
Hello,world
1

run#2

1
2
3
4
5
6
7
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

这个函数跟上面的函数功能是完全一样的。不同的是,block的receiver是当前调用对象,即在block中可以使用当前对象的上下文。

因此,如果你需要在执行的lambda表达式中使用当前对象的上下文的话,可以使用该函数。除此之外,两者没有任何差别

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
fun sayHi(name: String) {
println("Hello, $name")
}
}

class B {

}

fun main(args: Array<String>) {
val a = A()
val b = a.run {
// 这里你可以使用A的上下文
a.sayHi("Scott Smith")
return@run B()
}
println(b)
}

// 执行结果
Hello,Scott Smith
b@2314

从例子中,我们可以看到,这个函数还可以用于对数据类型进行转换。

with

1
2
3
4
5
6
7
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}

这个函数其实和run函数也是做了一样的事情。不同的是,这里可以指定block的接收者。

因此,如果你在执行lambda表达式的时候,希望指定不同的接收者的话,可以使用该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
fun sayHi(name: String) {
println("Hello, $name")
}
}


fun main(args: Array<String>) {
val a = A()
with(a) {
// 这里的接收者是对象a,因此可以调用a实例的所有方法
sayHi("Scott Smith")
}
}

apply

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

可以看到,这个方法是针对泛型参数的扩展方法,即所有对象都将拥有该扩展方法。相对于run#2方法,apply不仅执行了block,同时还返回了receiver本身。

这在链式编程中很常用,如果你希望执行lambda表达式的同时而不破坏链式编程,可以使用该方法

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
fun sayHi(name: String) {
println("Hello, $name")
}

fun other() {
println("Other function...")
}
}


fun main(args: Array<String>) {
val a = A()
a.apply {
println("This is a block")
sayHi("Scott Smith")
}.other()
}

// 执行结果
This is a block
Hello, Scott Smith
Other function...

also

1
2
3
4
5
6
7
8
9
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}

这个函数跟with又很像,不同的是,block带有一个当前receiver类型的参数。在block中,你可以使用该参数对当前实例进行操作。

这个函数和with完全可以互相通用,with函数可以直接在当前实例上下文中对其进行操作,而also函数要通过block参数获取当前类实例。因为用法完全一致,这里就不举例了

let

1
2
3
4
5
6
7
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}

如果你使用过RxJava,可能会感到似曾相识,这其实就是RxJava的map函数。这个函数也是针对泛型参数的扩展函数,所有类都将拥有这个扩展函数。

如果你希望对当前数据类型进行一定的转换,可以使用该方法。该方法的block中同样可以使用当前receiver的上下文

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Triangle {}

class Rectangle {}

fun main(args: Array<String>) {
val tr = Triangle()
val rect = tr.let { it ->
println("It is $it")
return@let Rectangle()
}
println(rect)
}

// 执行结果
It is Triangle@78308db1
Rectangle@27c170f0

从例子中可以看到,我们成功地将三角形转换成了矩形,这就是let函数的作用。

takeIf

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
return if (predicate(this)) this else null
}

这个函数也是针对泛型参数的扩展函数,所有类都将拥有这个扩展。这个函数使用了一个预言函数作为参数,主要用于判断当前对象是否符合条件。
这个条件函数由你指定。如果条件符合,将返回当前对象。否则返回空值。

因此,如果你希望筛选集合中某个数据是否符合要求,可以使用这个函数

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
fun main(args: Array<String>) {
val arr = listOf(1, 2, 3)
arr.forEach {
println("$it % 2 == 0 => ${it.takeIf { it % 2 == 0 }}")
}
}

// 执行结果
1 % 2 == 0 => null
2 % 2 == 0 => 2
3 % 2 == 0 => null

takeUnless

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
return if (!predicate(this)) this else null
}

这个函数刚好与takeIf筛选逻辑恰好相反。即:如果符合条件返回null,不符合条件返回对象本身。

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
fun main(args: Array<String>) {
val arr = listOf(1, 2, 3)
arr.forEach {
println("$it % 2 == 0 => ${it.takeUnless { it % 2 == 0 }}")
}
}

// 执行结果
1 % 2 == 0 => 1
2 % 2 == 0 => null
3 % 2 == 0 => 3

看到了吗?这里的执行结果和takeIf恰好相反。

repeat

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
contract { callsInPlace(action) }

for (index in 0 until times) {
action(index)
}
}

这个函数意思很明显,就是将一个动作重复指定的次数。动作对应一个lambda表达式,表达式中持有一个参数表示当前正在执行的次数索引。

看一个例子:

1
2
3
4
5
6
7
8
9
fun main(args: Array<String>) {
repeat(3) {
println("Just repeat, index: $it")
}
}

Just repeat, index: 0
Just repeat, index: 1
Just repeat, index: 2

简单总结

最后,我们用一个表格简单总结一下这些函数的用法:
函数|用途|特点|形式
:—:|:—:|:—:|:—:
run#1|执行block,并返回执行结果|block中无法获取接收者上下文|全局函数
run#2|执行block,并返回执行结果|block中可以获取接收者上下文|扩展函数
with|指定接收者,通过接收者执行block|block中可以获取接收者的上下文,可以对接收者数据类型做一定转换|全局函数
apply|执行block,并返回接收者实例本身|block中可以获取接收者的上下文,可用于链式编程|扩展
also|执行block,并返回接收者实例本身|block中有一个参数代表接收者实例,可用于链式编程|扩展
let|执行block,并返回执行结果|block中有一个参数代表接收者实例,可以对接收者数据类型做一定转换|扩展
takeIf|根据条件predicate判断当前实例是否符合要求|如果符合要求,返回当前实例本身;否则返回null|扩展函数
takeUnless|根据条件predicate判断当前实例是否不符合要求|如果不符合要求,返回当前实例本身;否则返回null|扩展

搞定Receiver

理解上面这几个函数,最重要的一点是要理解Receiver。遗憾的是,Kotlin官方文档中并没有针对Receiver的详细讲解。关于这部分的讲解,请扫描下方二维码关注欧阳锋工作室,回复搞定Receiver查看文章。

欢迎加入Kotlin交流群

关于Kotlin,如果你有任何问题,欢迎加入我的Kotlin交流群: 329673958。当前群交流活跃,问题解答速度很快,期待你的加入。

测测你的Kotlin基础

文 | 欧阳锋

本次测试满分160分,测测看,你能拿几分 <<<

1)Kotlin语言有基本数据类型吗?(5分)

2)Kotlin中有哪些访问控制符,分别代表什么意思?默认访问控制符是什么?(5分)

3)Kotlin接口是否允许有方法实现?是否允许声明成员变量?(5分)

4)Sealed类有什么作用?(5分)

5)Kotlin语言中如何实现类似Java创建匿名内部类对象?(10分)

6)Kotlin的扩展相对继承有什么优势?扩展方法的执行是否也遵循多态?(10分)

7)如果一个类同时实现多个接口,接口中存在同名方法,如何解决冲突?(5分)

8)Kotlin语言中是否存在static关键字,如果没有,如何声明静态变量,并实现与Java互通(5分)

9)使用Kotlin语言是否一定不会出现空指针异常?为什么?(10分)

10)Kotlin语言中推荐使用什么方式判断两个对象是否相等?如何判断两个对象是同一个对象?(5分)

11)如果使用Foo<out T: TUpper>这种方式声明泛型,使用Foo<*>这种方式接收该对象实例,代表什么意思?如何理解Kotlin泛型,与Java有什么区别?(10分)

12)如何自定义setter/getter方法?(5分)

13)使用语句var x = null声明变量x是否合法?如果合法,x的具体类型是什么?(5分)

14)下面这段代码的输出结果是什么?(10分)

1
2
3
val list = listOf(1, 2, 3)
list.add(4)
println(list)

15)下面这段代码的执行结果是什么?(5分)

1
2
3
4
5
6
7
8
9
// Kotlin端
object A {
fun init() {
println("A init")
}
}

// Java端
A.init()

16)下面代码的执行结果是什么?(5分)

1
2
3
fun sum(a: Int, b: Int) = { a + b }

println(sum(1, 3))

17)下面代码的执行结果是什么?(5分)

1
2
println(null is Any)
println(null!! is Nothing)

18)下面代码的执行结果是什么?(10分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
init() {
f()
}

val a = "a"

fun f() {
println(a)
}
}

fun main(args: Array<String>) {
A()
}

19)下面代码的执行结果是什么?(10分)

1
2
println(127 as Int? === 127 as Int?)
println(128 as Int? === 128 as Int?)

20)下面代码的执行结果是什么?如果运行异常,应该怎样修改才能达到预期效果?(10分)

1
2
3
4
(1..5).forEach {
if (it == 3) break
println(it)
}

21)下面代码的执行结果是什么?如果运行异常,应该怎样修改,为什么要这样修改?(10分)

1
2
3
val A.x: Int = 3

println(A().x)

22)下面这段代码的执行结果是什么?(10分)

1
2
3
4
5
6
7
8
9
10
11
12
13
fun isOdd(x: Int) = x % 2 != 0

fun length(s: String) = s.length

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
return { x -> f(g(x)) }
}

fun main(args: Array<String>) {
val oddLength = compose(::isOdd, ::length)
val strings = listOf("a", "ab", "abc")
println(strings.filter(oddLength))
}

注:本篇例子Kotlin版本为1.2.31,更新版本可能存在部分差异

下面是你的基础等级:

得分 评价
0 ~ 80 基础较差
80 ~ 108 基础较好
108 ~ 160 基础很棒

查看答案方法

微信扫描下方二维码关注欧阳锋工作室,回复“Kotlin测试题答案”即可获取当前测试题答案

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

关于Kotlin抛弃可检测的异常处理,你怎么看?

可检测的异常英文翻译为Checked Exception,以下简称为CE。CE是一个备受争议的话题,有人主张CE是一个不可或缺的特性,也有人认为CE带来了一些问题,是一个冗余特性。这其中的支持者和反对者中都不乏软件行业的大佬。今天,我们借助这篇文章一起来讨论一下CE存在的必要性。

什么是CE

部分同学可能还不知道CE到底是什么。因此,在开始命题之前,有必要给大家解释一下CE的概念。

CE其实你每天都在用,只是你不知道它的存在而已。看一个例子你就明白了:

1
public void readString() throws IOException, FileNotFoundException {}

在这个函数声明的最后面,我们指定了函数可能抛出的异常,在使用的时候我们就可以针对具体的异常使用try {} catch() {}进行处理了。

由于我们在方法声明中指定了可能抛出的异常,因此方法具体可能抛出的异常是已知的,这就称之为可检测的异常(CE)。由于CE的声明来自编译阶段,因此IDE可能帮助你智能判断强制你针对某些异常进行处理,并给出友好提示。

这是一个很好的特性,不是吗?

想象一下,如果有一天,我们不能在函数上指定可抛出的异常了,会怎样?我们无法确定函数可能抛出的异常,并且可能会因为没有正确处理某个异常而导致程序奔溃。

可是,就是有人认为CE多此一举,并且Kotlin语言就是其中的支持者。为什么会有人坚定地认为CE多此一举呢?这是下一个我们要讨论的话题。

关于CE的争论

Java语言的CE设计借鉴了C++,而在受到Java影响的那些语言中,例如C#、Ruby等都去掉了CE的设计,这从实践的角度证明CE的存在确实意义不大。

在这个问题中,C#的主导工程师Anders Hejlsberg最有发言权。老实说,笔者并没有用过C#。可是,如果你搜索一下网络上关于C#和Java对比的文章你就会发现:C#被认为是一门比Java更优秀的编程语言,它始终在新增一些现代语言的特性,使你毫不费力地使用它。而Java作为一门古老的语言,受限于一些原始设计,在增加新特性时总是步履维艰,甚至有点不伦不类。

关于CE的设计,有人对Anders Hejlsberg进行过一次采访。采访的原文链接在这里:https://www.artima.com/intv/handcuffs.html

关于CE,Anders Hejlsberg认为它带来了两个问题版本问题扩展问题

所谓的版本问题是什么意思呢?Anders Hejlsberg举了一个例子:

假设有一个方法foo,它声明了抛出异常A、B和C,在下一个版本设计的时候,foo增加了一个新的特性,可能会抛出异常D。对于设计者来说,很明显这是一个大的改变,几乎可以确定的是,客户程序员不会去处理这个异常。为了避免出现问题,设计者不得不声明一个新的方法foo2,抛出一个新的异常。然后,客户程序员可以将针对foo的逻辑处理切换到foo2。

而所谓的扩展问题又是什么意思呢?这更好理解,以下来自Anders Hejlsberg的原话翻译并整理:

如果你在设计一个很小的系统,声明一个方法抛出一个异常,这很棒。可是,如果你尝试构建一个大的系统,其中包含了四、五个小系统的时候,问题来了。假设每个子系统可能抛出四到五个异常,而每上升一个系统,就犹如爬阶梯,异常数量会指数倍增加,最终你可能处理的异常将达到40个甚至80个。很显然,这是一个很糟糕的设计!

Anders Hejlsberg的话有理有据。可是,中国的 王垠 并不同意这个观点。关于Kotlin的CE设计,他写了一篇文章专门讲了这个问题,文章的原稿在这里:Kotlin 和 Checked Exception

看完王垠的文章,你会发现,他并不赞同Anders Hejlsberg的话。他认为,所谓的版本问题和扩展问题,其实都来自于程序员的滥用。只要处理得当,CE带来的好处是无法取代的。

其实,关于CE的争论还远不止Anders Hejlsberg和王垠两人。互联网上有很多关于这个问题的讨论。如果你感兴趣,可以Google了解一下。

你怎么看?

很显然,Kotlin语言受到了C#设计的影响,手起刀落,去掉了CE的设计。对于客户程序员来说,显而易见的一个改变是,你再也不能在Kotlin的方法声明中指定可能抛出的异常了。对于Kotlin语言的这种设计,你是赞成还是反对呢?

欢迎参与投票讨论

扫描下方二维码关注欧阳锋工作室,回复“CE”参与投票,或在所有文章中选择同名文章进行投票。

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

Kotlin语言中的泛型设计哲学

文 | 欧阳锋

Kotlin语言的泛型设计很有意思,但并不容易看懂。关于这个部分的官方文档,我反复看了好几次,终于弄明白Kotlin语言泛型设计的背后哲学。这篇文章将讲述Kotlin泛型设计的整个思考过程及其背后的哲学思想,希望可以解答你心中的疑问。不过,可以预见地,即使看完,你也未必完全明白这篇文章在说什么,但至少希望你通过这篇文章可以快速掌握Kotlin泛型的用法。

Kotlin泛型的设计初衷

我们认为,Kotlin是一门比Java更优秀的JVM编程语言,Kotlin泛型设计的初衷就是为了解决Java泛型设计中一些不合理的问题。这样说可能不够直观,看下面这个例子:

1
2
3
 List<String> strs = new ArrayList<>();
// 这里将导致编译错误,Java语言不允许这样做
List<Object> objs = strs;

很明显,String和Object之间存在着安全的隐式转换关系。存放字符串的集合应该可以自由转换为对象集合。这很合理,不是吗?

如果你这样认为的话,就错了!继续往下看,我们扩展这个程序:

1
2
3
4
5
List<String> strs = new ArrayList<>();
List<Object> objs = strs;
objs.add(1);

String s = strs.get(0);

很明显,这不合理!我们在第一个位置存入了整型数值1,却在取的时候将它当成了字符串。strs本身是一个字符串集合,用字符串接收读取的数据的逻辑是合理的。却因为错误的类型转换导致了不安全写入出现了运行时类型转换问题,因此,Java语言不允许我们这样做。

大多数情况下,这种限制没有问题。可是,在某些情况下,这并不合理。看下面的例子:

1
2
3
4
5
6
7
interface List<T> {
void addAll(List<T> t);
}

public void copy(List<String> from, List<Object> to) {
to.addAll(from);
}

这是一个类型绝对安全的操作,但在Java语言中这依然是不允许的。原因是,泛型是一个编译期特性,一旦指定,运行期类型就已经固定了。换而言之,泛型操作的类型是不可变的。这就意味着,List并不是List的子类型。

为了允许正确执行上述操作,Java语言增加了神奇的通配符操作魔法。

1
2
3
interface List<T> {
void addAll(List<? extends T> t);
}

? extends T意味着集合中允许添加的类型不仅仅是T还包括T的子类,但这个集合中可以添加的类型在集合参数传入addAll时就已经确定了。因此,这并不影响参数集合中可以存放的数据类型,它带来的一个直接影响就是addAll方法参数中终于可以传入泛型参数是T或者T的子类的集合了,即上面的copy方法将不再报错。

这很有意思,在使用通配符之前我们并不能传入类型参数为子类型的集合。使用通配符之后,居然可以了!这个特性在C#被称之为协变(covariant)。

协变这个词来源于类型之间的绑定。以集合为例,假设有两个集合L1、L2分别绑定数据类型F、C,并且F、C之间存在着父子关系,即F、C之间存在着一种安全的从C->F的隐式转换关系。那么,集合L1和L2之间是否也存在着L2->L1的转换关系呢?这就牵扯到了原始类型转换到绑定类型的集合之间的转换映射关系,我们称之为“可变性”。如果原始类型转换和绑定类型之间转换的方向相同,就称之为“协变”。

用一句话总结协变:如果绑定对象和原始对象之间存在着相同方向的转换关系,即称之为协变

PS:以上关于协变的概念来自笔者的总结,更严谨的概念请参考C#官方文档

文章开头我们将不可变泛型通过通配符使其成为了可变泛型参数,现在我们知道这种行为叫做协变。很明显,协变转换中写入是不安全的。因此,协变行为仅仅用于读取。如果需要写入怎么办呢?这就牵扯到了另外一个概念逆变(contravariance)。

逆变协变恰恰相反,即如果F、C之间存在着父子转换关系,L1、L2之间存在着从L1->L2的转换关系。其绑定对象的转换关系与原始对象的转换关系恰好相反。Java语言使用关键字super(?super List)实现逆变

举个例子:假设有一个集合List<? super String>,你将可以安全地使用add(String)或set(Int,String)方法。但你不能通过get(Int)返回String对象,因为你无法确定返回的对象是否是String类型,你最终只能得到Object。

因此,我们认为,逆变可以安全地写入数据,但并不能安全地读取,即最终不能获取具体的对象数据类型。

为了简化理解,我们引入官方文档中 Joshua Bloch 说的一句话:

Joshua Bloch calls those objects you only read from Producers, and those you only write to Consumers. He recommends: “For maximum flexibility, use wildcard types on input parameters that represent producers or consumers”

Joshua Bloch是Java集合框架的创始人,他把那些只能读取的对象叫做生产者;只能写入的对象叫做消费者。为了保证最大灵活性,他推荐在那些代表了生产者和消费者的输入参数上使用通配符指定泛型。

相对于Java的通配符,Kotlin语言针对协变逆变引入两个新的关键词outin

out用于协变,是只读的,属于生产者,即用在方法的返回值位置。而in用于逆变,是只写的,属于消费者,即用在方法的参数位置。

用英文简记为:POCI = Producer Out , Consumer In。

如果一个类中只有生产者,我们就可以在类头使用out声明该类是对泛型参数T协变的:

1
2
3
interface Link<out T> {
fun node(): T
}

同样地,如果一个类中只有消费者,我们就可以在类头使用in声明该类是对泛型参数T逆变的:

1
2
3
interface Repo<in T> {
fun add(t: T)
}

out等价于Java端的? extends List通配符,而in等价于Java端的? super List通配符。因此,类似下面的转换是合理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Link<out T> {
fun node(): T
}

fun f1(linkStr: Link<String>) {
// 这是一个合理的协变转换
val linkAny: Link<Any> = linkStr
}

interface Repo<in T> {
fun add(t: T)
}

fun f2(repoAny: Repo<Any>) {
// 这是一个合理的逆变转换
val repoStr: Repo<String> = repoAny
}

小结:协变和逆变

协变逆变对于Java程序员来说是一个全新的概念,为了便于理解,我用一个表格做一个简单的总结:

- 协变 逆变
关键字 out in
读写 只读 可写
位置 返回值 参数
角色 生产者 消费者

类型投影

在上面的例子中,我们直接在类体声明了泛型参数的协变或逆变类型。在这种情况下,就严格限制了该类中只允许出现该泛型参数的消费者或者生产者。很显然,这种场景并不多见,大多数情况下,一个类中既存在着消费者又存在着生产者。为了适应这种场景,我们可以将协变或逆变声明写在方法参数中。Kotlin官方将这种方式叫做 类型投影(Type Projection)

这里我们直接使用官方文档的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Array<T>(val size: Int) {
fun get(index: Int): T { /* ... */ }
fun set(index: Int, value: T) { /* ... */ }
}

fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }

// 由于泛型参数的不变性,这里将出现问题
copy(ints, any)

很明显,我们希望from参数可以接收元素为Any或其子类的任意元素,但我们并不希望修改from,以防止出现类似文章开头的问题。因此,我们可以在from参数中添加out修饰,使其协变:

1
2
fun copy(from: Array<out Any>, to: Array<Any>) {
}

一旦添加out修饰符,你就会发现,当你尝试调用set方法的时候,编译器将会提示你在out修饰的情况下禁止调用该方法。

注:Java语言在使用”协变“的情况下,from参数依然可以调用set方法。从这里可以看出,Kotlin语言在泛型安全控制上比Java更加精细。

星号投影

除了上述明确的类型投影方式之外,还有一种非常特殊的投影方式,称之为星号投影(star projection)。

在某些情况下,我们并不知道具体的类型参数信息。为了适应这种情况,Java语言中我们会直接忽略掉类型参数:

1
2
3
4
5
6
7
8
class Box<T> {
public void unPack(T t) {
...
}
}

// 在不确定类型参数的情况下,我们会这样做
Box box = new Box();

在Kotlin语言中,我们使用星号对这种情况进行处理。因为,Kotlin针对泛型有严格的读写区分。同样地,使用*号将限制泛型接口的读写操作:

  • Foo<out T: TUpper>,这种情况下,T是协变类型参数,上边界是TUpper。Foo<*>等价于Foo,这意味着你可以安全地从Foo<*>读取TUpper类型。
  • Foo<in T>,在这种情况下,T是逆变类型参数,下边界是T。Foo<*>等价于Foo,这意味着在T未知的情况下,你将无法安全写入Foo<*>。
  • Foo<T: TUpper>,在这种情况下,T是不可变的。Foo<*>等价于你可以使用Foo安全读取值,写入等价于Foo,即无法安全写入。

泛型约束

在泛型约束的控制上,Kotlin语言相对于Java也技高一筹。在大多数情况下,泛型约束需要指定一个上边界。这同Java一样,Kotlin使用冒号代替extends:

1
fun <T: Animal> catch(t: T) {}

在使用Java的时候,经常碰到这样一个需求。我希望泛型参数可以约束必须同时实现两个接口,但遗憾的是Java语言并没有给予支持。令人惊喜的是,Kotlin语言对这种场景给出了自己的实现:

1
2
3
4
fun <T> swap(first: List<T>, second: List<T>) where T: CharSequence, 
T: Comparable<T> {

}

可以看到,Kotlin语言使用where关键字控制泛型约束存在多个上边界的情况,此处应该给Kotlin鼓掌。

总结

Kotlin语言使用协变逆变来规范可变泛型操作,out关键字用于协变,代表生产者。in关键字用于逆变,代表消费者。out和in同样可以用于方法参数的泛型声明中,这称之为类型投影。在针对泛型类型约束的处理上,Kotlin增加了多个上边界的支持。

Kotlin语言最初是希望成为一门编译速度比Scala更快的JVM编程语言!为了更好地设计泛型,我们看到它从C#中引入了协变逆变的概念。这一次,我想,它至少同时站在了Scala和C#的肩膀上。

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

你是否也被Kotlin语言的object绕晕了呢

文 | 欧阳锋

近日,在笔者的Kotlin语言交流群中。的确发现了一些同学对object的用法有一些疑问。于是,出现了下面这样错误的用法:

很自然的想法,c是一个接口类型的成员变量,访问外部类的成员变量,这不是理所应当的吗?

即使查看Kotlin官方文档,也有这样一段描述:

Sometimes we need to create an object of a slight modification of some class, without explicitly declaring a new subclass for it. Java handles this case with anonymous inner classes. Kotlin slightly generalizes this concept with object expressions and object declarations.

核心意思是:Kotlin使用object代替Java匿名内部类实现。

很明显,即便如此,这里的访问应该也是合情合理的。从匿名内部类中访问成员变量在Java语言中是完全允许的。

这个问题很有意思,解答这个我们需要生成Java字节码,再反编译成Java看看具体生成的代码是什么。

借助JD-GUI,我们可以看到下面的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class Outer
{
private String a;

public static final class c
implements Moveable
{
public static final c INSTANCE;

static
{
c localc = new c();INSTANCE = localc;
}

public void move()
{
Moveable.DefaultImpls.move(this);
}
}
}

很有意思,我们在Kotlin类中object部分的代码最终变成了下面这个样子:

1
2
3
4
5
6
7
8
9
10
public static final class c implements Moveable {
public static final c INSTANCE;
static {
c localc = new c();INSTANCE = localc;
}

public void move() {
Moveable.DefaultImpls.move(this);
}
}

这是一个静态内部类,很明显,静态内部类是不能访问外部类成员变量的。可是问题来了,说好的匿名内部类呢?

这里一定要注意,如果你只是这样声明了一个object,Kotlin认为你是需要一个静态内部类。而如果你用一个变量去接收object表达式,Kotlin认为你需要一个匿名内部类对象。

因此,这个类应该这样改进:

1
2
3
4
5
6
7
8
9
10
11
12
class Outer {
private var a: String? = null

// 用变量c去接收object表达式
private val c = object: Moveable {
override fun move() {
super.move()
// 改进后,这里访问正常
println(a)
}
}
}

为了避免出现这个问题,谨记一个原则:如果object只是声明,它代表一个静态内部类。如果用变量接收object表达式,它代表一个匿名内部类对象。

object能干啥?

很自然地想到,Kotlin的object到底有什么作用。其实,从上文的表述来看。很明显,object至少有下面两个作用:

  • 简化生成静态内部类
  • 生成匿名内部类对象

其实,object还有一个非常重要的作用,就是生成单例对象。如果你需要在Kotlin语言中使用单例,非常简单,只需要使用object关键字即可。

1
2
3
4
5
6
7
8
9
object Singleton {
fun f1() {

}

fun f2() {

}
}

这种方式声明object和上面的方式略有区别,其最终会生成一个名为Singleton的类,并在类中生成一个静态代码块进行单例对象生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class Singleton
{
public static final Singleton INSTANCE;

public final void f1() {}

public final void f2() {}

static
{
Singleton localSingleton = new Singleton();INSTANCE = localSingleton;
}
}

在Kotlin语言中对方法进行访问的时候最终其实是通过INSTANCE实例进行中转的。

在Kotlin语言中还有一个很常用的object叫做伴随对象。所谓的伴随对象只不过是名字叫做Companion的object而已。它主要用于类中生成类似Java的静态变量,Kotlin语言针对这个变量会认为你只是希望生成一个静态变量,而不希望引入多余的类。如果你是和Java语言混合开发的话,可以使用一个注解生成和Java语言静态变量完全一样的效果。

简单总结

Kotlin语言中使用object命名的方式的确容易让人误认为只要使用这个关键字就是生成了一个对象。而从上文的表述当中,你会发现,其实不同的使用姿势将产生不同的效果。因此,在日常使用中一定要学会随机应变。如果遇到了不明白的问题,不妨来看看这篇文章是否已经解答了你的问题。如果没有,请在文章下方留言告诉我。

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

Kotlin难点解析:extension和this指针

扩展(extension)是Kotlin语言中使用非常简单的一个特性。这篇文章并不是要讲解扩展的基本用法,而是解决在一些复杂场景中,扩展容易让人产生迷惑的一些问题。除了扩展,本篇文章还将讲解this指针在Kotlin语言中的基础用法。

扩展函数难点解析

大多数场景下,你都能轻松搞定Kotlin扩展。可是,看看下面这个题目,你还能脱口而出,告诉我答案是什么吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
open class E {

}

open class E1: E() {

}

open class A {

open fun E.f() {
println("E.f in A")
}

open fun E1.f() {
println("E1.f in A")
}

fun call(e: E) {
e.f()
}
}

class A1: A() {

override fun E.f() {
println("E.f in A1")
}

override fun E1.f() {
println("E1.f in A1")
}
}

fun main(args: Array<String>) {
// a)
A().call(E())

// b)
A1().call(E())

// c)
A().call(E())

// d)
A().call(E1())
}

问题:请告诉a,b,c,d位置代码执行的输出结果是什么?

对于这个问题,恐怕你在纸上写写画画半天也不一定能给出正确答案吧。关于这个问题,其实我之前的一篇文章 [Kotlin] Lambda and Extension 中有提到过。可是,我认为这篇文章关于这部分的解释不够清晰,有必要再详细阐述一次。

Ok,let’s started。

为了解决这个问题,官方提出了两个新的概念:dispatch receiverextension receiver

  • dispatch receiver:中文翻译为分发接收者。所谓的分发接收者,就是声明这个扩展方法所在的类。即:在哪个类中声明,那个类就是你的分发接收者。
  • extension receiver:中文翻译为扩展接收者。所谓的扩展接收者,就是你实际扩展的那个类。举个例子:你针对Int类扩展了一个方法add,这个add方法的扩展接收者就是Int类实例。

为了简化,这里我们将dispatch receiver简称为DR,将extension receiver简称为ER

还记得多态的概念吗?多态是一种运行时概念,即对象的类型要等到运行时才能最终确定。因此,一些语言中也将多态叫做类型延迟加载。解决上面这个问题我们需要关注就是扩展函数是否会产生多态行为。

这里我们将产生多态行为的技术叫做动态解析,与之相反的行为称之为静态解析

为了解决上面的问题,你需要记住下面这个规则:

  • DR类型是动态解析的
  • 与之相反,ER类型是静态解析的

先看上面例子的a、b部分,很显然:

  • a代码中f函数的DR是类A,ER是类E
  • b代码中f函数的DR是类A1,ER是类E

参照上面的规则,由于DR类型是动态解析的。在A1类中我们重写了E的扩展函数f,运行时最终会执行A1类中扩展的f方法。a部分很明显会输出A类中扩展的f方法。因此,最终的输出结果如下:

1
2
E.f in A
E.f in A1

继续看c、d部分,c、d部分的DR都是A,而对于ER,c、d分别是E、E1。参照上面的规则,ER是静态解析的。在call方法声明的地方,我们传入的对象类型是E,这就决定了无论扩展方法是来自E还是其子类,将始终执行E类的扩展方法。因此,c、d部分将输出同样的结果:

1
2
E.f in A
E.f in A

由此可见,如果你牢记上述两条规则,解决问题将变得非常容易。为了加强你的记忆,我用一个表格总结上面的知识点:

- DR ER
概念 扩展方法声明所在的类 声明扩展方法的类
解析方式 动态解析 静态解析

PS:由于新版本Kotlin中针对扩展函数也加入了override关键字,这非常有助于DR和ER的理解。如果你在使用Kotlin,强烈建议你更新到最新版本。

不太一样的this指针

在Java语言中,如果你在内部类中需要外部类的引用可以将this写在类名后面。可是,试试看Kotlin,果断不行。

为了获得外部类的引用,Kotlin语言引入了@符号。举个例子:

1
2
3
4
5
6
7
class Outer {
inner class Inner {
fun f() {
println(this@Outer)
}
}
}

可以看到,为了获取外部类的引用,只需要在@后面接外部类的名称即可。

如果对应一个扩展函数,this引用指向是什么呢?先说答案,扩展函数中的this指针指向ER,即实际扩展的那个类对象。

1
2
3
fun Outer.foo() {
println(this)
}

这里的this指向foo函数的接收者Outer类实例。

this指针还有一种场景是用在lambda表达式中,这是一种比较特殊的使用场景。lambda表达式本身没有任何接收者,如果是在全局声明一个lambda表达式,将不能使用this指针。而如果是在某个类或者扩展方法中使用this指针,将指向实际所在类或者扩展方法的接收者。

如果你习惯了Kotlin语言的这种表达方式,this指针的指向就不再是一个问题了。在你习惯这种用法之前,我用一个表格简单总结一下this指针的用法:

位置 指向
类中 默认指向当前类实例,使用@操作符指向具体外部类实例
扩展函数 默认指向扩展函数的接收者
lambda表达式 默认指向实际所在类实例或所在扩展函数的接收者

总结

关于扩展,大多数情况下,你不会遇到文章开头那种复杂的情况。如果遇到了这种情况,只要清楚地区分DR和ER,并牢记DR和ER的解析方式,就能轻松应对了。对于this指针,与Java语言不一样的地方是,为了引用具体类的实例,Kotlin语言使用@符号。个人认为,这种表述方式更自然。如果遇到某些比较复杂的情况,只需要弄清楚接收者,问题就引刃而解了。

Kotlin 操作符重载及中缀调用

操作符重载其实很有意思!但这个概念却很少有人知道,使用操作符重载在某种程度上会给代码的阅读带来一定的麻烦。因此,慎用操作符被认为是一个好习惯。的确,操作符重载是一把双刃剑,既能削铁如泥,也能“引火烧身”,这篇文章将从实用的角度来讲解操作符重载的基本用法。

支持重载的操作符类型

Kotlin语言支持重载的操作符类型比较多。以最新版本1.2.21为准,目前支持重载的操作符可以归纳为以下几类:

一元操作符

一元前缀操作符

操作符 对应方法
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

以上三个操作符在日常使用中频率很高,第一个操作符在基本运算中很少使用,第二个操作符就是常见的取反操作,第三个操作符是逻辑取反操作。接下来,我们使用扩展的方式重载这三个操作符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 一元操作符
*
* @author Scott Smith 2018-02-03 14:11
*/
data class Number(var value: Int)

/**
* 重载一元操作符+,使其对Number中实际数据取绝对值
*/
operator fun Number.unaryPlus(): Number {
this.value = Math.abs(value)
return this
}

/**
* 重载一元操作符-,使其对Number中实际数据取反
*/
operator fun Number.unaryMinus(): Number {
this.value = -value
return this
}

/**
* 这个操作符通常是用于逻辑取反,这里用一个没有意义的操作,来模拟重载这个操作符
* 结果:始终返回Number中实际数据的负值
*/
operator fun Number.not(): Number {
this.value = -Math.abs(value)
return this
}

fun main(args: Array<String>) {
val number = Number(-3)
println("Number value = ${number.value}")
println("After unaryPlus: Number value = ${(+number).value}")
println("After unaryMinus: Number value = ${(-number).value}")

number.value = Math.abs(number.value)
println("After unaryNot: Number value = ${(!number).value}")
}

运行上述代码,将得到如下结果:

1
2
3
4
Number value = -3
After unaryPlus: Number value = 3
After unaryMinus: Number value = -3
After unaryNot: Number value = -3

自增和自减操作符

操作符 对应方法
a++/++a a.inc()
a–/–a a.dec()

重载这个操作符相对比较难理解,官方文档有一段简短的文字解释,翻译成代码可以这样表示:

1
2
3
4
5
6
7
8
9
10
11
12
// a++
fun increment(a: Int): Int {
val a0 = a
a = a + 1
return a0
}

// ++a
fun increment(a: Int): Int {
a = a + 1
return a
}

看懂上面的代码后,我们换成需要重载的Number类,Kotlin最终会这样处理:

1
2
3
4
5
6
7
8
9
10
11
// Number++
fun increment(number: Number): Number {
val temp = number
val result = number.inc()
return result
}

// Number++
fun increment(number: Number): Number {
return number.inc()
}

因此,重载Number类自加操作符,我们可以这样做:

1
2
3
operator fun Number.inc(): Number {
return Number(this.value + 1)
}

重载自减操作符同理,完整代码请参考我的Git版本库:kotlin-samples

二元操作符

算术运算符

操作符 对应方法
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a..b a.rangeTo(b)

前5个操作符相对比较好理解,我们以a + b为例,举个一个简单的例子:

1
2
3
4
5
6
7
8
9
10
// 重载Number类的加法运算符
operator fun Number.plus(value: Int): Number {
return Number(this.value + value)
}

fun main(args: Array<String>) {
println((Number(1) + 2))
}
// 输出结果:
Number value = 3

相对比较难理解的是第六个范围运算符,这个操作符主要用于生成一段数据范围。我们认为Number本身就代表一个整型数字,因此,重载Number是一件有意义的事情。直接看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
operator fun Number.rangeTo(to: Number): IntRange {
return this.value..to.value
}

fun main(args: Array<String>) {
val startNumber = Number(3)
val endNumber = Number(9)

(startNumber..endNumber).forEach {
println("value = $it")
}
}

// 运行结果:
value = 3
value = 4
value = 5
value = 6
value = 7
value = 8
value = 9

“In”运算符

操作符 对应方法
a in b b.contains(a)
a !in b !b.contains(a)

这个操作符相对比较好理解,重载这个操作符可以用于判断某个数据是否在另外一个对象中。我们用一个非常简单的自定义类来模拟集合操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class IntCollection { 
val intList = ArrayList<Int>()
}

// 重载"in"操作符
operator fun IntCollection.contains(value: Int): Boolean {
return this.intList.contains(value)
}

fun main(args: Array<String>) {
val intCollection = IntCollection()
intCollection.add(1, 2, 3)
println(3 in intCollection)
}

// 输出结果:
true

索引访问运算符

操作符 对应方法
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, …, i_n] a.get(i_1, …, i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, …, i_n] = b a.set(i_1, …, i_n, b)

这个操作符很有意思,例如,如果你要访问Map中某个数据,通常是这样的map.get("key"),使用索引运算符你还可以这样操作:

1
val value = map["key"]

我们继续以IntCollection类为例,尝试重写a[i]a[i] = b两个运算符,其它运算符同理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 重载a[i]操作符
operator fun IntCollection.get(index: Int): Int {
return intList[index]
}

// 重载a[i] = b操作符
operator fun IntCollection.set(index: Int, value: Int) {
intList[index] = value
}

fun main(args: Array<String>) {
val intCollection = IntCollection()
intCollection.add(1, 2, 3)
println(intCollection[0])

intCollection[2] = 4
print(intCollection[2])
}

接下来,我们用索引运算符来做一点更有意思的事情!新建一个普通的KotlinUser

1
2
3
4
class User(var name: String,
var age: Int) {

}

使用下面的方式重载索引运算符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
operator fun User.get(key: String): Any? {
when(key) {
"name" -> {
return this.name
}
"age" -> {
return this.age
}
}

return null
}

operator fun User.set(key: String, value:Any?) {
when(key) {
"name" -> {
name = value as? String
}
"age" -> {
age = value as? Int
}
}
}

接下来,你会神奇地发现,一个普通的Kotlin类居然也可以使用索引运算符对成员变量进行操作了,是不是很神奇?

1
2
3
4
5
6
fun main(args: Array<String>) {
val user = User("Scott Smith", 18)
println(user["name"])
user["age"] = 22
println(user["age"])
}

因此,索引运算符不仅仅可以对集合类数据进行操作,对一个普通的Kotlin类也可以发挥同样的作用。如果你脑洞足够大,你还可以发现更多更神奇的玩法。

调用操作符

操作符 对应方法
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ……, i_n) a.invoke(i_1, ……, i_n)

重载这个操作符并不难,理解它的应用场景却有一定的难度。为了理解它的应用场景,我们来举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class JsonParser {

}

operator fun JsonParser.invoke(json: String): Map<String, Any> {
val map = Json.parse(json)
...
return map
}

// 可以这样调用
val parser = JsonParser()
val map = parser("{name: \"Scott Smith\"}")

这里的调用有点像省略了一个解析Json数据的方法,难道它仅仅就是这个作用吗?是的,调用操作符其实就这一个作用。如果一个Kotlin类仅仅只有一个方法,直接使用括号调用的确是一个不错的主意。不过,在使用的时候还是要稍微注意一下,避免出现歧义。

广义赋值操作符

操作符 对应方法
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)

这个操作符相对比较好理解,我们以Number类为例,举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 广义赋值运算符
operator fun Number.plusAssign(value: Int) {
this.value += value
}

fun main(args: Array<String>) {
val number = Number(1)
number += 2
println(number)
}

// 输出结果:
Number value = 3

相等与不等操作符

操作符 对应方法
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

重载这个操作符与Java重写equals方法是一样的。不过,这里要注意与Java的区别,在Java端==用于判断两个对象是否是同一对象(指针级别)。而在Kotlin语言中,如果我们不做任何处理,==等同于使用Java对象的equals方法判断两个对象是否相等。

另外,这里还有一种特殊情况,如果左值等于null,这个时候a?.equals(b)将返回null值。因此,这里还增加了?:运算符用于进一步判断,在这个情况下,当且仅当b === null的时候,a、b才有可能相等。因此,才有了上面的对应关系,这里以User类为例举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
class User(var name: String?,
var age: Int?) {

operator override fun equals(other: Any?): Boolean {
if(other is User) {
return (this.name == other.name) && (this.age == other.age)
}
return false
}
}

注意:这里有一个特殊的地方,与其它操作符不一样的地方是,如果使用扩展的方式尝试重载该操作符,将会报错。因此,如果要重载该操作符,一定要在类中进行重写。

比较操作符

操作符 对应方法
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

比较操作符是一个在日常使用中频率非常高的操作符,重载这个操作符只需要掌握以上表格中几个规则即可。我们以Number类为例举一个简单的例子:

1
2
3
operator fun Number.compareTo(number: Number): Int {
return this.value - number.value
}

属性委托操作符

属性委托操作符是一种非常特殊的操作符,其主要用在代理属性中。关于Kotlin代理的知识,如果你还不了解的话,请参考这篇文章
Delegation。这篇文章介绍的相对简略,后面会出一篇更详细的文章介绍代理相关的知识。

中缀调用

看到这里,可能有一些追求更高级玩法的同学会问:Kotlin支持自定义操作符吗?

答案当然是:不能!不过,别失望,infix也许适合你,它其实可以看做一种自定义操作符的实现。这里我们对集合List新增一个扩展方法intersection用于获取两个集合的交集:

1
2
3
4
5
6
7
8
9
10
11
// 获取两个集合的交集
fun <E> List<E>.interSection(other: List<E>): List<E> {
val result = ArrayList<E>()
forEach {
if(other.contains(it)) {
result.add(it)
}
}

return result
}

接下来,我们就可以在List及其子类中使用点语法调用了。但,它看起来仍然不像一个操作符。为了让它更像一个操作符,我们继续做点事情:

  • 添加infix关键词
  • 将函数名修改为∩(这是数学上获取交集的标记符号)
    然而,万万没想到,修改完成后居然报错了。Kotlin并不允许直接使用特殊符号作为函数名开头。因此,我们取形近的字母n用于表示函数名:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 获取两个集合的交集
    infix fun <E> List<E>.n(other: List<E>): List<E> {
    val result = ArrayList<E>()
    forEach {
    if(other.contains(it)) {
    result.add(it)
    }
    }

    return result
    }

接下来,我们就可以这样调用了val interSection = list1 n list2,怎么样?是不是很像自定义了一个获取交集的操作符n?如果你希望自定义操作符,可以尝试这么做。

其实infix的应用场景还不止这些,接下来,我们再用它完成一件更有意思的事情。

在实际项目开发中,数据库数据到对象的处理是一件繁琐的过程,最麻烦的地方莫过于思维的转换。那我们是否可以在代码中直接使用SQL语句查询对象数据呢?例如这样:

1
val users = Select * from User where age > 18

纸上学来终觉浅,觉知此事需躬行。有了这个idea,接下来,我们就朝着这个目标努力。
一、先声明一个Sql类,准备如下方法:

1
2
3
4
5
6
7
infix fun select(columnBuilder: ColumnBuilder): Sql {

infix fun from(entityClass: Class<*>): Sql

infix fun where(condition: String): Sql

fun <T> query(): T

二、我们的目的是:最终转换到SQL语句形式。因此,增加如下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class ColumnBuilder(var columns: Array<out String>) {

}

class Sql private constructor() {
var columns = emptyList<String>()
var entityClass: Class<*>? = null
var condition: String? = null

companion object {
fun get(): Sql {
return Sql()
}
}

infix fun select(columnBuilder: ColumnBuilder): Sql {
this.columns = columnBuilder.columns.asList()
return this
}

infix fun from(entityClass: Class<*>): Sql {
this.entityClass = entityClass
return this
}

infix fun where(condition: String): Sql {
this.condition = condition
return this
}

fun <T> query(): T {
// 此处省略所有条件判断
val sqlBuilder = StringBuilder("select ")

val columnBuilder = StringBuilder("")
if(columns.size == 1 && columns[0] == "*") {
columnBuilder.append("*")
} else {
columns.forEach {
columnBuilder.append(it).append(",")
}
columnBuilder.delete(columns.size - 1, columns.size)
}

val sql = sqlBuilder.append(columnBuilder.toString())
.append(" from ${entityClass?.simpleName} where ")
.append(condition)
.toString()
println("执行SQL查询:$sql")

return execute(sql)
}

private fun <T> execute(sql: String): T {
// 仅仅用于测试
return Any() as T
}
}

三、为了看起来更形似,再增加如下两个方法:

1
2
3
4
5
6
7
8
9
// 使其看起来像在数据库作用域中执行
fun database(init: Sql.()->Unit) {
init.invoke(Sql.get())
}

// 因为infix限制,参数不能直接使用可变参数。因此,我们增加这个方法使参数组装看起来更自然
fun columns(vararg columns: String): ColumnBuilder {
return ColumnBuilder(columns)
}

接下来,就是见证奇迹的时刻!

1
2
3
4
5
6
7
8
fun main(args: Array<String>) {
database {
(select (columns("*")) from User::class.java where "age > 18").query()
}
}

// 输出结果:
执行SQL查询:select * from User where age > 18

为了方便大家查看,我们提取完整执行代码段与SQL语句对比:

1
2
select          *       from User             where  age > 18
select (columns("*")) from User::class.java where "age > 18"

神奇吗?
至此,我们就可以直接在代码中愉快地使用类似SQL语句的方式进行方法调用了。

总结

本篇文章从操作符重载实用的角度讲解了操作符重载的所有相关知识。如文章开头所说,操作符重载是一把双刃剑。用得好事半功倍,用不好事倍功半。因此,我给大家的建议是:使用的时候一定要保证能够自圆其说,简单来说,就是自然。我认为相对于古老的语言C++来说,Kotlin语言操作符重载的设计是非常棒的。如果你知道自己在做什么,我非常推荐你在生产环境中使用操作符重载来简化操作。

本篇文章例子代码点这里:kotlin-samples


我是欧阳锋,一个热爱Kotlin语言编程的学生。如果你喜欢我的文章,请在文章下方留下你爱的印记。如果你不喜欢我的文章,请先喜欢上我的文章。然后再留下爱的印记!

下次文章再见,拜拜!


Kotlin 代理模式

代理模式是23种经典设计模式之一,代理模式被认为是继承的更好替代解决方案;因为代理比继承更加灵活,在Java语言中,通过反射可以实现动态代理,动态代理可以实现AOP编程,即:可以动态地往已有类中添加逻辑;比如:实现事务的自动提交,异常的自动捕获,热修复等等;

在Kotlin语言中,代理模式是默认支持的,不需要任何额外的代码,你只需要记住一个关键字by。我们不妨来试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Base {
fun sayHi()
}

class BaseImpl : Base {
override fun sayHi() {
println("BaseImpl->sayHi")
}
}
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
val b = BaseImpl()
val derived = Derived(b)
derived.sayHi()
}

这里Derived作为BaseImpl的代理类,拥有BaseImpl类中的所有方法,Derived将代理BaseImpl类执行BaseImpl类中的所有方法,就像继承自BaseImpl类一样。这样说起来有点抽象,来看一下Kotlin编译器具体为我们做了一些什么。但是,怎么看呢?教大家一个方法!
大家都知道,Kotlin和Java均是JVM语言,最终均转换到同样的Java字节码,这样我们就可以先将Kotlin编译为.class文件,再反编译为.java文件,看看对应的Java代码,我们就可以看到更多的细节。下面是最终反编译生成的Java代码:

1
2
3
4
5
6
7
8
public final class Derived implements Base {
public Derived(@NotNull Base b) {
this.$$delegate_0 = b;
}
public void sayHi() {
this.$$delegate_0.sayHi();
}
}

这里,我们可以清楚地看到,Kotlin编译器为我们动态添加了一个成员变量$$delegate_0,这个成员变量代表被代理的对象,这里对应的是BaseImpl对象,Derived里面的sayHi方法最终调用是代理对象的sayHi方法,即Kotlin编译器帮我们提供了一个非常漂亮的代理模式实现。

代理属性

在一些情况下,我们可能希望某些属性延迟加载,即在我们正在需要的时候才对它赋值;亦或者我们希望可以随时监听属性值的变化;在上述这些场景中,代理属性就可以发挥作用了。

代理属性的语法格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DelegateProperty {
val d: String by Delegate()
}
class Delegate {
operator fun getValue(thisRef: Any? , property: KProperty<*>): String {
return "Invoke getValue() , thisRef = $thisRef , property name = ${property.name}"
}
operator fun setValue(thisRef: Any? , property: KProperty<*> , value: String) {
println("Invoke setValue() , thisRef = $thisRef , property name = ${property.name} , value = $value")
}
}

fun main(args: Array<String>) {
val dp = DelegatedProperty()
dp.d = "Value0" // Invoke setValue() , thisRef = DelegatedProperty@2ef1e4fa , property name = d , value = Value0

println(dp.d) // Invoke getValue() , thisRef = DelegatedProperty@2ef1e4fa , property name = d
}

这里的代理是如何实现的呢?我们知道,Kotlin的属性值会自动生成set/get方法,而代理类通过代理set/get方法生成相应的代理方法,这里的方法对应关系如下:

1
2
3
4
// thisRef对应代理对象的引用,property对应代理属性的反射属性封装
// 注意这里的代理方法一定要添加operator关键字,operator关键字是重载操作符关键字,后续的文章中会讲到,敬请期待
get() -> operator fun getValue(thisRef: Any? , property: KProperty<*>)
set() -> operator fun setValue(thisRef: Any? , property: KProperty<*> , value: T)

Kotlin标准库提供了一些常用代理的方法实现,即上文提到的几种代理,先来看第一种:延迟加载。

延迟加载

Kotlin提供了一个lazy方法用于实现延迟加载,lazy方法有一个lambda表达式参数,用于对属性进行初始化赋值,而一旦完成赋值,该lambda表达式将不会再次调用。lambda表达式调用发生在第一次使用该属性的时候,即实现了属性赋值的延迟加载。来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
// 使用标准库实现的lazy函数,实现属性的延迟加载
private val lazyValue: String by lazy {
println("调用该初始赋值表达式完成赋值")
// 这里是实际赋值
"Hello, world"
}
fun main(args: Array<String>) {
// 仅在第一次会调用lazy方法的lambda表达式
println(lazyValue) // 打印:调用该初始赋值表达式完成赋值
println(lazyValue) // 打印: Hello, world, 再次调用将不再调用lambda表达式
}

lazy方法是一个线程安全的延迟加载方法,为了加深大家的理解,根据上面的原理,我们尝试自己来实现一个非线程安全的延迟加载方法,看具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private object UNINITIALIZE_VALUE

class MyLazy<T>(initialize: ()->T) {
private var value: Any? = UNINITIALIZE_VALUE
private val initialize = initialize
operator fun getValue(thisRef: Any? , property: KProperty<*>): T {
if(value == UNINITIALIZE_VALUE) {
value = initialize()
}
return value as T
}
operator fun setValue(thisRef: Any? , property: KProperty<*> , value: T) {
this.value = value
}
}
// 为了和标准库区分,使用__lazy命名
fun <T> __lazy(initialize: () -> T): MyLazy<T> = MyLazy(initialize)

var lazyValue1 by __lazy {
println("自定义lazy初始化赋值表达式被调用")
"Hello , world"
}

fun main(args: Array<String>) {
// 自定义延迟加载函数__lazy
println(lazyValue1)
lazyValue1 = "Other value"
println(lazyValue1)
}

由此可见,实现一个延迟加载接口并不复杂,最重要的是要理解延迟加载的过程以及实现原理。总结实现延迟加载接口,需要注意三个地方:

  • 需要提供初始化lambda表达式参数,用于初始赋值
  • 需要实现代理属性对象的setValue/getValue方法,如果是val则只需要实现getValue即可
  • 需要严格确保属性不会被多次初始化

Observable属性

Kotlin标准库还提供了一个可观察属性,这个属性使用观察者模式实现,如果属性值发生变化则会调用相应的回调lambda接口通知使用者,先看一个具体的例子:

1
2
3
4
5
6
7
8
9
var observableValue by Delegates.observable("Initial value") {  prop , old , new ->
println("$old -> $new")
}

fun main(args: Array<String>) {
println(observableValue) // 打印:Initial value
observableValue = "Hello" // 打印: Initial value -> Hello
println(observableValue) // 打印:Hello
}

这里的具体实现,感兴趣的同学请参看文章开头的方法进行追踪!

Storing Properties in a Map

这也是Kotlin标准库提供的一个非常有用的特性,它主要用于JSON数据的解析。看官方的例子:

1
2
3
4
5
6
7
8
9
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}

val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))

该方法比较简单,这里就不再赘述了!

总结

至此,关于代理的介绍可以暂时告一段落了!
代理模式是一个非常经典设计模式,在解决某些问题中可以发挥事半功倍的效果。幸运的是,Kotlin语言原生支持代理模式,实现代理模式如同声明一个属性一样简单。而且,代理模式的设计也非常漂亮,仅仅使用一个关键字by极尽简约之美。在日常编码中,一定要灵活运用代理模式,比如实现延迟加载,实现属性观察等等。KotterKnife 是一个非常经典的代理模式的实现例子,有兴趣的同学可以clone该仓库,查看源码,领会代理模式的优美。

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

文章源码地址

Kotliner: https://github.com/yuanhoujun/Kotliner,
别忘了点击仓库右上方的star哦!

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×