Kotlin 系列文章详细计划-02-视频脚本

这是码上开学 Kotlin 系列第 2 集的视频脚本。

视频脚本不是文章结构要求,所以不属于「必读」;但建议参与文章编写的作者看一下视频脚本再去看「文章结构要求」和写文章,这样写出来的文章和视频会更容易打配合,更容易比较「搭」。

如果你想用视频脚本直接作为基准来扩展成文章,也没问题,写得好、容易读是唯一标准。

标题:Kotlin 里那些「不是那么写的」

脚本:

开场白

大家好,我是扔物线朱凯。上期我们讲了 Kotlin 上手最基础的三个点:变量、函数和类型。大家都听说过,Kotlin 完全兼容 Java,这个意思是用 Java 写出来的 class 和 Kotlin 可以完美交互,而不是说你用 Java 的写法去写 Kotlin 也完全没问题,这个是不行的。这期内容我们就讲一下,Kotlin 里那些「不 Java」的写法。

Constructor

首先,Kotlin 的构造函数的写法和 Java 就不一样。

Kotlin 的构造函数的命名规则不是像 Java 的构造方法那样,和类同名,而是所有构造函数统一直接叫 constructor 就好:

  1. public class User {
  2. int id;
  3. String name;
  4. public User(int newId, String newName) {
  5. id = newId;
  6. name = newName;
  7. }
  8. }
  1. class User {
  2. val id: Int
  3. val name: String
  4. constructor(newId: Int, newName: String) {
  5. id = newId
  6. name = newName
  7. }
  8. }

还有,Java 里常常配合构造方法一起使用的 init 代码块,在 Kotlin 里的写法也有了一点点改变:你需要给它加一个 init 前缀。

「啊哈?Java 有这个功能吗?」这个我就不讲了哈。

final

Java 里面有个字叫 final,当它用来修饰变量的时候,表示这个变量不能被修改,或者说它的值只能被赋值一次:

  1. final int final1 = 1;
  2. void method(final String final2) {
  3. System.out.println(final2);
  4. final Date final3 = new Date();
  5. System.out.println(final3);
  6. }

这种不能修改的变量,在 Kotlin 里面不是用 final 来额外修饰,而是直接把 var 换掉,改成 val

  1. val final1 = 1;

上一期说过,var 是 variable 的缩写, val 是 value 的缩写。

其实我们写 Java 代码的时候,很少会有人用 final 的对吧?但是我从 final 开始说起,是因为它是一个很好的切入点,来让我们去看一看 Kotlin 里那些「不 Java」的写法。所以稍后几分钟的内容,也都是从 final 来展开的。

我们继续说。final 用来修饰变量其实是很有用的,但大家都不用;可你如果去看看国内国外的人写的 Kotlin 代码,你会发现很多人的代码里都会有一堆的 val。而且这些写 val 的人,就是当初那些不写 final 的人。为什么?因为 final 写起来比 val 麻烦一点:我需要多写一个单词。虽然只麻烦这一点点,但就导致很多人不写。

这就是一件很有意思的事:从 finalval,只是方便了一点点,但却让它们的使用频率有了巨大的改变。这种改变是会影响到代码质量的:在该加限制的地方加上限制,就可以减少代码出错的概率。

val 自定义 getter

不过 valfinal 也有一个小小的区别,就是虽然 val 修饰的变量不能被二次赋值,但你可以通过自定义变量的 getter 来让变量的实际取值是动态的:

  1. val size: Int
  2. get() {
  3. return items.size
  4. }

不过这个属于特殊用法,val 的定位确实是对应的 Java 的 final,只不过功能顺便有了这么点小扩展。

static property / function

先引出 companion object

另外,刚才说到大家都不喜欢写 final 对吧?但有一种场景,大家是最喜欢用 final 的:常量。

  1. public static final String WEBSITE_NAME = "kaixue.io";

在 Java 里面写常量,我们用的是 static + final。而在 Kotlin 里面,除了 final 的写法不一样,static 的写法些也不一样,而且是更不一样。确切地说:在 Kotlin 里,静态变量和静态方法这两个概念被去除了。

不过你如果上网搜,还是能看到 Kotlin 里静态变量和方法的等价写法:使用一个叫做 companion object 的东西:

  1. companion object {
  2. val WEBSITE_NAME = "kaixue.io"
  3. }

(愣住)

image-20190707174036403

很多人第一次看见 companion object 这个东西,都会觉得「很麻烦」,觉得 Kotlin 作为一个新语言,为什么会有这么麻烦的东西。

然后讲讲 object

这个我先不谈,我先给你来讲讲 Kotlin 里的 object 是个什么东西。

Kotlin 里的 object ——首字母小写的啊,不是大写,Java 里的 Object 在 Kotlin 里不用了,不过这是另一个话题——另外,Kotlin 里的 object 也不是一个类,而是一个修饰词。它最基本的用法是去修饰一个类,替换掉 class 关键字:

  1. object Kotlin {
  2. ...
  3. }

