├── 0. 如何判断一门语言的好坏.md ├── 1. Kotlin hello world!与函数声明.md ├── 10. Kotlin 类声明和构造器(constructor).md ├── 11. Kotlin 类声明与伴生对象(companion object).md ├── 12. Kotlin 作用域函数(scope function)run,let,apply,also.md ├── 13. Kotlin 作用域函数 run,let,apply,also 的使用.md ├── 14. Kotlin 使用高阶函数处理集合数据.md ├── 15. Kotlin 究竟该不该用 lateinit?.md ├── 2. Kotlin 变量声明与类型推断.md ├── 3. Kotlin 变量声明与空安全(Void Safety).md ├── 4. Kotlin 变量声明和变量状态设计.md ├── 5. Kotlin 函数声明与闭包(Closure).md ├── 6. Kotlin 变量声明与基本类型(Primitive Type).md ├── 7. Kotlin 变量声明和属性(property).md ├── 8. Kotlin 函数声明与默认参数(Default argument).md ├── 9. Kotlin 函数声明和扩展(extension).md └── README.md /0. 如何判断一门语言的好坏.md: -------------------------------------------------------------------------------- 1 | 我在组内推广 Kotlin 遇到不少挑战,虽然我自己觉得它确实是一门优秀的语言,有着丰富的特性,能提高我们开发效率,减少 bug 的出现,但同事们并不觉得。而且他们的观点往往不是“Kotlin 并没有比 Java 好多少”,而是“Kotlin 就没比 Java 好“。Kotlin 语法不习惯;Kotlin 这样强推这么多年,也不温不火,说明 Kotlin 不行。 2 | 3 | 所以本着“先问是不是,再问为什么”的原则,我们在讨论“Kotlin 比 Java 好在哪里之前”,必须先说清楚“Kotlin 比 Java 好吗”这个问题。 4 | 5 | 关于判断语言是否优秀,我们听过最多的可能就是“XXX 是最好的语言”了。不过这种旧世界的观点,在大家充分学习了网络上的编程知识之后,现在都成为大家调侃的段子了。现在,我们更喜欢“编程语言没有优劣之分”,“没有最好的编程语言,只有最适合的应用场景”这样的论调。这听起来就很有哲理,很编程正确嘛!如果把这些的观点套在 Java 和 Kotlin 身上,也可以得到“要根据具体场景,有些场景确实 Java 更好”的观点。 6 | 7 | 不过今天,我们不谈这些和稀泥的观点。这篇文章会告诉大家,客观准确评判一门语言好坏的标准。 8 | 9 | 无论如何,编程语言是给人使用的,那我们就从人本身特点出来来讲这个逻辑。我们人是靠大脑思考的,而大脑有个致命的缺点:**容量有限,且有一定错误率**。这就导致我们程序员在编写**状态复杂**,**流程冗长**的代码的时候,容易出现错误。这是人大脑的特点决定的,无法避免。 10 | 11 | 但劳动人民的智慧是无穷的,程序员们想出各种办法来降低自己犯错概率。从机器码到汇编,到面向流程,到面向对象,到设计模式,编码规范,高级语法特性,不断有新编程技术的出现,让程序员们可以写**更简单**的代码。也就是**用更抽象的表达来表示同样的意图**。 12 | 13 | > 阿拉伯人:30 加 42 等于 72。 14 | > 15 | > 法国人: 30 加 40又2 等于 60又10又2。 16 | 17 | 你能想象,用汇编去实现你现在要的一个业务,需要多长时间?写出来会有多少个 bug 吗?当你用 Python 快速实现了一个算法并开始验证的时候,别人可能还在用 C++ 吭哧吭哧地写着 std::vector 的 for 循环;当你用 Swift 快速实现了一个 iOS demo 的时候, 别人可能还在用 objective-C 吭哧吭哧的写着头文件声明;当你用 Go 的协程快速实现了并发处理时,别人可能还在用 Java 吭哧吭哧地实现一个线程池。又或者说,用 C++ 和 Java 实现同一个需求,C++ 你需要花费额外的精力关注内存管理,数组越界,类型安全等问题,这样你留给业务本身的精力就少了,开发变慢了,bug 也更容易出现了。 18 | 19 | > Java:你知道单例线程安全的四种写法吗? 20 | > 21 | > Kotlin:你是说 by lazy 吗? 22 | 23 | 编程语言是为了实现业务而存在的,那我们就应该选择一门实现业务**编写效率高,维护成本低**的编程语言。 24 | 25 | 编写效率高意味着同样的功能我可以用更少的代码实现;同时 sdk 功能齐全,轮子多,大部分基础组件不需要重复开发。没错就像 Python 那样。Kotlin 相比 Java,他的语法表达更简洁,更容易写出低耦合,高内聚的代码;且和 Java 互操作的特性,可以直接使用 Java 的轮子,大大缩短了建设 Kotlin 生态的过程。 26 | 27 | 维护成本低分为几个方面,分别是:bug 少,代码简洁易懂,对需求变更友善。 28 | 29 | bug 少。国外有对千行 bug 数量进行了研究,研究提出 bug 数量和所使用的语言没有直接关系,和语法表达流畅性有关系[Basis for claim that the number of bugs per line of code is constant regardless of the language used 30 | ](https://stackoverflow.com/questions/2898571/basis-for-claim-that-the-number-of-bugs-per-line-of-code-is-constant-regardless), [Basis for claim that the number of bugs per line of code is constant regardless of the language used](https://stackoverflow.com/questions/2898571/basis-for-claim-that-the-number-of-bugs-per-line-of-code-is-constant-regardless)。 我觉得可以理解为,人大脑的犯错频率是比较固定的,使用时长越长,出现的“bug”也就越多。如果你能通过选择一门语言,更快的实现指定功能,那么 bug 数量会相应减少。 31 | 32 | 代码简洁易懂。Kotlin 需要编写的代码更少,是因为 Kotlin 对语意有更精简的表达,你在习惯之后可以比 Java 更快的阅读完同样的功能。这点在后面我会继续说明。 33 | 34 | 对需求变更友善。这是代码简洁易懂的自然延伸,所谓 less is more 嘛。 35 | 36 | > Pythonista:人生苦短,我用 Python。 37 | > 38 | > Androider:以前我没得选,现在我想做个 Kotlin boy。 39 | 40 | PS:我也了解到很多同学拒绝 Kotlin 的理由是“不习惯”。比如说语法用着不习惯,看着也不习惯,很难看懂云云。我想说的是,无论是编程语言还是其他工作外的事,千万要忌讳用“习惯”作为理由。那些 30 多岁的外企程序员,失业中年危机,不就是“习惯”习出来的么?习惯会让你避开新的东西,而能让你能力,事业,资产产生“增量”的,往往就是这些新的东西。新的东西最容易产生“增量”。 41 | 42 | 我们判断一个东西好不好,有没有价值,有没有必要去投入,不要用“习惯”。要把好与不好列出来。就像我这样,我说 Kotlin 好,我把好的理由讲给你听,你觉得没道理,你可以针对这些点进行反驳,或者提出新的观点,然后和我进行讨论。用“不习惯”作为理由来拒绝,只会让自己错失“增量”的机会。 43 | 44 | -------------------------------------------------------------------------------- /1. Kotlin hello world!与函数声明.md: -------------------------------------------------------------------------------- 1 | 激动人心的 Hello world 教程,他来了!首先我们看 Java 的 hello world: 2 | 3 | ``` 4 | public class HelloWorld { 5 | public static void main(String[] args) { 6 | System.out.println("Hello world!"); 7 | } 8 | } 9 | ``` 10 | 11 | 非常熟悉。可在我漫长的编程生涯中,我大概是第 5 次在 Google 搜索了“Java Hello world”之后,才能独自完整的默写出来。。因为他有些“不太好记住”的点:比如他必须通过该类的一个叫 main 的 public 的 static 的函数,且这个函数入参必须是有且仅有一个 String[] 数组。如果错了其中一个,那你就没法运行。 12 | 13 | 这是 Kotlin 版本: 14 | 15 | ``` 16 | fun main() { 17 | println("Hello world!") 18 | } 19 | ``` 20 | 21 | 写起来还蛮快乐的,是吧?没有太多多余的东西,很简洁。但大家可能也会有很多疑问。那我们一起来看: 22 | 23 | ### 1. Kotlin 的 Hello world 没有声明类。 24 | 25 | 这很不 Java!要知道 Kotlin 或者其他基于 JVM 的语言无论怎么设计,他最终也是要在 JVM 上跑的,而 JVM 恰好就是一套基于类来设计的运行机制,和 Java 的思想是一致的。你 Kotlin 现在自己搞一套,连类都没有了,怎么在 JVM 上跑? 26 | 27 | 这其实是 Kotlin 编译器的功劳。这点很重要,以后你遇到 Kotlin 的新特性,和 Java 对不上的时候,就回想这点:**都是 Kotlin 编译器的功劳,他把 Kotlin 代码转换成了符合 Java 思想的 JVM 字节码。**小本本记好啦,这句话将贯穿整个 Kotlin 学习。 28 | 29 | 如果你用过 [jadx](https://github.com/skylot/jadx)的话,可以反编译试试。这里的结论就是:HelloWorld.kt 会被编译为一个叫 HelloWorkKt 的类,把这段代码塞进去。当然你再去创建一个 HelloWorldKt 的类就会报错了,你可以试试 :) 30 | 31 | ### 2. 没有分号。 32 | 33 | 古时候,一行完整的代码用分号来分隔。后来大家觉得分号已经没有必要了,也基本没有人会在一行写几行代码了。新的语言一部分是柔和派,分号变为可选;另一部分激进派直接去掉了分号。Kotlin 属于前者,如果你写一个分号,IDE 爸爸会告诉你,没得必要,但不会报错。如果你故意把两行代码写成一行,中间加一个分号,这个分号就是必要的。 34 | 35 | ### 3. 函数的声明通过 fun 关键字。 36 | 37 | 欸?Java 不需要关键字来声明函数呀。以 JavaScript 为首的语言认为,函数是一等公民,应当做一个对象看待。这样函数就可以被持有,被传递,提高他的灵活性。所以像 JavaScript 这样的语言,方法声明会有关键字,否则你就不知道自己到底是在调用一个函数,还是在执行一个变量持有的函数了。 38 | 39 | Kotlin 也支持函数是一等公民,所以函数声明需要关键字。Java 其实也有类似的东西,他叫匿名类。只不过匿名类需要声明一个类,再用匿名类的特殊写法去创建一个看起来像函数对象的东西。 40 | 41 | ### 4. 函数属性默认是 public。 42 | 43 | Java 默认是用的很少的 package private。 44 | 45 | ### 5. 函数默认返回值是 Unit。 46 | 47 | Java 没有默认返回值,需要显式声明为 void。Kotlin 不声明则返回为 Unit,Unit 是一个类,而且是一个单例。为啥 Kotlin 不用 void 呢?因为声明返回值为 Unit 可以**让一切对象化**,在某些场景可以简化代码编写。举一个简单的情况,你可以写 ```return println("Hello world!")```,而不再需要写成两行了。 48 | 49 | 如果需要声明返回类型,则在函数声明末尾以“: Class”的方式声明,如:```fun getString(): String```。这样的写法是为了方便做成可选的声明,后面文章讲解的类型声明也是这样的。 50 | 51 | ### 6. println 代替了 System.out.println。 52 | 53 | 学 Java 的时候大家应该都吐槽过,写个打印好废键盘啊!这是因为 Java 严格按照对象调用的规则办事,方法必须是属于类的,除非你在类里面调同一个类里的办法,可以省略```this.```,其他地方都需要加对象才能调用一个方法(类也是对象嘛)。 54 | 55 | 所以 Kotlin 是怎么做到不用指定对象也能调用方法呢?是有顶层声明(top-level declaration)的特性。就像第一点提到的那样,Kotlin 编译器会通过各种各样的方法把顶层声明的函数编译成对象方法调用的形式。顶层声明还有更多酷炫的能力,比如给任意一个类“增加方法”,后面我们会展开来讲。 56 | 57 | 以上就是 Kotlin 的 Hello world 涉及到的几个知识点。是不是觉得要写个 Hello world 也要懂这么多很费劲呢?其实要弄懂 Java 的 Hello world 也很费劲的,只是你已经过去那个初学的阶段了。在 Java 的基础上理解 Kotlin 相对还是简单的,如果你有其他语言的开发经验那就更简单了,因为 Kotlin 的特性,基本都能在某个语言上找到,它本身并不是新特性的创造者,他只是好用特性的搬运工。 58 | -------------------------------------------------------------------------------- /10. Kotlin 类声明和构造器(constructor).md: -------------------------------------------------------------------------------- 1 | ### 1. Java 和 Kotlin 构造器代码对比 2 | 3 | Java 的构造器声明和方法声明没有太大区别,也支持重载,唯一的限制是:必须调用父类构造器(如果父类只有一个构造器而且是无参的,编译器会帮你自动加上,这是特例)。我们使用 Java 多年,构造器很少会给我们带来不便,也不曾听人吐槽 Java 的构造器声明的不合理,便是无功无过,规规矩矩。但现代编程语言还是从构造器身上找到了优化空间,~~Scala--~~Kotlin 是其中之一。 4 | 5 | 我们不妨直接上代码对比 Kotlin 和 Java 的构造器声明的区别。现在我们有这样一个 android.view.View 的子类,它的 Java 实现: 6 | 7 | ``` 8 | public class RecordingBottomView extends ConstraintLayout implements View.OnClickListener { 9 | 10 | private static final String TAG = "RecordingBottomView"; 11 | private static final int DEFAULT_VALUE = 100; 12 | private EffectBarDialog effectBarDialog = new EffectBarDialog(getContext()); 13 | 14 | private TextView channelButton; 15 | public TextView effectButton; 16 | private TextView restartButton; 17 | private TextView finishBtn; 18 | 19 | private RecordPlayButton playButton; 20 | private TextView switchCameraButton; 21 | private TextView filterButton; 22 | private TextView resumeTips; 23 | 24 | private int defaultValue = DEFAULT_VALUE; 25 | 26 | public RecordingBottomView(Context context) { 27 | super(context); 28 | init(context, null, 0); 29 | } 30 | 31 | public RecordingBottomView(Context context, AttributeSet attrs) { 32 | super(context, attrs); 33 | init(context, attrs, 0); 34 | } 35 | 36 | public RecordingBottomView(Context context, AttributeSet attrs, int defStyleAttr) { 37 | super(context, attrs, defStyleAttr); 38 | init(context, attrs, defStyleAttr); 39 | } 40 | 41 | private void init(Context context, AttributeSet attrs, int defStyleAttr) { 42 | LayoutInflater.from(context).inflate(R.layout.recording_bootom_panel, this, true); 43 | findViewById(R.id.recording_channel_switch_btn).setOnClickListener(this); 44 | channelButton = findViewById(R.id.recording_channel_switch_btn); 45 | effectButton = findViewById(R.id.recording_effect_btn); 46 | effectButton.setOnClickListener(this); 47 | 48 | 49 | restartButton = findViewById(R.id.recording_restart_btn); 50 | restartButton.setOnClickListener(this); 51 | finishBtn = findViewById(R.id.recording_finish_btn); 52 | finishBtn.setOnClickListener(this); 53 | 54 | switchCameraButton = findViewById(R.id.song_record_camera_switch); 55 | switchCameraButton.setOnClickListener(this); 56 | filterButton = findViewById(R.id.song_record_filter); 57 | filterButton.setOnClickListener(this); 58 | playButton = findViewById(R.id.recording_play_button); 59 | playButton.setOnClickListener(this); 60 | resumeTips = findViewById(R.id.song_record_resume_tip_btn); 61 | 62 | effectBarDialog.setSoundSelectListener(reverbId -> { 63 | LogUtil.i(TAG, "soundSelect"); 64 | return Unit.INSTANCE; 65 | }); 66 | effectBarDialog.setToneChangeListener((isUp, toneValue) -> { 67 | LogUtil.i(TAG, "toneChange"); 68 | return Unit.INSTANCE; 69 | }); 70 | effectBarDialog.setVolumeChangeListener(volume -> { 71 | LogUtil.i(TAG, "volumeChange"); 72 | return Unit.INSTANCE; 73 | }); 74 | 75 | if (attrs != null) { 76 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RecordingBottomView); 77 | defaultValue = ta.getInt(R.styleable.RecordingBottomView_default_value, DEFAULT_VALUE); 78 | } 79 | } 80 | 81 | @Override 82 | public void onClick(View v) { 83 | // handle click 84 | } 85 | } 86 | ``` 87 | 88 | 非常经典的代码,在构造器中初始化所有的子 View 成员变量以及 View 参数。 89 | 90 | 对应的,Kotlin 风格的实现如下: 91 | 92 | ``` 93 | class RecordingBottomView(context: Context, attrs: AttributeSet?, defStyleAttr: Int): 94 | ConstraintLayout(context, attrs, defStyleAttr), 95 | View.OnClickListener { 96 | 97 | constructor(context: Context): this(context, null, 0) 98 | 99 | constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0) 100 | 101 | init { 102 | LayoutInflater.from(context).inflate(R.layout.recording_bootom_panel, this, true) 103 | findViewById(R.id.recording_channel_switch_btn).setOnClickListener(this) 104 | } 105 | 106 | private val effectBarDialog = EffectBarDialog(context).also { 107 | it.soundSelectListener = { reverbId: Int? -> 108 | LogUtil.i(TAG, "soundSelect") 109 | } 110 | it.toneChangeListener = { isUp: Boolean?, toneValue: Int? -> 111 | LogUtil.i(TAG, "toneChange") 112 | } 113 | it.volumeChangeListener = { volume: Float? -> 114 | LogUtil.i(TAG, "volumeChange") 115 | } 116 | } 117 | 118 | private val channelButton = findViewById(R.id.recording_channel_switch_btn).also { 119 | it.setOnClickListener(this) 120 | } 121 | 122 | var effectButton = findViewById(R.id.recording_effect_btn).also { 123 | it.setOnClickListener(this) 124 | } 125 | 126 | private val restartButton = findViewById(R.id.recording_restart_btn).also { 127 | it.setOnClickListener(this) 128 | } 129 | 130 | private val finishBtn = findViewById(R.id.recording_finish_btn).also { 131 | it.setOnClickListener(this) 132 | } 133 | 134 | private val playButton: RecordPlayButton? = null 135 | private val switchCameraButton = findViewById(R.id.recording_finish_btn).also { 136 | it.setOnClickListener(this) 137 | } 138 | 139 | private val filterButton = findViewById(R.id.recording_finish_btn).also { 140 | it.setOnClickListener(this) 141 | } 142 | 143 | private val resumeTips = findViewById(R.id.song_record_resume_tip_btn) 144 | 145 | private val defaultValue: Int = let { 146 | attrs?: DEFAULT_VALUE 147 | 148 | val ta = context.obtainStyledAttributes(attrs, R.styleable.RecordingBottomView) 149 | val value = ta.getInt(R.styleable.RecordingBottomView_default_value, DEFAULT_VALUE) 150 | ta.recycle() 151 | value 152 | } 153 | 154 | override fun onClick(v: View) {} 155 | 156 | companion object { 157 | private const val TAG = "RecordingBottomView" 158 | private const val DEFAULT_VALUE = 100 159 | } 160 | } 161 | ``` 162 | 163 | > 这里暂且不展开谈 默认参数,view binding,also let 闭包的知识点,这些知识点会在其他文章单独介绍。 164 | 165 | 对我而言,在我接触 Kotlin 这种构造器声明之前,我没有想过 Java 的构造器声明有什么缺点。但当我接触之后,我开始思考 Kotlin 为什么要这样设计构造器声明,以及 Java 构造器声明的不足之处: 166 | 167 | 1. **Java 构造器成员变量如果依赖构造参数,它们的声明和最终赋值是分离的,同一个成员变量的代码是低内聚的。**而且像```defaultValue```这个参数,虽然他有初始值,但在一定条件下,他可能会被重新赋值。这些问题都会增加阅读者的心智负担; 168 | 169 | 2. 所有的初始化代码都在一个函数中,很容易出现“超级函数”。**不同成员变量的初始化代码大部分互相没有联系,但是却以先后顺序的形式耦合在同一个函数中,这是高耦合的。** 170 | 171 | 3. Java 构造器允许重载,虽然设计规范提倡重载函数应最终调用同一个实现,以得到清晰的逻辑表达。但这不是强制性的。这意味着你可能会遇到多个构造器各自拥有自己的实现,这会加重问题 1,2 的严重性。 172 | 173 | 对应的,Kotlin 采用了如下对策来一一解决这些问题: 174 | 175 | 1. property 声明初始化时允许使用主构造器参数,变量声明和初始化代码都写在同一个地方,代码是高内聚的; 176 | 177 | 2. 使用 let 闭包后,成员变量的所有的初始化代码都可以写在闭包内。不同的成员变量初始化代码相互独立,代码是低耦合的; 178 | 179 | 3. 仅允许一个主构造器,其他构造器为从构造器,并约定从构造器必须调用主构造器,让主构造器去调用父构造器。 180 | 181 | > 如果 Kotlin 类没有声明主构造器,全部都是从构造器,则退化为 Java 构造器风格,没有调用主构造器的约束。这样的设计一是为了 Java 转 Kotlin 代码时能兼容旧代码结构,不用重构也能直接转换为 Kotlin 代码;二也方便了 Java 转 Kotlin 自动化工具的实现。但 property 的初始化无法引用从构造器的入参,因为从构造器是可以有多个的,从调用上无法保证每个从构造器的每个参数都存在。 182 | 183 | 184 | ### 2. Kotlin 构造器实现分析 185 | 186 | 上面我们简单的过了一遍 Kotlin 对 Java 构造器的优化,但 Java 采用这样的设计,是因为它忠实的反映了 JVM 的构造器实现。而 Kotlin 的构造器设计,并不符合 JVM 的实现。**Kotlin 要最终在 JVM 上运行,必须在编译期处理,最终变回类似 Java 构造器的实现。** 187 | 188 | 我们直接看一下 Kotlin 编译再反编译后的字节码: 189 | 190 | ``` 191 | public RecordingBottomView(@NotNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 192 | Intrinsics.checkParameterIsNotNull(context, "context"); 193 | super(context, attrs, defStyleAttr); 194 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RecordingBottomView); 195 | int value = ta.getInt(R.styleable.RecordingBottomView_default_value, 100); 196 | ta.recycle(); 197 | this.defaultValue = value; 198 | LayoutInflater.from(context).inflate(R.layout.recording_bootom_panel, (ViewGroup) this, true); 199 | findViewById(R.id.recording_channel_switch_btn).setOnClickListener(this); 200 | EffectBarDialog it = new EffectBarDialog(context); 201 | it.setSoundSelectListener(RecordingBottomView$effectBarDialog$1$1.INSTANCE); 202 | it.setToneChangeListener(RecordingBottomView$effectBarDialog$1$2.INSTANCE); 203 | it.setVolumeChangeListener(RecordingBottomView$effectBarDialog$1$3.INSTANCE); 204 | this.effectBarDialog = it; 205 | View findViewById = findViewById(R.id.recording_channel_switch_btn); 206 | ((TextView) findViewById).setOnClickListener(this); 207 | this.channelButton = (TextView) findViewById; 208 | View findViewById2 = findViewById(R.id.recording_effect_btn); 209 | ((TextView) findViewById2).setOnClickListener(this); 210 | this.effectButton = (TextView) findViewById2; 211 | View findViewById3 = findViewById(R.id.recording_restart_btn); 212 | ((TextView) findViewById3).setOnClickListener(this); 213 | this.restartButton = (TextView) findViewById3; 214 | View findViewById4 = findViewById(R.id.recording_finish_btn); 215 | ((TextView) findViewById4).setOnClickListener(this); 216 | this.finishBtn = (TextView) findViewById4; 217 | View findViewById5 = findViewById(R.id.recording_finish_btn); 218 | ((TextView) findViewById5).setOnClickListener(this); 219 | this.switchCameraButton = (TextView) findViewById5; 220 | View findViewById6 = findViewById(R.id.recording_finish_btn); 221 | ((TextView) findViewById6).setOnClickListener(this); 222 | this.filterButton = (TextView) findViewById6; 223 | this.resumeTips = (TextView) findViewById(R.id.song_record_resume_tip_btn); 224 | RecordingBottomView recordingBottomView = this; 225 | if (attrs == null) { 226 | Integer.valueOf(100); 227 | } 228 | } 229 | 230 | public RecordingBottomView(@NotNull Context context) { 231 | Intrinsics.checkParameterIsNotNull(context, "context"); 232 | this(context, null, 0); 233 | } 234 | 235 | public RecordingBottomView(@NotNull Context context, @Nullable AttributeSet attrs) { 236 | Intrinsics.checkParameterIsNotNull(context, "context"); 237 | this(context, attrs, 0); 238 | } 239 | 240 | ``` 241 | 242 | 可以看到,property 和 init 块的初始化代码会被顺序的放入主构造器中,也就是说代码是从上往下按顺序执行的。**因此 Kotlin 的初始化代码不仅可以使用主构造器的参数,还可以使用比自己先初始化的 property 和 init 块。** 243 | 244 | -------------------------------------------------------------------------------- /11. Kotlin 类声明与伴生对象(companion object).md: -------------------------------------------------------------------------------- 1 | ### 1. companion object 的诞生 2 | 3 | > Scala 说,要有伴生对象。 4 | > 5 | > 于是 Kotlin 便有了 companion object。 6 | 7 | companion object 的出现是为了解决 Java static 方法的反面向对象(Anti-OOP)的问题。static 方法无法声明为接口,无法被重写——用更学术的话来说,static 方法没有面向对象的**消息传递**和**延迟绑定**特性[[参考](https://stackoverflow.com/questions/4002201/why-arent-static-methods-considered-good-oo-practice)]。而 Scala 为了完成一切皆对象的使命,以及提高与 Java 的兼容性,提出了**伴生对象**这个概念来代替 static 方法。随后出身的 Kotlin 也借鉴了这个概念。 8 | 9 | **companion 伴生对象是一个对象,它在类初始化时被实例化。** 因为伴生对象不再是类的 static 方法,而是某个类的实例化对象,所以它可以声明接口,里面的方法也可以被重写,具备面向对象的所有特点。 10 | 11 | ### 2. companion 的实现 12 | 13 | 在 Kotlin 中,调用 Java 的 static 方法和调用 Kotlin 的 companion object 方法是一样的: 14 | 15 | ``` 16 | AJavaClass.staticFun() 17 | AKotlinClass.companionFun() 18 | ``` 19 | 20 | 而在 Java 中,调用 static 方法和 Kotlin 的伴生 companion object 方法,有一点不同: 21 | 22 | ``` 23 | AJavaClass.staticFun(); 24 | AKotlinClass.Companion.companionFun(); 25 | ``` 26 | 27 | 从 Java 的调用我们可以发现,companion object 的 JVM 字节码体现,是一个声明了一个叫 Companion 的 static 变量。 28 | 29 | 而 Kotlin 调用一致,其实是编译器的特殊处理的结果。 30 | 31 | 如果我们反编译```AKotlinClass```,可以看到: 32 | 33 | ``` 34 | // AKotlinClass.class 35 | public final class AKotlinClass { 36 | public static final Companion Companion = new Companion(null); 37 | } 38 | ``` 39 | 40 | ``` 41 | // AKotlinClass$Companion.class 42 | public final class AKotlinClass$Companion { 43 | private DownloadExecutor$Companion() { 44 | } 45 | 46 | public /* synthetic */ DownloadExecutor$Companion(DefaultConstructorMarker $constructor_marker) { 47 | this(); 48 | } 49 | 50 | public final void companionFun() { 51 | } 52 | } 53 | ``` 54 | 55 | 可以看到,Companion 是一个叫 ```AKotlinClass$Companion``` 的类的实例,带 $ 符号表示这个类是 AKotlinClass 的内部类,名字叫 Companion,所以在```AKotlinClass```中直接```new Companion(null) ```即可。 56 | 57 | > 你也许还留意到实例化 Companion 使用的是一个带 DefaultConstructorMarker 入参的构造器。它出现的场景是,如果是 Kotlin 编译器生成的特殊构造器,就会带这个参数。在这里,Kotlin 希望能够实例化 Companion 类,但又不想声明一个 public 的构造器,于是就声明了这样一个特殊的构造器。DefaultConstructorMarker 值永远为 null。 58 | 59 | > DefaultConstructorMarker 出现的另一个场景是:带默认参数的构造器。因为带了默认参数后,构造器需要增加 int 类型的 bitmask 入参,来识别哪个入参需要被替换为默认参数。而为构造器增加一个入参,容易和其他构造器“撞车”,即构造器的入参完全一样,导致编译失败。所以默认参数的构造器末尾还会增加一个 DefaultConstructorMarker 入参,来防止构造器参数一致的问题。[[参考](https://stackoverflow.com/questions/53912047/two-additional-types-in-default-constructor-in-kotlin)] 60 | -------------------------------------------------------------------------------- /12. Kotlin 作用域函数(scope function)run,let,apply,also.md: -------------------------------------------------------------------------------- 1 | ### 0. 绕不开的四兄弟 2 | 3 | 学习 Kotlin 一定绕不开 run/let/apply/also 这四兄弟,它们是 Kotlin 使用频率最高的扩展方法(扩展方法在之前文章有介绍),它们也被称为作用域函数(scope functions)。今天我们就来了解一下它们。本文依然是按代码比较,字节码分析,和扩展思考三个方面进行分析。 4 | 5 | 搞懂其中一个,其他作用域函数可以视为其变种。这篇文章我们先看 ```run``` 方法。 6 | 7 | ### 1. run 方法使用 8 | 9 | 在工程中,我们有一段这样的 Java 代码: 10 | 11 | ``` 12 | public class PlayManager { 13 | /** 初始值为空,需要在资源初始化之后再拿到对象 */ 14 | private Player player = null; 15 | 16 | /** 播放 */ 17 | public void play(String path) { 18 | if (player != null) { 19 | player.init(path); 20 | player.prepare(); 21 | player.start(); 22 | } 23 | } 24 | } 25 | ``` 26 | 27 | Kotlin等效代码为: 28 | 29 | ``` 30 | public class PlayManager { 31 | /** 初始值为空,需要在资源初始化之后再拿到对象 */ 32 | private var player: Player? = null 33 | 34 | /** 播放 */ 35 | fun play(path: String) { 36 | player?.init(path) 37 | player?.prepare() 38 | player?.start() 39 | } 40 | } 41 | ``` 42 | 43 | 如果用上 Kotlin 的 ```run``` 会是这样的: 44 | 45 | ``` 46 | public class PlayManager { 47 | /** 初始值为空,需要在资源初始化之后再拿到对象 */ 48 | private var player: Player? = null 49 | 50 | /** 播放 */ 51 | fun play(path: String) { 52 | player?.run { 53 | init(path) 54 | prepare() 55 | start() 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | > 这里的 run 调用是一种函数调用的特殊写法,即当 lambda 作为函数的最后一个参数时,可以写在函数括号外部,也就是说```object.run { }```和```object.run({ })```是等价的。这种写法的好处在我看来,一是不用再去末尾数括号了,写 Java 的时候声明一个匿名类比如```View.OnClickListener```,总是忘了加括号,在 Kotlin 没有这个烦恼;二是像```run```,```map``` 这种以函数作为参数的高阶函数,代码写起来看起来都更简洁利落。 62 | 63 | 看起来简洁了不少。 64 | 65 | ```run```的功能很简单,它就做了两件事: 66 | 67 | 1. 把 lambda 内部的 ```this``` 改成了对应的调用对象。这个看起来很神奇,我们稍后再分析; 68 | 2. run 函数会返回 lambda 的返回值。 69 | 70 | ```run``` 方法达到了三个效果: 71 | 72 | 1. 因为```this``` 的变化,不再需要重复的输入变量,和**链式调用**异曲同工,但你并不需要额外花费精力来编写链式调用的代码; 73 | 74 | 2. 把可空对象转换为了非空对象,因为```run```方法是问号调用,```player``` 不为空才会执行。因为考虑到并发,Kotlin 要求每次调用可空属性的时候都进行判空,如此一来属性这个小朋友就会有很多问号。。使用 ```run``` 方法等效于先把可空属性用临时变量持有再使用,这样就消除了并发竞争的影响(Java 经常也有这种代码,不过要自己手写罢了)。 75 | 76 | 3. 在一个函数里声明的这个一个小“代码块“,表示和其他无关的代码隔离,**实现了函数内部的高内聚**。这个效果可以增加代码的可读性,让人一看就明白:“哦,这是针对这个对象的一系列操作,这个函数里关于这个对象的使用我只需要关注这个代码块就可以了”。 77 | 78 | 第三点是我尤其喜欢的一个点,我觉得这样的设计不仅是为了提高开发效率,它更是在引导开发者写出好维护的代码。在写 Java 的时候,大家都很容易不自觉的写出某个对象在函数头操作一下,隔几行调用一下,隔几行又操作一下的代码。阅读者很容易误以为这些代码之间有着顺序上的耦合,从而继续按照这个“隐含的规则“来维护代码。却不知当时的开发者只是想到哪写到哪,实际并不存在这样的隐含关系。使用 ```run``` 可以在函数内部快速建立起一个个代码块,让函数拥有更清晰的结构,又不用花费很大精力把代码块拆成一个个小函数,毕竟给函数起名字可是非常头秃的事情。 79 | 80 | 说到不用起名字,lambda 本身就有“匿名函数”的外号,这样的使用方法可以说十分贴切了。而从耦合程度来看,代码块介于函数和过程代码之间。 81 | 82 | > 函数是面向过程的产物,它天生就很容易产生耦合度高的代码。就我看来,作用域函数更像是给函数打上的一个“补丁”。 83 | 84 | ### 3. run 方法代码分析 85 | 86 | ```run``` 源码如下: 87 | 88 | ``` 89 | @kotlin.internal.InlineOnly 90 | public inline fun T.run(block: T.() -> R): R { 91 | contract { 92 | callsInPlace(block, InvocationKind.EXACTLY_ONCE) 93 | } 94 | return block() 95 | } 96 | ``` 97 | 98 | 卧槽看起来好吊(看不懂),不是说好了很简单吗?因为这函数涉及的基本都是编译器相关的,平时开发用不到。这里包含了泛型,inline,类扩展 lambda(```T.() -> R```),contract 这 4 个概念。泛型我就默认你懂了,毕竟这里只讲 Kotlin 的新东西,Kotlin 泛型和 Java 的泛型除了写法没有什么区别。剩下的三个概念我们简单过一下吧。 99 | 100 | **inline**,中文名内联函数,是 C/C++ 的老活儿了。inline 的意思是,虽然你声明了一个函数,但在编译期调用这个函数的地方会被替换为函数包含的代码。inline 的好处是调用该方法不再有调用方法的性能消耗,即不会跳转和产生栈帧;坏处是可能会使二进制文件膨胀,尤其是函数很大的时候。所以 inline 适合被频繁调用但代码量很小的函数,```run``` 就很符合这个条件。我们可以因此得出结论:**由于编译器编译时会把 inline 函数内联到实际调用位置,所以使用 ```run``` 方法不会有方法调用的性能损耗。** 101 | 102 | 而 ```@kotlin.internal.InlineOnly```,实际效果为对 Java 不可见(private),因为 Java 不支持 inline。对 Java 不可见后,这个 inline 方法则可以不在字节码里存在,因为调用的地方全部都内联了。 103 | 104 | > 值得注意的是,和 C/C++ 一样,Kotlin 的 inline 也不是必然内联的。具体机制,我们有机会再聊(还没有学到)。 105 | 106 | > 虽然 Java 没有内联函数,但是 JVM 是有内联优化的,只是这个优化无法精确控制。Java 的设计者一直尽可能让 Java 语言保持简单,这可能也是 Java 为什么能持续热门这么久的原因。 107 | 108 | **类扩展 lambda**(关键字 lambda with class extension),即入参的声明 ```T.() -> R```。lambda 我们了解了,扩展方法我们也了解了(强行假设你看过之前的文章),扩展 lambda 也可以理解为给类扩展一个 lambda 函数。它的效果也和扩展方法一样,在 扩展 lambda 作用域内,你可以以对象作为 ```this``` 来操作这个对象。 109 | 110 | 最后一个 **contract 契约**,指的是代码和 Kotlin 编译器的契约。举一个例子,我们对局部变量增加了如果为空则 return 的逻辑,Kotlin 编译器便可以智能的识别出 return 之后的局部变量一定不为空,局部变量的类型会退化为非空类型。但如果我们把是否为空的代码封装进一个扩展方法如 ```Any?.isNotNull()``` 里,那么编译器就无法识别 return 后面的代码局部变量是否为空了,这个局部变量依然是可空类型。 111 | 112 | 那么这个时候 contract 就派上用场了。我们可以声明一个 contract,告诉编译器如果```Any?.isNotNull()``` 返回了 true,则表示对象非空。这样我们在代码里执行了 ```isNotNull()``` 方法之后,return 后面的代码,局部变量也能正确退化为非空类型。具体例子我们可以看官方 Collections.kt 的 ```Collection.isNullOrEmpty()```。 113 | 114 | 了解了 contract 的作用后我们再看 ```run``` 包含的契约。它意思是这个 lambda 只会被 ```run``` 方法执行一次,且 ```run``` 执行完后不会再被执行。对于了解到这个额外信息的 Kotlin 编译器,他就可以更有针对性的优化这里的代码(怎么针对,也还没有学到。。)。 115 | 116 | ### 4. 为何 Java 没有作用域函数? 117 | 118 | 作用域函数需要**类扩展**和**内联**这两个能力,才能最大化体现其价值。没有类扩展,```this``` 的切换需要通过继承或者匿名类来实现,且做不到通用;而像 ```let``` 这种不需要切换 ```this``` 的作用域函数,因为没有类扩展能力而为了追求通用性,也只能通过静态工具类来实现,效果是打折扣的。 119 | 120 | 而没有内联能力的 Java,虽然有 JVM 内联优化支撑,但内联优化只对 final 且调用次数数量级较大的方法有效。如果像 Kotlin 这样规模化的使用作用域函数,对性能是有不可忽视的影响的。 121 | 122 | ### 5. 其他作用域函数的使用和适用场景 123 | 124 | 下一篇! 125 | -------------------------------------------------------------------------------- /13. Kotlin 作用域函数 run,let,apply,also 的使用.md: -------------------------------------------------------------------------------- 1 | 上一篇文章我们介绍了作用域函数,并以其中一个作用函数```run```为例,介绍了作用域函数的使用和原理。除了```run```之外,Kotlin 官方还内置了```let```,```apply```,```also```这几个作用域函数,下面我们一起来他们的相同点和区别,并举例说明他们的使用场景。 2 | 3 | ### 1. 4 个作用域函数 = 2 个特性的两两组合 4 | 5 | ```run```,```let```,```apply```,```also```,这 4 个作用域函数,其实是 2 个特性的组合结果: 6 | 7 | * 调用作用域函数的对象,是作为```this```传入,还是作为唯一参数(默认为```it```)传入; 8 | * 作用域函数的返回值,是调用的对象,还是 lambda 的返回值。 9 | 10 | 配合伪代码解释一下: 11 | 12 | ``` 13 | val result = object.xxx { param -> 14 | val lambdaResult = param.toString() 15 | lambdaResult 16 | } 17 | ``` 18 | 19 | ```xxx```可以是```run```,```let```,```apply```,```also```其中一个。 20 | 21 | 如果```xxx = run/let```,那么 ```result == lambdaResult```; 22 | 而如果```xxx = apply/also,那么```result == object```。且```lambdaResult```这一行会报错,因为```apply```,```also``` 的 lambda 返回值必须是 Unit。 23 | 24 | > lambda 最后一行的值会作为 lambda 的返回值。它等价于 ```return@xxx lambdaResult```。```@xxx```表示返回的是这个lambda,而不是退出整个上层方法。如果是不在最后一行返回的代码,比如异常分支,就可以(也只能)这样用。 25 | 26 | 27 | 如果```xxx = let/also```,那么```param == object```; 28 | 而如果```xxx = run/apply```的话,那么 lambda 是个无参数 lambda,不存在```param```,``` param ->``` 这里会报错; 29 | 30 | > 如果 lambda 为单参数 lambda,此时```param ->```可以省略,Kotlin 提供默认的单参数名```it```。这个很方便,文章后面的代码都会采用这种简写。 31 | 32 | 总结成表就是: 33 | 34 | 特性 | 返回值为this | 返回值为lambda结果 35 | -|-|- 36 | **调用对象转换为this** | apply | run | 37 | **调用对象转换为it** | also | let | 38 | 39 | 我们只需要知道 4 个作用域函数分别是 2 个特性的两两组合即可。用的时候直接查源代码,不需要专门记忆。 40 | 41 | ### 2. run/let/apply/also 各自的使用场景举例 42 | 43 | 我们已经知道这 4 个作用域函数的特点了,那么我们怎么用好它们呢?下面一起来看下这几个作用域函数的使用场景。 44 | 45 | #### run 46 | 47 | 这是工程中的一段代码: 48 | 49 | ``` 50 | mRecordViewHelper?.run { 51 | mEffectsView.visibility = View.INVISIBLE 52 | mLyricViewer.visibility = View.INVISIBLE 53 | mStatusView = AudioRecordStatusView(CommonContext.getApplicationContext(), null) 54 | enableSoloCountBackView() 55 | } 56 | ``` 57 | 58 | ```run```方法很适合用在**对某个对象做一系列操作**的地方。这里可空对象```mRecordViewHelper```使用```run```方法把自己转换成非空对象```this```“传入”到 lambda 中,并在 lambda 内部进行一系列的赋值和方法调用。 59 | 60 | ```run```的返回值是 lambda 的最后一行结果,在这个例子是```Unit```。使用 lambda 结果的用例较少,主要就是这种转换为```this```的用法。如果后续遇到特别适合的再补充。 61 | 62 | #### let 63 | 64 | 使用```run```还是使用```let```更像是一个个人喜好的问题,因为有些人就是不习惯把调用对象转换成```this```,而是更喜欢用入参```it```。 65 | 66 | 使用```run```还是使用```let```的确没有太大区别。不过还是有这几种情况,我更建议你用```let```: 67 | 68 | * 调用对象主要作为参数,而不是用于初始化或方法调用时。 69 | 70 | ``` 71 | // 赋值和调用的方法都是类的成员和方法,不是 mRecordViewHelper 的成员和方法 72 | mRecordViewHelper?.run { 73 | myFirstView = firstView 74 | mySecondView = secondView 75 | setTargetView(targetView) 76 | } 77 | 78 | // 使用 let,不会存在两个 this 混用,代码可读性更高。 79 | mRecordViewHelper?.let { 80 | myFirstView = it.firstView 81 | mySecondView = it.secondView 82 | setTargetView(it.targetView) 83 | } 84 | ``` 85 | 86 | * 当 lambda 中需要用到类的```this```时; 87 | 88 | ``` 89 | // 因为 this 的变化,导致引用类的 this 需要加@MainActivity。这个和 Java 匿名类的 MainActivity.this 效果一样 90 | mRecordViewHelper?.run { 91 | setOnClickListener(this@MainActivity) 92 | } 93 | 94 | // let 不会改变 this,在需要引用类的 this 的时候比较方便。 95 | mRecordViewHelper?.let { 96 | it.setOnClickListener(this) 97 | } 98 | ``` 99 | 100 | 总结一下:当 lambda 主要执行的是调用对象的方法和赋值时,建议使用```run```;而当调用对象主要用作参数时,建议使用```let```。当 lambda 会用到类的```this```时,建议使用```let```。 101 | 102 | 103 | #### apply 104 | 105 | ```apply```和```run```的区别主要在于,```apply```返回的是调用对象。这个特性使得```apply```很适合用来做类似初始化的工作。如: 106 | 107 | ``` 108 | class VideoConfig { 109 | val profile = VideoEncodeProfile().apply { 110 | audioBitRate = 44100 111 | videoWidth = 480 112 | videoHeight = 480 113 | videoFrameRate = 30 114 | setDefaultQuality() 115 | } 116 | } 117 | ``` 118 | 119 | ```apply``` 也很适合用来做 property 的初始化,这样 property 的初始化代码就不用写在 init 块里了,做到了代码的高内聚。上面代码的```profile``` 就是 property 初始化的例子。 120 | 121 | #### also 122 | 123 | also 的适用场景,和```run```与```let```一样,是与```apply```来对比的。具体建议也和```run```与```let```一样: 124 | 125 | 当 lambda 主要执行的是调用对象的方法和赋值时,建议使用```apply```;而当调用对象主要用作参数时,建议使用```also```。当 lambda 会用到类的```this```时,建议使用```also```。 126 | 127 | ### 3. 只有 4 个作用域函数吗? 128 | 129 | 细心的同学可能已经发现,在 Standard.kt 中,除了```run```,```let```,```apply```,```also```之外,还有好几个作用域函数。其实掌握了这 4 个作用域函数,已经覆盖了大部分使用场景。剩下的几个使用需求没有那么的迫切,但掌握之后,可以帮助你写出更有 Kotlin 味道的代码。 130 | 131 | 下一篇文章会介绍 Standard.kt 中剩余的作用域函数。 132 | -------------------------------------------------------------------------------- /14. Kotlin 使用高阶函数处理集合数据.md: -------------------------------------------------------------------------------- 1 | 本文将介绍如何使用 Kotlin 的高阶函数,如`sumBy`, `reduce`, `fold`, `map`,`filter`,`forEach` 等,来应对常见的集合数据处理场景。不了解高阶函数的同学可以先看下之前的文章。 2 | 3 | ### 遍历求和 sumBy 4 | 5 | 场景:输入一个账户列表`List`,求这些账户的财产总和`sum`。 6 | 7 | 一般来说 Java 可以这样写: 8 | ``` 9 | public int getAccountsSum(List accounts) { 10 | int sum = 0; 11 | for (Account a: accounts) { 12 | sum += a.value; 13 | } 14 | return sum; 15 | } 16 | ``` 17 | 18 | Kotlin 可以使用高阶函数 `sumBy`: 19 | 20 | ``` 21 | val sum = accounts.sumBy { it.value } 22 | ``` 23 | 24 | 那么`sumBy`做了什么呢?点击源码可以看到,其实它做的事情和上面 Java 实现的`getAccountsSum`是一样的,只是增加的值是通过我们传入的 lambda 来计算,而不是写死的`Account.value`。**这种通过传入函数来完成函数功能的函数,被称为高阶函数,高阶函数也因此具有很高的通用性和复用效率。** 25 | 26 | > 不仅传入函数作为参数的函数被称为高阶函数,返回值为函数的函数也同样被称为高阶函数。 27 | 28 | ### 遍历求值 reduce 29 | 30 | `sumBy`有一点不好,他只能求和,而且只接受`Int`和`Double`两种类型的值(sumBy:不然我起这个名字干嘛?)。如果我们要得到一个更复杂的逻辑的结果呢? 31 | 32 | 场景:输入一个列表`List`,返回它们全部相乘的结果。 33 | 34 | Java: 35 | ``` 36 | public int getResult(List values) { 37 | int result = 0; 38 | for (Account a: accounts) { 39 | result *= values; 40 | } 41 | return result; 42 | } 43 | ``` 44 | 45 | Kotlin 我们可以使用`reduce`: 46 | 47 | ``` 48 | val result = values.reduce { acc, v -> acc * v } 49 | ``` 50 | 51 | `reduce` 的逻辑是:将初始值`acc`设置为集合的第一个值,然后从第二个值开始,依次执行`acc = lambda(acc, v)`,遍历完后返回`acc`。**`reduce`不仅限做加法运算,它比`sumBy`具有更广的通用性。 52 | 53 | 那如果`reduce`可以代替`sumBy`,为什么还需要`sumBy`?——因为它写起来更简单呀! 54 | 55 | > 如果集合为空,`reduce`会抛出 UnsupportedOperationException("Empty collection can't be reduced.") 56 | 57 | ### 更通用的遍历求值 fold 58 | 59 | 细心的同学已经发现了,`sumBy`的场景和`reduce`的场景用的是不同的数据结构。**因为`acc`会被初始化为集合的第一个元素,所以`reduce`函数的输出也被限制为集合的范型类型**。也就是说,`sumBy`的场景无法用`reduce`代替。 60 | 61 | 那 Kotlin 有没有能指定`acc`类型的高阶函数?有的,它叫`fold`。 62 | 63 | 我们再回到`sumBy`的场景:输入一个账户列表`List`,求这些账户的财产总和`sum`: 64 | 65 | ``` 66 | val result = accounts.fold(0) { acc, v -> acc + v.value } 67 | ``` 68 | 69 | `fold`比`reduce`多了一个参数——初始值,用来赋值给`acc`。得益于范型,我们可以通过这个办法来指定`acc`的类型。这样一来,`fold`可以完美替代`sumBy`的场景。而相比`fold`,`sumBy`更专用,表意更清晰,写起来也更简洁。 70 | 71 | `fold`还有另一点好:因为`acc`由传入参数初始化,所以没有集合不能为空的限制。所以绝大部分情况下,我都建议使用`fold`来代替`reduce`。 72 | 73 | > JavaScript 的 reduce 函数就是 Kotlin 的 fold 函数。u1s1,Kotlin 的 reduce 函数挺危险的,还有类型限制,不建议使用。 74 | 75 | ### 过滤集合 filter 76 | 77 | 场景:输入一个账户列表`List`,返回资产小于 100 的账户: 78 | 79 | Java: 80 | ``` 81 | public List getPoorAccounts(List accounts) { 82 | List qbAccounts = new ArrayList<>(); // 想起虾米音乐的 穷逼 VIP 了 83 | for (Account a: accounts) { 84 | if (a.value < 100) { 85 | qbAccounts.add(a); 86 | } 87 | } 88 | return qbAccounts; 89 | } 90 | ``` 91 | 92 | Kotlin 可以使用`filter`函数: 93 | ``` 94 | val qbAccounts = accounts.filter { it.value < 100 } 95 | ``` 96 | 97 | `filter`的逻辑是,新建一个空的 ArrayList(),然后把 lambda 返回值为 true 的元素加入到这个列表里。 98 | 99 | ### 列表生成列表 map 100 | 101 | 场景:输入一个账户列表`List`,找到所有资产大于 10000 的账户,封装成 VIP 账户返回: 102 | 103 | Java: 104 | ``` 105 | public List getVipAccounts(List accounts) { 106 | List vipAccounts = new ArrayList<>(); 107 | for (Account a: accounts) { 108 | if (a.value >= 10000) { 109 | vipAccounts.add(new VipAccount(a)); 110 | } 111 | } 112 | return vipAccounts; 113 | } 114 | 115 | ``` 116 | 117 | Kotlin 可以通过`filter`函数加`map`函数完成: 118 | ``` 119 | val vipAccounts = accounts 120 | .filter { it.value >= 10000 } 121 | .map { VipAccount(it) } 122 | ``` 123 | 124 | 第一步我们用`filter`函数筛选出资产大于 10000 的账户,然后用`map`函数将过滤后的每一个账户转换为`VipAccount`。`map`的逻辑也很简单,它回返回一个和调用者大小相同的列表,具体的元素值为 lambda 的执行结果。 125 | 126 | ### 实在不适合,就用 forEach 吧 127 | 128 | 如果遇到了已知高阶函数都不适合的场景,不妨试试用`forEach`代替传统的 for 循环。为什么?因为写起来稍微简单一点。。 129 | 130 | ``` 131 | accounts.forEach { 132 | println("account: $it") 133 | } 134 | ``` 135 | 136 | 什么?你还想要 index下标? 137 | 138 | ``` 139 | accounts.forEachIndexed { index, account -> 140 | println("index: $index") 141 | println("account: $account") 142 | } 143 | ``` 144 | 145 | 不仅`forEach`有下标版本`forEachIndexed`,几乎所有高阶函数都有对应的 Indexed 版本。 146 | 147 | Kotlin 官方提供了数十个高阶函数,但其实掌握了以上几个高阶函数,基本可以 cover 所有场景了。其他的只是写的简洁还是写的复杂一点的区别。而且你还有另一条路可以走:自己写一个特定的高阶函数。 148 | 149 | ### 担心性能? 150 | 151 | 大家可能会担心,如此频繁的声明 lambda,会不会使得类的数量大量膨胀?其实官方提供的高阶函数,都是用`inline`关键字修饰的。这意味着不仅高阶函数的调用最终会被函数的实际代码代替,而且声明的 lambda 也会被解析成具体的代码,而不是方法调用。所以**Kotlin 高阶函数用 inline 关键字修饰,所以 lambda 不会生成新的 jvm class**。而我们在声明自己的高阶函数时,也应该用`inline`关键字修饰,防止类数量膨胀。 152 | 153 | 大家可能担心的另一点,像`map`,`filter`这样返回列表的高阶函数,每一次操作都会生成一个列表,这会不会增加垃圾回收的压力?答案是会的。但如果数据量不是万级别的,操作频率不是毫秒级别的,对性能的影响实在小之又小,特别是在移动端的场景更是难以遇到。但我们还是要了解高阶函数对性能开销,在对性能要求高的位置避免对象申请(如UI绘制的回调)。 154 | 155 | ### Java 有高阶函数吗? 156 | 157 | Java 也类似高阶函数的能力,如`Collections.sort`这种允许自定义排序的方法,和 Java 8 的 steam API。但因为 Java 没有 inline 无法有效的优化 lambda,且 Java 的 lambda 没有完整的闭包特性,无法修改外部变量。还有一些语法的原因,Java 的高阶函数使用起来相对没有那么舒服。 158 | -------------------------------------------------------------------------------- /15. Kotlin 究竟该不该用 lateinit?.md: -------------------------------------------------------------------------------- 1 | 2 | ## 使用 lateinit 的初衷 3 | 4 | 你是如何看待 lateinit?不少同学对它敬而远之,特别是使用 lateinit 踩坑之后。因为被 lateinit 标记的变量,不再接受空安全检查,它的表现更像是一个普通的 Java 变量。也有同学喜欢尽可能的用上它,把 lateinit 作为介于 nonnull 和 nullable 之间的一个状态:对象构造时为 null,在某一个时刻被初始化后一直都是 nonnull,**这样属性的不确定性便减少了。** 5 | 6 | 我也是一个 lateinit 的坚定支持者。原因之一是 **lateinit 属性比 nullable 属性在行为上更可靠**。所谓可靠,即其行为是确定的。当调用 lateinit 变量时,它此时如果没有被初始化,就会抛出```UninitializedPropertyAccessException```;如果已经初始化了,则操作一定会执行。反看 nullable 变量,你在任一时刻操作它的时候,它都可能不被执行,因为可空变量在任意时刻都可能被置空。这样的行为在排查问题的时候会造成阻碍。**为了减少程序运行的不确定性,我更希望尽可能使用 lateinit 代替 nullable。** 7 | 8 | 另一个原因是既然 Kotlin 语言设计者提供这样的关键字,说明是有可用之处的。 9 | 10 | ## 使用 lateinit 的坚持 11 | 12 | 理性分析完,随后我便开始一顿操作。只要是符合以下条件,我就会使用 lateinit 修饰属性: 13 | 14 | * 该属性在对象构造时无法初始化(缺少必要参数),在某个阶段被初始化之后会一直使用。典型的初始化阶段:```Activity.onCreate()```,自定义模块的 ```init()```; 15 | * 保证对象的调用都在初始化之后 16 | * 属性无法用空实现代替。 17 | 18 | 这个策略看起来是没什么问题的,执行的也比较顺利。自测没有问题,测试那边也顺利通过了。但在灰度的期间还是出现了 ```UninitializedPropertyAccessException```。 19 | 20 | Crash 量也不多,但总还是得解的。Crash 的原因无非就一个:**在初始化 lateinit 属性之前调用了该属性。**而解决方案根据不同情况有两种: 21 | 22 | * **是异常路径导致**,如 ```Activity.onCreate()``` 时数据不正确,需要 finish Activity 不再执行后续初始化代码。此时 Activity 仍然会执行 ```onDestroy()```,而 lateinit 属性没有被初始化。如果 ```onDestroy()``` 有对 lateinit 属性的操作,此时就会抛出 ```UninitializedPropertyAccessException```。 23 | 24 | **解决方案**:使用 ```::lateinitVar.isInitialized``` 方法,对异常路径的 lateinit 属性进行判断,如果没有初始化则不操作。 25 | 26 | **对比 nullable 属性**:lateinit 属性会 crash,nullable 属性不会,且和 lateinit 属性加了初始化判断的效果一致。这种场景下 nullable 属性表现的更好。 27 | 28 | * **是代码逻辑结构不正确导致**,如在某些情况下,上层在调用模块 ```init()``` 方法之前,就调用了模块的其他方法。此时抛出 ```UninitializedPropertyAccessException```。 29 | 30 | **解决方案**:调整代码调用逻辑,保证调用模块```init()```方法之前不调用模块的其他方法。 31 | 32 | **对比 nullable 属性**:lateinit 属性会 crash,nullable 属性不会。但 lateinit 属性会把问题暴露出来,而 nullable 属性会把问题隐藏起来,导致问题难以发现和解决。 33 | 34 | 开发者对 lateinit 的争论也大多源自于此。**支持 lateinit 的开发者,是希望代码有更好的逻辑性;反对 lateinit 的开发者,是希望代码有更好的健壮性。**而对于我来说,我更希望代码有更好的逻辑性,但我也认可“希望代码有更好的健壮性”的想法,就看开发者的取舍了。 35 | 36 | 这个想法使我坚持使用 lateinit 半年以上。而这一段使用 lateinit 的痛点,也让我开始重新思考 lateinit 的收益。 37 | 38 | ## 使用 lateinit 的痛苦 39 | 40 | 理论和实践都完善了,但使我苦恼的是,```UninitializedPropertyAccessException```并没有得到高效的解决,而是三头两日时不时的在灰度时冒出来,使我被迫打断当前工作,花上一点时间解决,并延长版本灰度的时间。这不是我想要的效果。 41 | 42 | ```UninitializedPropertyAccessException```主要出现这几种场景: 43 | 44 | * 新代码使用了 lateinit 特性,因没有考虑异常路径在测试期间出现 crash; 45 | * 旧代码重构后对部分属性使用了 lateinit 特性,在复杂的线上环境中出现 crash; 46 | * 模块内部代码调整/外部调用逻辑调整,如调用时机的调整,导致之前没有问题的代码,在复杂的线上环境中出现 crash。 47 | 48 | Kotlin 的 ```UninitializedPropertyAccessException```本质上和 Java 的空指针错误是一样的,都是**错误的估计此处对象不可能为空**导致的。在 Java 中我们通过增加一堆空判断来解决这个问题,Kotlin 可以使用 nullable 对象。 49 | 50 | 而 lateinit 通过舍弃空安全机制,把空安全交回到开发者手上(就像 Java 那样)。但在这几个月的实践中,我发现让开发者自己掌控空指针问题,是困难的。 51 | 52 | 我发现之前我对 lateinit 的思考,缺少了一个很重要的角度:**软件工程的角度。**代码是不断迭代的,维护者可能不止一个人,而 lateinit 对空指针问题的保护不足,容易让新的空指针问题出现在代码迭代之后。没有从软件工程的角度去看待问题,导致我对代码的规划过于理想,让代码降低了健壮性。现在我想给 lateinit 增加这样一个观点:**lateinit 是一个和软件工程相悖的特性,它不利于软件的健康迭代。** 53 | 54 | ## 使用 lateinit 的建议 55 | 56 | 如果你仍想使用 lateinit,那么我建议: 57 | 58 | 1. 充分考虑异常分支的执行情况; 59 | 2. 充分考虑异常时序的执行情况; 60 | 3. 充分考虑代码稳定性,是否容易发生需求变更导致结构调整。 61 | 62 | 目前依然有典型的 lateinit 适用场景,如```Activity.onCreate()```初始化的属性。但是不要忘了如果有可能初始化失败,需要在异常路径```onDestroy()```上增加```::lateinitVar.isInitialized```判断。 63 | 64 | > 对于 Fragment,如果在```onCreate```执行了 ```finish()```,它的异常路径会是```onCreateView()```,```onViewCreate()```和```onDestroy()```。 65 | -------------------------------------------------------------------------------- /2. Kotlin 变量声明与类型推断.md: -------------------------------------------------------------------------------- 1 | 本文介绍 Kotlin 变量声明涉及的相关知识点。首先我们来回顾一下 Java 局部变量声明的几个例子(成员变量的修饰符先不讨论): 2 | 3 | ``` 4 | // 播放器的一些变量 5 | boolean isPlaying = false; 6 | final String songName = "Dingdingdong"; 7 | final ReadyForPlayingData readyForPlayingData = new ReadyForPlayingData(); 8 | WeakReference onProgressListener = new WeakReference<>(this); 9 | ResultData result = getPlayingResult(); 10 | ``` 11 | 12 | 以上基本涵盖了所有情况:基础类型,字符串,对象,范型,函数返回值接收。接下来我们看 Kotlin 是怎么声明的: 13 | 14 | ``` 15 | // 播放器的一些变量 16 | var isPlaying = false 17 | val songName = "Dingdingdong" 18 | val readyForPlayingData = ReadyForPlayingData(); 19 | var onProgressListener = WeakReference(this); 20 | var result = getPlayingResult() 21 | ``` 22 | 23 | 微微统计一下,Java 需要打 263 个字符,Kotlin 需要打 198 个字符。不考虑自动补全帮助的话,**Kotlin 变量声明的效率比 Java 高 (263 - 198) / 198 = 33%**。一般程序员打字速度在理想(思维行云流水)情况下可以去到 150~200 CPM (字符/分钟),这意味者在声明这段变量 Kotlin boy 比 Java boy 能**节省 20~26 秒**。 24 | 25 | > 感兴趣的同学试试这个网站:[livechatinc.com](https://www.livechatinc.com/typing-speed-test)。点击 global-scores 还可以看到全球人民的速度分布。 26 | > 27 | > 我是 232 CPM,53 WPM。考虑到编程需要输入大小写和标点,实际会慢不少。 28 | > 29 | > 玩完记得回来。。 30 | 31 | **更高效率的代码编写可以提高你的开发效率。**诚然有很多二指禅的大神,但当你思路确定,需要快速的编写出来的一段小代码并调试的时候,这种效率的优势是实实在在的,特别在变量声明这种低思考密度的代码上。这也是现代语言的威力。 32 | 33 | 那么理清了 Kotlin 变量声明带给我们的好处后,我们一起来看一下里面的几个知识点: 34 | 35 | ### 1. 类型推断与 var 36 | 37 | Kotlin 不再需要显式的声明变量的类型,取而代之的是**通过赋值的类型来判断**。事实证明,绝大部分情况都是 work 的。而且编译器非常聪明,甚至连参杂了多种类型的范型都能推断出来!极小部分情况需要显式声明,如: 38 | 39 | * 被赋值的类型不是期望的类型,如想声明为其父类 40 | * 某些极限情况会出现无法推断的情形,如循环推断依赖 41 | 42 | 但确实是极小部分的情况,而且 IDE 都能给出解决办法。 43 | 44 | 声明变量使用 var / val 来代替原本的声明类型的地方。而需要声明类型的时候,在变量名后以“: Class”的形式声明,如:```var abc: CharSequence = "abc"```。 45 | 46 | > 2000 年后出现的编程语言基本都支持类型推断了。连 Java 8 也开始支持类型推断。 47 | > 48 | > 可参考:[程式語言歷史](https://zh.wikipedia.org/wiki/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80%E6%AD%B7%E5%8F%B2#%E7%8F%BE%E4%BB%8A%E7%9A%84%E8%B6%A8%E5%8B%A2) 49 | 50 | ### 2. final 与 val 51 | 52 | val = final var,不过 Kotlin 没有 final 这个关键字,只是代表的意义是这个意思。这个知识点已经讲完了,再见! 53 | 54 | 开个玩笑,我们还是需要知道为什么不要 final var,要val。在 Java 年代,我们很少用 final 这个关键字,虽然很多变量,类型和函数都符合 final 的设定。大部分变量我们只会设置一次,大部分的函数也不会被继承。那为什么不用上呢?唯一的原因就是因为打 final 太麻烦了!**而 val 就是为了解决“final”打起来太麻烦而设计的**。 55 | 56 | final 属性其实是一个很好用的代码约束,他代表这个变量后面不会再被修改。如果是个 Java 成员变量,你甚至不需要他被担心设置为 null。否则你就要在很多地方加上非空判断。或者在首次维护一段别人代码的过程中,需要时刻考虑这个变量是否会被更改。 57 | 58 | final 意味这这个变量的可能性变少了,我们在阅读代码的过程中,不需要再去关注这个变量的赋值变化,这对我们快速读懂代码是很有帮助的,毕竟我们脑容量都是有限的,并不能同时关注非常多的变化。**更少的变化,意味着更清晰易懂的逻辑。** 59 | 60 | > Swift 是 var 和 let。 61 | 62 | 63 | -------------------------------------------------------------------------------- /3. Kotlin 变量声明与空安全(Void Safety).md: -------------------------------------------------------------------------------- 1 | 上一篇文章介绍了 Koltin 的声明类型语法,但我有意避开了 Kotlin 类型系统里最重要的部分:**空安全(Void Safety/Null Safety)**。在 Kotlin 中,不可能为空的变量和可能为空的变量被强行分开了(Java 有 @Nullable 和 @NonNull 注释,但只会提供警告)。那 Kotlin 为什么要这样设计呢?我们来看一下今天的代码场景:(只想看使用办法的可以跳过这一节) 2 | 3 | ## 0. 场景分析 4 | 5 | 某一天你正在优雅的编写新业务代码,老板突然告诉你,**有一个线上的空指针 crash,赶紧处理一下**。你赶紧 git stash 了自己的代码,切换到出问题的那个类。 6 | 7 | 这是一个管理音频播发的类,叫 PlayerController,用来播放用户上传的 ugc 音频内容。播放是一个很基础通用的功能,**所以这个类依赖了一个播放库 AudioPlayer,PlayerController 主要是实现业务功能**。 8 | 9 | 这个类之前的维护者刚离职,你临时接任,对里面的结构是不够熟悉的。这个类年代久远,在某个初期版本就上线了,承载了无数的业务变更。里面代码逻辑混乱,业务和通用代码耦合在了一起。你想过重构,但功能实在太多了,需要很长的时间,**且现在功能也比较稳定了,重构的收益对业务增长没有明显帮助**。那还是先打个补丁呗。 10 | 11 | 我们来看看代码: 12 | 13 | PlayerController.java: 14 | 15 | ``` 16 | /** 17 | * 用户音频 ugc 播放器。 18 | * 如果看到奇怪的逻辑,请不要随便删除,那都是为了规避 19 | * AudioPlayer 库一些奇怪的 bug,或者是为了兼容业务做的处理。 20 | */ 21 | public class PlayerController { 22 | private AudioPlayer mAudioPlayer; 23 | 24 | public PlayerController() { 25 | 26 | } 27 | 28 | /** 初始化,只会初始化一次 */ 29 | public void init () { 30 | // 构造播放组件 31 | if (mAudioPlayer != null) { 32 | mAudioPlayer = AudioPlayer(); 33 | } 34 | } 35 | 36 | /** 播放前需要先初始化数据 **/ 37 | public void prepare(String audioPath) { 38 | // 设置音频文件路径 39 | if (mAudioPlayer != null) { 40 | mAudioPlayer.prepare(audioPath); 41 | } 42 | } 43 | 44 | /** 开始播放 **/ 45 | public void play() { 46 | if (mAudioPlayer != null) { 47 | // 前置条件判断 48 | // ... 49 | mAudioPlayer.play(); 50 | } 51 | } 52 | 53 | /** 暂停 **/ 54 | public void pause() { 55 | if (mAudioPlayer != null) { 56 | mAudioPlayer.pause(); 57 | } 58 | } 59 | 60 | /** 跳转到指定时间 **/ 61 | public void seekTo(long time) { 62 | if (mAudioPlayer != null) { 63 | mAudioPlayer.seekTo(time); 64 | } 65 | } 66 | 67 | public void stop() { 68 | if (mAudioPlayer != null) { 69 | // 数据处理 70 | // ... 71 | mAudioPlayer.stop(); // 该行空指针错误了 72 | } 73 | } 74 | 75 | public void release() { 76 | if (mAudioPlayer != null) { 77 | mAudioPlayer.release(); 78 | mAudioPlayer = null; 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | 这是个很典型的**依赖了底层组件的封装类**。初始化,释放,播放,暂停这些是外部接口。里面还充斥着很多空判断和 proxy 的代码。这样写代码迅速膨胀了起来。 85 | 86 | > 这个类在后面讲解很多 Kotlin 特性的时候都会引用它,可以多看两眼 87 | 88 | 开始 crash 分析。通过错误上报,我发现是 ```mAudioPlayer.stop()```这行空指针错误了。mAudioPlayer 在```init()```时被赋值,```release()```时被释放,且为了防止内存泄漏被设置为 null。再考虑到并发操作,即**mAudioPlayer**这个变量在任何使用的时候都可能为 null。 89 | 90 | 但外部已经有空条件判断了,且这是最新的版本才暴露的问题,为什么会这样呢? 91 | 92 | 我通过 git 提交记录排查后了解到,是```mAudioPlayer.stop()```之前新增了一些业务代码,而**新增代码有耗时操作。这导致了在空判断时非空,但进入 if 代码块之后,线程被切换了,上层调用了```release()```,等线程再切回来的时候 mAudioPlayer 已经变成 null 了,再执行就出现了空指针错误。** 93 | 94 | 那最简单的解决办法就是**给```mAudioPlayer.stop()```单独再包一层***。虽然很丑,但很管用,大伙也很喜欢用,特别是灰度不允许大幅改动的时候。 95 | 96 | 又或者是**给所有 mAudioPlayer 操作都加上锁 synchronized**。不过考虑到里面 API 有耗时操作,这样写有可能会造成 UI 卡顿。 97 | 98 | > 不加锁的话也有多次调用,即破坏幂等性的风险。 99 | 100 | 总之事情就这样暂时解决了。代码随着时间的迁移,越来越多变量可能为空的地方加上了```if (xxx != null)```的保护代码,**甚至可能一个类 10% 的行都是空指针保护!涉及到逻辑冗长的地方,空保护的嵌套甚至到达了 5 层以上!**那画面太美。。 101 | 102 | 这确实是我们 Java Boy 的最通用解决办法。那么 Kotlin Boy 可以如何优雅的解决这个问题呢? 103 | 104 | ## 1. Kotlin 非空类型/可空类型(NonNull/Nullable)声明 105 | 106 | 最开始时我们提到:**在 Kotlin 中,不可能为空的变量和可能为空的变量被强行分开了。**具体是怎么分开的呢?很简单,**默认的类型声明不能为空,类型后面跟问号"?"则可以为空。**我们来看下面这段代码: 107 | 108 | Nullable.kt: 109 | 110 | ``` 111 | fun main() { 112 | var string1: String = "123" // ok 113 | string1 = "456" // ok 114 | 115 | var string2: String = null // 编译器报错了 116 | 117 | var string3: String? = null // ok 118 | string3 = "456" // ok 119 | string3 = null // ok 120 | 121 | var string4 = "123" // ok,类型推断为 String 122 | string4 = null // 编译器报错了 123 | 124 | var string5 = null // ok,类型推断为 Nothing? 125 | string5 = "123" // 编译器报错了 126 | } 127 | ``` 128 | 129 | 观察 string1,string2 我们可以得出: 130 | 当你像 Java 那样声明一个 String 对象的时候,他在之后的赋值也是不能被赋值为空的。这意味着**如果一个变量的类型为 String,则他在任何时候都不可能为空。** 131 | 132 | 观察 string3 我们可以得出: 133 | 声明对象为 String? 类型,可以将其设置为空。典型场景是,在你初始化这个变量的时候,还暂时无法得到其值,就必须用可空类型的声明方法了。 134 | 135 | 观察 string4,string5 我们可以得出: 136 | **类型推断是完全根据初始化时的赋值来确定的。**他不会根据后面的赋值作为依据来推断这个变量的类型。所以我们需要像 string3 那样显式声明为 String?。至于 Nothing 类型我们暂且不管,实际也很少用到,后面再分析。 137 | 138 | ## 2. Kotlin 可空(Nullable)类型的调用 139 | 140 | 声明一个非空变量,意味着你可以随意的调用他的方法而不用担心空指针错误,相对应的,可空变量则无法保证了。Kotlin 通过不允许可空变量直接调用方法来保证不会出现空指针错误。那么可空变量应该怎么调用呢? 141 | 142 | Kotlin 可空变量的调用方法是:**调用的"."号前加"?"或"!!"。前者的行为是,如果非空则调用,否则不调用;后者行为是,如果非空则调用,否则抛出 Illegalstateexception。**来看看例子: 143 | 144 | Nullable2.kt: 145 | 146 | ``` 147 | /** 很普通的一个类,有一个“成员变量”,一个返回该变量的方法 **/ 148 | class A { 149 | var code = 0 150 | 151 | fun getMyCode(): Int { // 返回 Int 类型,就像是 Java 的 Integer 那样 152 | return code 153 | } 154 | } 155 | 156 | fun main() { 157 | var a1 = A() 158 | a1.code = 3 159 | a1.getMyCode() // ok 160 | 161 | var a2: A? = A() 162 | a2.code = 3 // 编译错误 163 | a2.getMyCode() // 编译错误 164 | 165 | var a3: A? = A() 166 | a3?.getMyCode() // ok 167 | a3!!.getMyCode() // ok 168 | } 169 | ``` 170 | 171 | 生产环境不建议使用双叹号!!,一般只用于测试环境。使用双叹号可以理解为放弃 Kotlin 的空安全特性。 172 | 173 | ## 3. Kotlin 可空(Nullable)的传递性 174 | 175 | 如果一个可空对象调用了方法,因为这个方法有可能不被执行,那么如果我们接收它的返回值,那么返回值的类型应该是什么呢?我们继续使用上面```A```这个类,来看看这个例子: 176 | 177 | ``` 178 | /** 很普通的一个类,有一个“成员变量”,一个返回该变量的方法 **/ 179 | class A { 180 | var code = 0 181 | 182 | fun getMyCode(): Int { // 返回 Int 类型,就像是 Java 的 Integer 那样 183 | return code 184 | } 185 | } 186 | 187 | var a4: A? = null 188 | 189 | fun main() { 190 | var myCode: Int = a4?.getMyCode() // 编译错误 191 | var myCode2: Int? = a4?.getMyCode() // ok 192 | 193 | myCode2.toFloat() // 编译错误 194 | myCode2?.toFloat() // ok 195 | 196 | var myCode3: Int? = a4!!.getMyCode() // ok 197 | myCode3.toFloat() // ok 198 | } 199 | ``` 200 | 201 | 我们可以看到,本来```getMyCode()```方法返回的是 Int 类型,但由于调用时 a4 为可空类型,所以 myCode 被编译器认为是 Int? 类型。所以,**可空是具有传递性的。** 202 | 203 | 双叹号由于在变量为空时会抛出异常,所以它的返回值就还是为 Int,因为抛了异常的话,后面的代码已经不会被执行了。 204 | 205 | > 这个 a4 要写在外面的原因是,如果声明为局部变量,即使 a4 被声明为 A?,但由于局部变量的关系,编译器会把 myCode 纠正为 Int,而不是 Int?。 206 | 207 | 如果链式调用的话,就会变成这个样子: 208 | 209 | ``` 210 | myCode2?.toFloat()?.toLong()?.toByte() 211 | myCode2!!.toFloat().toLong().toByte() 212 | ``` 213 | 214 | 看起来比较 ugly。。但不用担心,Kotlin 有其他的特性来协助你处理可空变量,不用写出像这样的嘲讽代码(疯狂打问号 ???)。请继续期待后面的文章吧! 215 | 216 | 217 | ## 4. 回到场景 218 | 219 | 如果用 Kotlin 来实现场景中的代码,只需要将 mAudioPlayer 声明为可空类型就可以了: 220 | 221 | PlayerController.kt: 222 | 223 | ``` 224 | /** 225 | * 用户音频 ugc 播放器。 226 | * 如果看到奇怪的逻辑,请不要随便删除,那都是为了规避 227 | * AudioPlayer 库一些奇怪的 bug,或者是为了兼容业务做的处理。 228 | */ 229 | 230 | class PlayerController { 231 | private var mAudioPlayer: AudioPlayer? = null 232 | 233 | /** 初始化,只会初始化一次 */ 234 | fun init() { 235 | // 构造播放组件 236 | if (mAudioPlayer != null) { 237 | mAudioPlayer = AudioPlayer() 238 | } 239 | } 240 | 241 | /** 播放前需要先初始化数据 */ 242 | fun prepare(audioPath: String) { 243 | // 设置音频文件路径 244 | mAudioPlayer?.prepare(audioPath) 245 | } 246 | 247 | /** 开始播放 */ 248 | fun play() { 249 | // 前置条件判断 250 | // ... 251 | mAudioPlayer?.play() 252 | } 253 | 254 | /** 暂停 */ 255 | fun pause() { 256 | mAudioPlayer?.pause() 257 | } 258 | 259 | /** 跳转到指定时间 */ 260 | fun seekTo(time: Long) { 261 | mAudioPlayer?.seekTo(time) 262 | } 263 | 264 | fun stop() { 265 | // 数据处理 266 | // ... 267 | mAudioPlayer?.stop() // 不会空指针错误了 268 | } 269 | 270 | fun release() { 271 | mAudioPlayer?.release() 272 | mAudioPlayer = null 273 | } 274 | } 275 | ``` 276 | 277 | 写起来方便很多,而且少了一层嵌套,人也舒服了。 278 | 279 | 280 | 281 | 282 | > 空安全特性首次出现在 F#(2005) 上,此外 Swift 和 TypeScript 等也是空安全语言。 283 | > 284 | > 空指针首次出现在 Algol W(1965) 上,用作者的原话说,就是:后悔,非常的后悔。。(I call it my billion-dollar mistake) 285 | > 286 | > 参考:[https://en.wikipedia.org/wiki/Void_safety](https://en.wikipedia.org/wiki/Void_safety) 287 | 288 | 289 | -------------------------------------------------------------------------------- /4. Kotlin 变量声明和变量状态设计.md: -------------------------------------------------------------------------------- 1 | 本篇文章将会介绍如何通过正确的变量状态设计来达到简化代码逻辑的效果。 2 | 3 | 本篇并不是针对 Kotlin 的语言特性介绍,但它比语言特性更为重要。**上一篇文章讲的是空安全特性,它允许你方便的处理对象可能为空的情况。但他价值更大的另一面在于,Kotlin 可以声明不可能为空的对象**。 4 | 5 | ## 1. 非空类型 6 | 7 | 对象不可能为空意味着程序复杂度的降低。而且这不是一般的降低,因为我们开发过程很多时候都是在处理“这个变量可能为空”的情况。在 Java 的环境里,我们出于对调用的 SDK 的不信任,总是要去判断以下是否为空,以保平安,这样处理的代价就是,增加了大量的异常分支代码。如果一个变量他永远都不可能为空,那其实是一件很快乐的事!一个对象可能的状态减少了,程序逻辑会变得更简单清晰,代码的可维护性会大大的提高。**我们应该尽量将一个变量声明为非空类型。** 8 | 9 | > Java 提供了 @NonNull 和 @Nullable 注解来满足对象状态的空设计。但由于默认只会产生警告级别的提示(相信我,很多程序员不看 warning),以及使用的繁琐,它最终落得和 final 一样的使用频率。 10 | 11 | 你很可能会担心非空类型会带来内存泄漏。因为在 Java 很多释放操作都会将引用的变量设置为空,这是个很常见的防止内存泄漏的办法。但代价是将程序状态复杂化。我们确实应该慎重考虑一个变量是否可以一直被持有,但大部分情况我们是可以不用担心的。如 Android 开发基本只要考虑 Activity 是否间接被单例这样生命周期过长的对象持有即可。我还依稀记得刚学 Android 的时候,有些网上教程还会教你在 onDestroy 的时候将 onClickListner 设置为 null 防止内存泄漏。。 12 | 13 | ## 2. lateinit 14 | 15 | 说到尽量声明为非空类型,有人就会提出质疑了:非空类型说来简单,但部分依赖外部调用完成初始化的变量,无法声明为非空类型啊?Activity 的初始化,就是通过 onCreateView 回调初始化的,各种 UI 对象只能在 onCreate 回调的时候被赋值。 16 | 17 | 针对这种情况,可以使用 Kotlin 的 lateinit 关键字。lateinit 人如其名,它表示这个对象会在稍后被初始化。它还有两条限制: 18 | 19 | 1. 无法用 val 修饰,只能用 var 修饰; 20 | 2. 必须为非空类型。 21 | 22 | 1 很好理解,val 意义是声明后无法再被重新赋值,就和 final 一样。而 lateinit 变量要在稍后才被赋值,所以必须是 var。var 也意味着 lateinit 变量可以被多次赋值,可被多次赋值可能是你想要的,也有可能是你不想要的。 23 | 24 | 2 的话,设想一下,如果是可空类型,也没必要用 lateinit 了,直接初始化为 null 即可。所以 2 也是合理的。 25 | 26 | 如果一个变量被声明为 lateinit,你可以不用在声明时初始化它,在任意地方把它当作非空类型直接使用。注意了,此时如果你在初始化这个变量前就使用了该变量,则会丢出一个 RuntimeException: 27 | 28 | ``` 29 | UninitializedPropertyAccessException: lateinit property has not been initialized 30 | ``` 31 | 32 | 意思就是你还没初始化这个变量就使用它了。所以使用 lateinit 关键字,就需要你自己保证调用顺序,保证调用时变量已经被初始化,Kotlin 不再帮你把关了。这看起来像是一个把 Kotlin 空安全废掉,退化为原来 Java 的无空检查的行为。这样就很没意思了,但其实不是这样,lateinit 有他特有的表意,即:**这个变量在稍后会被初始化,且以后都不再为空。**以后不再为空即是他和可空变量的区别,**从状态复杂度来看,lateinit 变量是介于非空变量和可空变量之间的。** 33 | 34 | 使用 lateinit 是一个有风险的事情,因为非空的条件变复杂了(初始化后才是非空)。如果你不能保证所有调用都在赋值后发生,则不应使用它。但对于 Activity 的 onCreate 这种简单的场景,还是建议使用 lateinit 的。但需要注意一点: 35 | 36 | 如果 Activity 在 onCreate 的时候初始化失败了,你需要弹窗或直接 finish 的时候,此时你的 lateinit 变量可能没有被赋值,而 Activity 仍会执行 onStart onResume onDestroy 这些回调。这种情况就是“没法保证调用前变量已经初始化”的情况了。 37 | 38 | 这个时候你可以选择将变量声明为可空类型。也可以用 lateinit 变量专有的判断方法```::xxx.isInitialized```在关键路径进行判断,比如 Activity onCreate finish 掉的话,关键路径就只剩 onDestroy了(Fragmet 还有 onCreateView 和 onViewCreated)。但相比这两种办法,我更建议你思考,这样复杂的情景是不是我想要的,设计是否能够简化?因为正确设计的程序的状态应该是简单清晰的。 39 | 40 | ## 3. 空对象模式(Null Object Pattern) 41 | 42 | 其实相对于 lateinit,我更喜欢空对象这个设计模式。它没有 lateinit 引入的风险,是一种更简单的状态。**空对象就是拥有这个类默认实现的对象。**对于数据类来说,它的空对象可能所有成员变量都是0,false,长度为0的字符串;对于带方法的类来说,它的空对象可能是所有方法都是空的,可以调用但没有任何效果。这样一个空对象,它可以帮你代替 null,临时顶替正常实现,直到被重新赋值。同样是初始化异常,lateinit 可能会崩溃,而空对象最多是表现异常。 43 | 44 | > 可参考:[https://en.wikipedia.org/wiki/Null_object_pattern](https://en.wikipedia.org/wiki/Null_object_pattern) 45 | 46 | ## 4. final 47 | 48 | 除了 Kotlin 的**非空类型**/可空类型,**val**/var(即 Java 的 final 关键字)也是减少变量状态的利器。而且它比非空类型更彻底,非空类型只是不允许这个变量变为 null,val 直接不允许变量重新被赋值!声明为 val 的变量状态可能性更少,并发竞争的问题都没有了。 49 | 50 | ## 变量状态设计原则 51 | 52 | 经过上面的变量状态介绍,我们按照变量状态从简单到复杂的顺序,可以得到一个变量状态声明的优先级: 53 | 54 | 1. 声明为 val 变量,无法满足再考虑 var 55 | 2. 声明为非空变量 56 | 3. 无法满足声明时赋值,优先考虑赋值为空对象 57 | 4. 无法满足空对象,看看是否可以用 lateinit 58 | 5. 声明为可空变量 59 | 60 | 61 | -------------------------------------------------------------------------------- /5. Kotlin 函数声明与闭包(Closure).md: -------------------------------------------------------------------------------- 1 | 今天介绍闭包。闭包也不是新东西了。其实 Kotlin 就基本没有新东西,不,是新语言都基本没有新东西。新语言都是把近些年好用的特性组装起来,再加点自己的见解,因地制宜 2 | 一下。 3 | 4 | ### 0. 闭包概念介绍 5 | 6 | 闭包我第一次接触是在 JavaScript 上,函数当作“一等公民”的编程语言都有这个概念。**函数是“一等公民”的意思是,函数和变量一样,它是某种类型的实例,可以被赋值,可以被引用。**当然函数还可以被调用。变量类型是某个声明的类,函数类型就是规定了入参个数,类型和返回值类型(不规定名字。函数名就和变量名一样,随便起)。如我要声明 Kotlin 一个函数类型,它的入参是两个整数,出参是一个整数,那应该这样写:```val add: (Int, Int) -> Int```。箭头左边括号内表示入参,括号不可省略。箭头右边表示返回值。 7 | 8 | wiki 上闭包的定义是:**引用了自由变量的函数**,这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。从定义来说,对闭包的理解,是基于普通函数之上的。一般的函数,能处理的只有入参和全局变量,然后返回一个结果。闭包比普通函数更多一点能力,**它还捕获了当前环境的局部变量**。当然了,捕获局部变量的前提是可以在局部环境里声明一个函数,这只有把函数当作“一等公民”才可以做到。 9 | 10 | ### 1. 闭包与匿名类比较 11 | 12 | 在函数不是“一等公民”的 Java 老大哥这里,匿名类其实就是代替闭包而存在的。只不过 Java 严格要求所有函数都需要在类里面,所以巧妙的把“声明一个函数”这样的行为变成了“声明一个接口”或“重写一个方法”。匿名类也可以捕获当前环境的 final 局部变量。但和闭包不一样的是,匿名类无法修改捕获的局部变量(final 不可修改)。 13 | 14 | 而匿名类能引用 final 的局部变量,是因为在编译阶段,会把该局部变量作为匿名类的构造参数传入。因为匿名类修改的变量不是真正的局部变量,而是自己的构造参数,外部局部变量并没有被修改。所以 Java 编译器不允许匿名类引用非 final 变量。 15 | 16 | > Java8 lambda 是进一步接近闭包的特性,lambda 的 JVM 实现是类似函数指针的东西。但注意: Java7 的 lambda 语法糖兼容不是真正的 lambda,它只是简化了匿名类的书写。同样的 lambda 也只能引用 final 变量。 17 | 18 | 19 | ### 2. 闭包使用 20 | 21 | 我们来看一个闭包的例子: 22 | 23 | ``` 24 | fun returnFun(): () -> Int { 25 | var count = 0 26 | return { count++ } 27 | } 28 | 29 | fun main() { 30 | val function = returnFun() 31 | val function2 = returnFun() 32 | println(function()) // 0 33 | println(function()) // 1 34 | println(function()) // 2 35 | 36 | println(function2()) // 0 37 | println(function2()) // 1 38 | println(function2()) // 2 39 | } 40 | ``` 41 | 42 | 分析上面的代码,```returnFun```返回了一个函数,这个函数没有入参,返回值是```Int```。我们可以用变量接收它,还可以调用它。```function```和```function2```分别是我创建的两个函数实例。 43 | 44 | 可以看到,我每调用一次```function()```,```count```都会加一,说明```count``` 被```function```持有了而且可以被修改。而```function2```和```function```的```count```是独立的,不是共享的。 45 | 46 | 而我们通过 jadx 反编译可以看到: 47 | 48 | ``` 49 | public final class ClosureKt { 50 | @NotNull 51 | public static final Function0 returnFun() { 52 | IntRef intRef = new IntRef(); 53 | intRef.element = 0; 54 | return (Function0) new 1<>(intRef); 55 | } 56 | 57 | public static final void main() { 58 | Function0 function = returnFun(); 59 | Function0 function2 = returnFun(); 60 | System.out.println(((Number) function.invoke()).intValue()); 61 | System.out.println(((Number) function.invoke()).intValue()); 62 | System.out.println(((Number) function2.invoke()).intValue()); 63 | System.out.println(((Number) function2.invoke()).intValue()); 64 | } 65 | } 66 | ``` 67 | 68 | 被闭包引用的 int 局部变量,会被封装成 IntRef 这个类。这个 IntRef 里面保存着 int 变量,原函数和闭包都可以通过 intRef 来读写 int 变量。Kotlin 正是通过这种办法使得局部变量可修改。除了 IntRef,还有 LongRef,FloatRef 等,如果是非基础类型,就统一用 ObjectRef 即可。Ref 家族源码:[https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/jvm/runtime/kotlin/jvm/internal/Ref.java](https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/jvm/runtime/kotlin/jvm/internal/Ref.java) 69 | 70 | > 在 Java 中,我们如果想要匿名类也可以操作外部变量,一般做法是把这个变量放入一个 final 数组中。这和 Kotlin 的做法本质上是一样的,即通过持有该变量的引用来使得两个类可以修改同一个变量。 71 | 72 | ### 3. 总结 73 | 74 | 根据上面分析,我们可以了解到: 75 | 76 | * 闭包不是新东西,是把函数作为“一等公民”的编程语言的特性; 77 | * 匿名类是 Java 世界里的闭包,但有局限性,即只能读 final 变量,不能写任何变量; 78 | * Kotlin 的闭包可以捕获上下文的局部变量,并修改它。实现办法是 Kotlin 编译器给引用的局部变量封装了一层引用。 79 | -------------------------------------------------------------------------------- /6. Kotlin 变量声明与基本类型(Primitive Type).md: -------------------------------------------------------------------------------- 1 | 本文将会介绍 Java 的基本类型和 Kotlin 的区别。我们知道,Java 的基本类型是 boolean, char, short, int, long, float, double。这些基本类型不是对象,只可以进行基本的数学逻辑运算。Java 虽然打着“一切皆对象”的口号,但在基本类型还是留了一手。**他们是特别的存在。** 2 | 3 | 至于为什么要保留基本类型,真相只有一个:性能。大部分基本类型操作是一条指令就可以完成的,而对象方法调用则需要很多条指令才能完成;另外占用内存相比对象,也小很多。可以说 Java 诞生初期,在概念统一和性能的权衡下,把天平偏向了性能。这也很合理:90 年代时候的硬件速度还非常慢。且 Java 最初是为嵌入式设备而设计的,后面才把目标改为互联网。 4 | 5 | > 现在市面上大部分的银行卡,里面装的是 Java 虚拟机,开发者通过编写受限的 Java 代码来实现一个叫 Applet 的应用单元,并装载到银行卡中。银行卡被插入后,机器会通过针脚或 NFC 和银行卡进行通讯。这种技术叫 Java Card 技术。 6 | > 所谓受限的 Java 代码,没有 String,没有 JDK,甚至大部分连 int 都不支持。只能用 byte 和 short。因为芯片是 16 位的。 7 | > 我上一份工作,在银行卡上实现了三种数字货币的交易协议。。 8 | 9 | 我们不妨把 Java 的面向对象称为不完全面向对象。那么是否有“真·面向对象”语言?有的。如 Smalltalk,Python,Kotlin 就是。在他们的编程环境里,没有基本类型,是真正的“一切皆对象”。这样带来的好处是概念的统一。“基本类型”这样的概念不再被需要,不再需要特别的处理它,所有声明出来的变量都具有同样的行为,不再需要区分引用类型和值类型。说到引用类型和值类型,大家在初学 Java 的时候应该都花了不少功夫去理解吧? 10 | 11 | 当然了,Java 也有基本类型对应的对象封装。如 int 对应 Integer,float 对应 Float,并且 jdk1.5 之后提供了自动装箱拆箱的编译器特性。但因为写起来比基本类型麻烦,且考虑性能问题,导致如果不是限定场景,大家都不会主动用它们。 12 | 13 | 而 Kotlin 为了提供完全面向对象的特性,摒弃了基本类型。但 Kotlin 没有直接使用 Java 的 java.lang.Integer,java.lang.Float 装箱类,而是另起山头,创造了 kotlin.Int,kotlin.Float 等类,~~因为别人写的代码都是 shit,~~因为 Java 的装箱类是集成在 JDK 的,无法随着 Kotlin 版本更新而更新。且在 Kotlin 中,数值类还有拥有额外的编译特性: 14 | 15 | 前面说到 Java 因为性能问题,保留了基本类型。那么 Kotlin 选择了完全面向对象,那理应要承受一定的性能损失。但其实 Kotlin 在编译成 jvm 字节码的时候,大部分的 Int 都会编译回 int,小部分会被编译成 Integer。这个小部分,典型的情况就是你声明一个变量为可空类型时,即声明为 Int?,这个时候无法使用 jvm 的基本类型结构。 16 | 17 | 而我们观察 kotlin.Int 时,可以看到除了数学运算的运算符重载方法,和强转的方法(toFloat,toLong 等)外,就没有其他方法了,而这些方法都可以直接对应基本类型运算的操作。kotlin.Int 声明为这样一个简洁的数值封装类,让转换为 jvm 字节码的基本类型铺平道路。 18 | 19 | **所以使用 kotlin 的数值类型时,绝大部分场景下,不会有额外的性能开销。** 20 | -------------------------------------------------------------------------------- /7. Kotlin 变量声明和属性(property).md: -------------------------------------------------------------------------------- 1 | ## 1. Java 的成员变量和它们的 get/set 方法 2 | 3 | 在 Java 中,我们把在类中声明的变量,称为为成员变量(field),函数中声明的变量称为局部变量。在经典的 Java 设计理念中,成员变量是不建议暴露的,而当你想访问修改成员变量时,应声明其对应的 get/set 方法。**因为成员变量没有办法继承重写 4 | ,无法声明为接口,get/set 权限无法分开控制等。使用 get/set 方法代替直接修改成员变量,更符合面向对象设计。** 因此 get/set 方法在 Java 大地上遍地开花,无处不在。所以我们经常能看到这样的代码: 5 | 6 | ``` 7 | public class StringEntity { 8 | 9 | private String resId; 10 | private String value; 11 | 12 | public StringEntity() { 13 | } 14 | 15 | public StringEntity(String resId, String value) { 16 | this.resId = resId; 17 | this.value = value; 18 | } 19 | public String getResId() { 20 | return resId; 21 | } 22 | public void setResId(String resId) { 23 | this.resId = resId; 24 | } 25 | public String getValue() { 26 | return value; 27 | } 28 | public void setValue(String value) { 29 | this.value = value; 30 | } 31 | } 32 | ``` 33 | 34 | 噼里啪啦的写了一大堆代码,但功能却极其简单:StringEntity 包含了 resId 和 value 两个 String 的**属性**,你可以读取或修改它。虽说现在 IDE 都可以帮你快速的生成这些代码,但无法摆脱代码的信息密度低,可读性差的缺点。那么有没有什么语言能够更精简的表达 get/set 的语义呢?有的。 35 | 36 | > 更详细的 get/set 与 field 的比较,参考:[https://stackoverflow.com/questions/1568091/why-use-getters-and-setters-accessors](https://stackoverflow.com/questions/1568091/why-use-getters-and-setters-accessors) 37 | 38 | ## 2. 引入属性 property 的概念 39 | 40 | 最开始我是在 objective-C 上了解到 property 的概念。属性 property 和成员变量 field 的声明和使用方法都没有什么区别,但**property 允许你自定义它的 get/set 方法**。如果你不自定义 property 的 get/set 方法,那它就和一个普通的变量没什么区别;而如果你自定义了 get/set 方法,在你读取/修改 property 时,实际上是调用了 property 的 get/set 方法。简单来说,**属性 property=成员变量 field + get/set 方法,且 get/set 方法拥有默认实现。** 41 | 42 | property 的优点也很明显: 43 | 44 | 1. 你可以用更简洁的方式实现 get/set 方法; 45 | 2. field 和 get/set 方法统一后,代码的内聚性更高了,不会出现 field 在文件头,get/set 方法在文件尾的情况; 46 | 3. 在 Java 类内部调用中,你既可以调用 field,也可以调用 get/set 方法,这种情况下内部调用是不统一的,当 get/set 方法添加了更多的行为时,原本直接调用 field 的内部代码可能会出错。property 统一了入口,避免了这种问题。 47 | 48 | ## 3. Kotlin 的 property 使用 49 | 50 | 在你不知道 property 的概念时,你就像声明一个局部变量一样声明 property 即可。 51 | 52 | 当你希望自定义 property 的 get/set 方法时,就为它增加 get/set 方法,按特定的语法结构来声明。举个例子: 53 | 54 | ``` 55 | class DataList { 56 | var dataList: List = listOf() 57 | private set // 1 58 | var size: Int // 2 59 | get() = dataList.size // 3 60 | set(value) { // 4 61 | dataList = ArrayList(value) 62 | } 63 | var name: String = "DataList" 64 | set(value) { 65 | field = value // 5 66 | println("new name: $field") 67 | } 68 | } 69 | ``` 70 | 71 | 这段代码列举了几种常用的 property 用法,简要说明: 72 | 73 | 1. 当你只需要修改 property 的 get/set 访问权限时,不需要定义方法体; 74 | 2. 如果定义了 get 方法且类型可以推断时,类型是可以省略的 75 | 3. get 方法的声明方式,是一个无参的函数(= 号用法适用于代码只有一行的情况,用大括号也可以) 76 | 4. set 方法的声明方式,是一个只有一个参数的函数,入参名字可随意发挥。 77 | 5. 调用 property 的 field 的方法。前面说到 property = field + get/set 就是这个意思。 78 | 79 | > Kotlin properties 介绍:[https://kotlinlang.org/docs/reference/properties.html](https://kotlinlang.org/docs/reference/properties.html) 80 | 81 | ## 4. Kotlin property 的 JVM 字节码体现 82 | 83 | Kotlin property 被编译成字节码后,通过反编译我们可以看到,一个 property 会生成一个同名的 field,以及驼峰命名的 get/set 方法。所以 Java 想要调用 Kotlin 的 property 时,直接调用 get/set 方法即可。而 Kotlin 调用 Kotlin 的property,则没有 Java 那么麻烦,就像局部变量一样调用即可。 84 | -------------------------------------------------------------------------------- /8. Kotlin 函数声明与默认参数(Default argument).md: -------------------------------------------------------------------------------- 1 | ## 1. Java 的函数重载和烦恼 2 | 3 | 在 Java 中,当我们要实现同一种功能,但函数入参出参不一样的函数的时候,我们可以用到 Java 的函数重载功能。在 Android framework 中同样也存在大量的重载函数,以方便开发者调用。重载函数深入人心,得到大家的认可。 4 | 5 | 东西确实是好东西,但当重载函数过多的时候,代码就显得臃肿了,比如这里有一个 Toast 显示的工具类,在经过不断的功能扩展后,发展成一个拥有海量重载方法的类: 6 | 7 | ``` 8 | // ... 9 | 10 | public static void show(CharSequence msg) { 11 | show(Toast.LENGTH_SHORT, msg, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 12 | } 13 | 14 | public static void showLong(CharSequence msg) { 15 | show(Toast.LENGTH_LONG, msg, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 16 | } 17 | 18 | public static void show(Activity activity, int resId) { 19 | show(activity, resId, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 20 | } 21 | 22 | public static void show(Activity activity, CharSequence msg) { 23 | show(Toast.LENGTH_SHORT, activity, msg, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 24 | } 25 | 26 | /** 27 | * 当msg0不为空时展示msg0,忽略msg1;否则显示msg1 28 | */ 29 | public static void show(Activity activity, CharSequence msg0, CharSequence msg1) { 30 | if (!TextUtils.isEmpty(msg0)) { 31 | show(Toast.LENGTH_SHORT, activity, msg0, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 32 | } else if (!TextUtils.isEmpty(msg1)) { 33 | show(Toast.LENGTH_SHORT, activity, msg1, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 34 | } 35 | } 36 | 37 | public static void show(Activity activity, int resId, int gravity) { 38 | show(Toast.LENGTH_SHORT, activity, resId == 0 ? null : getString(resId), gravity); 39 | } 40 | 41 | public static void show(int duration, Activity activity, int resId) { 42 | show(duration, activity, resId == 0 ? null : getString(resId), Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 43 | } 44 | 45 | public static void show(int duration, Activity activity, CharSequence msg) { 46 | show(duration, activity, msg, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 47 | } 48 | 49 | public static void show(final int duration, Activity activity, final CharSequence msg, final int gravity) { 50 | // 具体实现 51 | } 52 | 53 | // ... 54 | ``` 55 | 56 | 和 get/set 方法一样,这是典型的信息密度低的代码。那么有什么办法能够更精简的表达**同样的功能,不同的入参**的特性呢?有的,就是**默认参数**特性。 57 | 58 | ## 2. 重载函数的替代者,默认参数 59 | 60 | Kotlin 拥有默认参数的特性,如果用 Kotlin 实现上述 Java 代码,可以简化为: 61 | 62 | ``` 63 | fun show(msg: CharSequence, 64 | msg2: CharSequence? = null, 65 | context: Context = Global.getApplicationContext(), 66 | gravity: Int = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, 67 | duration: Int = Toast.LENGTH_SHORT) { 68 | // 具体实现 69 | } 70 | ``` 71 | 72 | 我们看到,声明默认参数的方法很简单,只需要把可以使用默认参数的入参用“=“号给他赋值一个默认值即可。一般来说,我们会把必须提供的参数写在前面,有默认参数的入参排在后面。因为你在调用的时候,函数的入参默认是按顺序映射的,按上面的顺序排列的话,你只需要填完必须的参数即可;而如果还想提供可选参数,就继续按顺序填写。 73 | 74 | 那如果我只想提供部分可选参数,比如上面的```show```函数我只想提供```duration```参数,跳过其他可选参数呢?Kotlin 提供了这样的调用办法: 75 | 76 | ``` 77 | show("this is a toast"); 78 | show("this is a toast, duration = Toast.LENGTH_LONG); 79 | show(msg = "this is a toast, duration = Toast.LENGTH_LONG); 80 | show(duration = Toast.LENGTH_LONG, msg = "this is a toast); 81 | ``` 82 | 83 | 我们发现,**Kotlin 方法调用时,可以显式的指明对象和入参的映射关系,无需按顺序传递**。注意,这个特性不分必须参数和可选参数,所有的参数都可以用这种形式指定映射。 84 | 85 | 但一般来说,我们只在可选参数时用到。还有一种应用场景是,当你觉得必须参数的值让人迷惑,想显式的告诉阅读者这个值所对应的入参时。 86 | 87 | > 大家可能已经发现,很早以前,Android Studio 对没有提供名字的函数参数,已经默认显示这个参数对应的名字。 88 | 89 | > Flutter 的 Dart 语言也有默认参数特性,而且 Flutter 组件对默认参数的使用可谓是淋漓尽致。它会把一个控件所有可配置的参数都提供在构造函数中,而且把必须参数和可选参数分开。这样开发者可以很方便的看到它必须配置和可以配置的所有参数,不用再去慢慢找这个控件提供了什么设置方法。 90 | 91 | ## 3. 默认参数和函数重载对比 92 | 93 | 默认参数和重载函数对比,重载函数可以改变入参和出参(返回值),默认参数只可以改变入参。不过改变出参的场景实在很少,一般我们都会用不同的函数名来区分不同的返回值,比如 ```covertToFloat```,```covertToInt```。 94 | 95 | 其次,每一个重载函数都是一个方法,会记录在方法表,占用 Dex 的最大方法数。默认参数会生成 2 个方法,一个是全参数的方法 A,另一个方法 B 也是全参数,但比全参数方法多出来了 flag 参数,该 flag 参数用来记录哪些参数是可选参数。外部调用的时候调用的是方法 B,没有指定的可选参数将会被赋值 0 或 false 或 null,指定了的可选参数会被赋值,且对应个 flag 位会被标记。到了方法 B 内部,没有被 flag 标记的参数,会被设置为默认值,最后方法 B 调用 方法 A。**Kotlin 通过这种方式,减少了重载函数可能带来过多的方法数。** 96 | 97 | > Kotlin 也支持函数重载。 98 | 99 | ## 4. 函数声明的特性发展 100 | 101 | 如果是一开始接触的都是高级语言的同学,可能会觉得函数重载是个比较奇怪的特性:为什么这也算是一种特性?他们除了方法名是一样的,入参不一样,出参不一样,为什么要单独拿出来说呢?这是因为在 C 语言时代,方法的识别只通过函数名,所以无法做到函数重载。后来大家感受到同名函数不同参数存在的必要性,像 C++,就把方法的入参和出参都写到了符号表里。Java 的方法签名,也是包含入参和出参的。这样的语言,就具备识别重载函数的能力,所以函数重载就成为了一种新特性。 102 | 103 | 但函数重载,是一个个不同的函数,只是名字一致而已。在语义精简和代码规范有一定的缺陷。语义精简就是“更少的代码表达相同的意图”;而代码规范,因为函数重载的功能基本是相同的,更推荐的做法是函数重载只有一份实现代码,其他函数重载都补全参数,然后调用这个完整的实现代码,就像开头的 Toast 工具类代码一样。但实际上由于缺少实际约束,有些开发者会复制多份实现,填入到不同的重载函数中。可以说函数重载容易写出,“smelly”的代码。 104 | 105 | 而默认参数特性,避免了函数重载的语义精简和代码规范缺陷。 106 | -------------------------------------------------------------------------------- /9. Kotlin 函数声明和扩展(extension).md: -------------------------------------------------------------------------------- 1 | ## 1. Java 的老朋友 Utils 工具类 2 | 3 | Utils 工具类是无构造参数的 static 方法集合,用于扩展某个对象的功能,如 MathUtils,ToastUtils,FIleUtils,StringUtils, LogUtils。Utils 类在一定程度上减少了重复代码的问题,它是成本最低的 DRY(Don't repeat yourself)实践。 4 | 5 | Utils 工具类实在太常见了,以至于很多开发者都不曾质疑他的合理性。但 Utils 实际上是反 OOP (面向对象模式)的妥协产物。我们从代码设计的角度看,Utils 方法是 static 的,没有 OOP 的继承,重写,抽象的特性(static 本身就是反 OOP 的)。且 Utils 违反了单一职责,一个类应该包含其属性和所有操作方法。而 Utils 实现的方法并不在这个类内。 6 | 7 | 而从使用者的角度,**使用者必须预先知道这个 Utils 工具类的存在,他能使用为这个类添加的扩展方法。** 在实际项目实践中,这个条件往往是缺失的,因为在团队开发中,个人无法掌握所有代码,因为不知道这个代码已经有人实现过了,导致大家都实现了自己的 Utils。一个工程里同一个类的 Utils 往往会有好几个。 8 | 9 | 但存在必然是合理的。我自己就是一个写 Utils 的老司机。从个人角度来看,让我使用 Utils 而不是对象继承的原因,主要是因为: 10 | 11 | 1. 无法继承/重写这些类及其方法,只能通过 Utils 扩展; 12 | 2. 继承一个类比抽取代码块封装为函数的实现成本+替换成本高; 13 | 3. Utils 绝大情况下只是用来存储代码块,需求非常稳定,无需面向对象。 14 | 15 | 依赖的类是 SDK 提供的时候 Utils 往往是不可避免的。且使用 Utils 的场景里很少会用到面向对象的特性,那么没有面向对象的缺点也并没有那么严重了。那么抛开 Utils 的设计缺点,我们是否可以避免使用上的缺点?Kotlin 提供的解决方法就是扩展(extension)。 16 | 17 | ## 2. Kotlin 扩展的使用和实现分析 18 | 19 | 声明一个 Kotlin 扩展如下: 20 | 21 | ``` 22 | // StringUtils.kt 23 | fun String.appendHaha(): String { 24 | return this + "haha" 25 | } 26 | ``` 27 | 28 | 它与普通的方法声明很接近,只是方法名前多了一个类名,来表示其归属的类。扩展声明为顶层声明的时候可以被外部调用(是的,因为函数是一等公民,在方法内部也可以声明扩展方法)。 29 | 30 | 在函数体内用 this 来引用调用的实例,属性和方法的访问权限与普通调用一致。一致的意思是和你正常在其他方法内部调用的权限一致,并不会因为是扩展声明就可以访问 private/propect 权限的属性和方法。这是因为扩展声明在字节码层面上其实是 static 方法。下面是```appendHaha``` 对应 jvm 字节码的反编译结果: 31 | 32 | ``` 33 | public class StringUtilsKt { 34 | @NotNull 35 | public static final String haha(@NotNull String $this$haha) { 36 | Intrinsics.checkParameterIsNotNull($this$haha, "$this$haha"); 37 | return $this$haha + "haha"; 38 | } 39 | } 40 | ```` 41 | 42 | 所以从 Java 的角度来看,Kotlin 的扩展方法和 Utils 的调用没有差别,都是调用类的 static 方法然后传入操作的参数。实际上 Java 想要调用 Kotlin 的扩展方法也确实是这样调用的。 43 | 44 | **扩展方法的调用和实例方法调用一致,在调用者角度没有区别。** Android Studio 会自动提示对应类所有的扩展方法,且扩展方法的颜色(黄色)会和普通实例方法(白色)区分开来。 45 | 46 | > Kotlin 的扩展特性和 objective-C 的 category 特性功能非常相似,都是为一个现有的类添加方法(且只能添加方法),只是代码组织结构上有些许差异。但 objective-C 的 category 特性是 runtime 特性,Kotlin 扩展的实现更接近语法糖。 47 | 48 | ## 3. 总结 49 | 50 | Kotlin 扩展依然没有解决 Utils 类的设计缺点。就像 Kotlin companion object 对 Java static,Kotlin Int 对 Java int,Kotlin property 对 Java field 一样,Kotlin 扩展是 Kotlin 对 Java 不完全面向对象的“清理”,使 Kotlin 更接近完全面向对象。**相比 Utils 工具类,Kotlin 扩展特性能有效减少开发者心智负担和沟通成本,从而提高开发效率。** 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin 从拒绝到真香 2 | 3 | 《Kotlin 从拒绝到真香》是一系列介绍 Kotlin 好用的特性的文章。文章会以某一个经典的 Java 使用场景开始,提供相应的 Kotlin 实现代码,并讲解其中用到的特性。 4 | 5 | 文章主要面向 Android 开发者。如果你正在考虑是否使用 Kotlin 来代替 Java 作为 Android 开发语言,这些文章会非常适合你。笔者所在团队正积极使用 Kotlin,并有计划的使用 Kotlin 重构项目代码。但对团队成员来说,使用 Kotlin 并不是强制的。笔者团队内推广 Kotlin 的过程中也遇到了部分同事“拒绝”Kotlin,而这些文章正是我在推广和不断尝试说服他们的过程中逐渐积累起来的“真香”部分。 6 | 7 | 文章不求鞭辟入里,但尽可能保证有趣不枯燥。如果恰好你也喜欢这些文章,请务必 star 它! 8 | 9 | watch 本仓库,可直接接收文章更新。 10 | 11 | > 作为 Android 开发者的你肯定已经拥有了 Android Studio,但我还是建议你下载一个 JetBains CE 或其他版本,这样你可以方便的创建一个 Kotlin 文件并运行它。大部分例子中,我们只需要 kotlin 的环境就可以了。 12 | > 13 | > [https://play.kotlinlang.org](https://play.kotlinlang.org) 是一个在线 Kotlin playground,使用它可以方便快速的运行一小段代码。 14 | 15 | --------------------------------------------------------------------------------