它的意思很直接:创建一个类,并且创建一个这个类的对象。这个就是 object 的意思,对象。

另外,你在程序的其他地方要使用这个对象,可以直接用类名来访问:

  1. println(Kotlin.name)

这是什么?这不就是单例吗?对吧。所以在 Kotlin 里要创建单例,不用 Java 那一套复杂的操作(给个图),只要把 class 换成 object 就行了。另外你看,这个单例对象,它的内部字段和方法的使用方式,是不是特别像静态字段和静态方法?

所以其实 object 也是 Java 的 static 的替代品之一。只是不像 Java 那样,你要给每个字段和方法都加上 static,Kotlin 的 object 是不用加的;不过其实你也没有选择:当你给一个类用了 object 关键字,这个类的所有方法全都相当于静态方法,因为它本质上是个单例对象嘛,所以你没得选,全都是直接用类名访问的。

另外 object 也可以用来创建匿名类对象,不过视频里就不讲了(但给个图)。

我们继续说。object 可以用于写单例,也可以用于写静态的变量和函数对吧?但你如果希望某个 class 不是单例,又要有自己的静态变量和函数,用 object 就不行了。不过你可以在这个 class 的内部写一个 object

  1. class A {
  2. object B {
  3. var c
  4. }
  5. }

这样,你就可以把这个内部 object 的属性和函数当做是外部 class 的静态属性和函数来用了:

  1. A.B.c

再拐回来讲 companion object

另外,基于 object 的这种用法, Kotlin 提供了一个更方便的写法:在 object 的左边写上 companion,就可以省略掉 object 的名字:

  1. class A {
  2. companion object {
  3. var c
  4. }
  5. }
  1. A.c

这个就是 companion object 的作用:省事。从名字来看,它是陪伴着这个类共同前行的一个对象,而实际上它的作用就是让这个类有一个没名字的 object,简化它的写法。

另外通过这种写法,你在一个 class 内部创建一个没名字的 object,这样在使用的时候,就可以通过 class 的类名来访问内部 object 的属性和函数,这种用法就跟 Java 的静态变量和静态方法是一样的了。这就是 Java 的静态变量和方法在 Kotlin 中的「等价写法」。

最后讲 top-level declarations

不!

过!

这不是 Kotlin 推荐的做法。在 Kotlin 里有个更简便的东西,叫 top-level declaration,「顶-层-声明」。其实就是把属性和函数的声明不写在 class 里面,这个在 Kotlin 里是允许的:

  1. package com.hencoder.plus
  2. fun topLevelFuncion() {
  3. }

这样写的属性和函数,不隶属于任何 class,而是直接隶属于 package 。它和静态变量、静态方法一样是全局的,但用起来更方便:你在其它地方用的时候,就连类名都不用写:

  1. import com.hencoder.plus.topLevelFunction
  2. topLevelFunction()

这也就是所谓的「top-level」。它除了「方便」,对于写惯了 Java 的程序员来说,还会有一种很强的不安:

「这种游离于 class 之外的变量和函数,会不会有什么问题?」

其实乍一想,觉得变量和方法如果脱离于 class,不免有一种写多了之后会堆成山一团糟的预感。实际上真用起来,真的还好。而且由于暴露在了顶级,这些方法在使用的时候可以直接被 Android Studio 的代码提示显示出来,所以一个人写的方法,别人就算不知道,但试一下也能试出来。这可不是我强行想出的优势,大家要知道,码上开学只负责讲解 Kotlin ,不负责安利 Kotlin。很多人应该都有这样的经验:在 Java 项目里,你写了一个工具方法,同事却从来不知道,以及同样的工具方法被多个人在同一个项目里写多遍的问题,是非常常见的。而你如果在 Kotlin 里把工具方法写成了 top-level 的,只要方法命名恰当,别人在要用某种功能之前,只靠试就能把这个方法试出来,这个是真的很好用的。

文章写一下:top-level 函数重名。

实际上你看一下,Java 的 System.out.println() 在 Kotlin 里只要写一个 println() 就行了,这个 println() 就是 Kotlin 额外添加的一个 top-level function。它的内部其实还是调用的 System.out.println()

  1. public actual inline fun println(message: Any?) {
  2. System.out.println(message)
  3. }

对比

那么在实际使用的时候,应该用哪种呢?是用 top-level 还是用 companion object 或者 object

其实一般来说,如果你想写工具类,那直接创建一个文件,里面全都写成 top-level functions 就行了;而像 TAG 这种只是针对类的而不是针对外部使用者的属性或者方法,就可以写进 companion object 或者 object;另外, companion objectobject 是可以有父类和接口的,所以你利用这点可以对你的全局函数进行一些功能扩展和延伸,具体怎么延伸,用的时候慢慢就懂了。

所以简单的判断原则是:能写在 top-level 就写在 top-level,而 companion object 或者 object,看情况按需使用。

常量

在 Java 里面,我们写常量用的是 static final,而在 Kotlin 里面,有一个专门的关键字用来写常量:const

  1. const val PI = 3.14

不过它是针对的「编译期常量」,compile-time constant。

其实「编译期常量」是 Java 里面就有的概念了,它的意思是「编译器在编译的时候就知道这个东西在每个调用处的实际值」,因此可以在编译时直接把这个值硬编码到代码里任何地方。具体来说,就是这个东西不仅要是 static final,而且类型也只能是基本类型或者 String。因为这些类型在不重新赋值的情况下,是不能通过调用内部方法来修改内容的。

什么意思呢?比如一个 User 类:

  1. public class User {
  2. public User(int id, String name) {
  3. this.id = id;
  4. this.name = name;
  5. }
  6. int id;
  7. String name;
  8. }

我在一个地方声明了一个 static finalUser,它是不能二次赋值的:

  1. static final User user = new User(123, "rengwuxian");

但是你通过访问这个 User 的内部成员,还是可以对它进行修改:

  1. user.name = "zhukai";

这其实并不符合「常量」的概念。而如果把类型限制为基本类型或者 String,再加上 static final 的限制,就可以确保真的「不可变」了。

在 Kotlin 里,如果你要写一个这样的常量,就可以用 const 来修饰 val

数组和集合

数组

Java 的数组到了 Kotlin 里,变成了和集合类一样的泛型式写法:

  1. val strs: Array<String> = arrayOf("a", "b", "c")

具体的使用跟 Java 的数组是一样的:

  1. println(strs[0])
  2. strs[1] = "B"

另外,你也可以用 get()set() 函数:

  1. println(strs.get(0))
  2. strs.set(1, "B")

Kotlin 的 Array 在编译成 java 字节码的时候,依然用的是 Java 的数组,不过因为语言层面改成了泛型实现,因此 Kotlin 的数组失去了协变 (covariance) 的特性。也就是说,在 Kotlin 里,你不能把一个子类的数组对象赋值给一个父类的数组:

  1. val strs: Array<String> = arrayOf("a", "b", "c")
  2. val anys: Array<Any> = strs // ❌

这个在 Java 里面是没问题的:

  1. String[] strs = {"a", "b", "c"};
  2. Object[] objs = strs; // ✅

(愣住)

image-20190707174036403

关于 covariance 的问题,我就不展开说了,这个是跟泛型相关的一个大话题。Kotlin 对数组做出这种改变,不是为了收窄功能,而是为了给数组添加一系列的工具方法:

配图列几个工具方法,比如:

  • contains()
  • first()
  • average()

这样,数组的实用性就大大增加了。

集合

另外,Kotlin 对集合类也进行了重写,创造了自己的一套 List、Set、Map 类型,目的也是给它们扩展功能:

也展示一下 List 的那些类。

在 Kotlin 里要使用 List,写法大概是这样的:

  1. val strs: List<String> = listOf("a", "b", "c")

跟数组的写法很像哈?

不过 Kotlin 的 List 是不可变的,也就是不可修改:不能添加、修改和删除元素。如果要修改,需要用可变的 List

  1. val strs: MutableList<String> = mutableListOf("a", "b", "c")

不可变 List 除了不可变这个限制之外,也多了一个特性:它是 covariant (协变)的。也就是说,你可以把子类的 List 赋值给父类的 List

  1. val strs: List<String> = listOf("a", "b", "c")
  2. val anys: List<Any> = strs // ✅

另外,Kotlin 是有类型推断的,所以很多时候,数组和集合的类型也可以不用标明:

  1. val strs = arrayOf("a", "b", "c")
  2. val strs = listOf("a", "b", "c")
  3. val strs = mutableListOf("a", "b", "c")

对比

所以 Kotlin 里的数组和可变 List 的 API 是非常像的,最主要的区别是 Kotlin 的数组内部实现本质上还是 Java 的数组,所以继承了 Java 数组的元素个数不可变的性质,跟 List 比起来会有点不方便。

(愣住)

那我要数组干嘛?

这个问题其实在用 Java 的时候就存在了:Java 的数组和 List 功能这么相似,我该用谁?用谁都行,只不过 List 用起来更舒服一些,功能也更多些,所以更多的人会倾向于用 List。只是有一点:由于 Java 的基本类型的数组(int[]float[] 等)没有自动装箱和拆箱,而 List 是有的,所以对于基本类型,数组的性能会比 List 好一些。

不过如果在 Kotlin 里用基本类型的数组,要用专门的数组类(IntArray FloatArray)才能免于自动装箱拆箱。

总结

好了,这就是本期内容:Kotlin 里那些「不是那么写的」,也就是 Kotlin 里那些跟 Java 完全不兼容的写法。如果你觉得有用,欢迎关注收藏留言分享在看。另外,别忘了看文章!

可见性修饰符

脚本还没写。

  • 全部默认 public
  • @hideinternal
    • module 内可见
    • 什么是 「module」?
  • Java 的 default (package visible):没了
  • private 和 Java 里 private 的区别