这是码上开学 Kotlin 系列第 2 集的视频脚本。
视频脚本不是文章结构要求,所以不属于「必读」;但建议参与文章编写的作者看一下视频脚本再去看「文章结构要求」和写文章,这样写出来的文章和视频会更容易打配合,更容易比较「搭」。
如果你想用视频脚本直接作为基准来扩展成文章,也没问题,写得好、容易读是唯一标准。
大家好,我是扔物线朱凯。上期我们讲了 Kotlin 上手最基础的三个点:变量、函数和类型。大家都听说过,Kotlin 完全兼容 Java,这个意思是用 Java 写出来的 class 和 Kotlin 可以完美交互,而不是说你用 Java 的写法去写 Kotlin 也完全没问题,这个是不行的。这期内容我们就讲一下,Kotlin 里那些「不 Java」的写法。
首先,Kotlin 的构造函数的写法和 Java 就不一样。
Kotlin 的构造函数的命名规则不是像 Java 的构造方法那样,和类同名,而是所有构造函数统一直接叫 constructor
就好:
public class User {
int id;
String name;
public User(int newId, String newName) {
id = newId;
name = newName;
}
}
class User {
val id: Int
val name: String
constructor(newId: Int, newName: String) {
id = newId
name = newName
}
}
还有,Java 里常常配合构造方法一起使用的 init 代码块,在 Kotlin 里的写法也有了一点点改变:你需要给它加一个 init 前缀。
「啊哈?Java 有这个功能吗?」这个我就不讲了哈。
Java 里面有个字叫 final
,当它用来修饰变量的时候,表示这个变量不能被修改,或者说它的值只能被赋值一次:
final int final1 = 1;
void method(final String final2) {
System.out.println(final2);
final Date final3 = new Date();
System.out.println(final3);
}
这种不能修改的变量,在 Kotlin 里面不是用 final
来额外修饰,而是直接把 var
换掉,改成 val
:
val final1 = 1;
上一期说过,var
是 variable 的缩写, val
是 value 的缩写。
其实我们写 Java 代码的时候,很少会有人用 final
的对吧?但是我从 final
开始说起,是因为它是一个很好的切入点,来让我们去看一看 Kotlin 里那些「不 Java」的写法。所以稍后几分钟的内容,也都是从 final
来展开的。
我们继续说。final
用来修饰变量其实是很有用的,但大家都不用;可你如果去看看国内国外的人写的 Kotlin 代码,你会发现很多人的代码里都会有一堆的 val
。而且这些写 val
的人,就是当初那些不写 final
的人。为什么?因为 final
写起来比 val
麻烦一点:我需要多写一个单词。虽然只麻烦这一点点,但就导致很多人不写。
这就是一件很有意思的事:从 final
到 val
,只是方便了一点点,但却让它们的使用频率有了巨大的改变。这种改变是会影响到代码质量的:在该加限制的地方加上限制,就可以减少代码出错的概率。
不过 val
和 final
也有一个小小的区别,就是虽然 val
修饰的变量不能被二次赋值,但你可以通过自定义变量的 getter 来让变量的实际取值是动态的:
val size: Int
get() {
return items.size
}
不过这个属于特殊用法,val
的定位确实是对应的 Java 的 final
,只不过功能顺便有了这么点小扩展。
另外,刚才说到大家都不喜欢写 final
对吧?但有一种场景,大家是最喜欢用 final
的:常量。
public static final String WEBSITE_NAME = "kaixue.io";
在 Java 里面写常量,我们用的是 static
+ final
。而在 Kotlin 里面,除了 final
的写法不一样,static
的写法些也不一样,而且是更不一样。确切地说:在 Kotlin
里,静态变量和静态方法这两个概念被去除了。
不过你如果上网搜,还是能看到 Kotlin 里静态变量和方法的等价写法:使用一个叫做 companion object
的东西:
companion object {
val WEBSITE_NAME = "kaixue.io"
}
(愣住)
很多人第一次看见 companion object
这个东西,都会觉得「很麻烦」,觉得 Kotlin 作为一个新语言,为什么会有这么麻烦的东西。
这个我先不谈,我先给你来讲讲 Kotlin 里的 object
是个什么东西。
Kotlin 里的 object
——首字母小写的啊,不是大写,Java 里的 Object
在 Kotlin 里不用了,不过这是另一个话题——另外,Kotlin 里的 object
也不是一个类,而是一个修饰词。它最基本的用法是去修饰一个类,替换掉 class
关键字:
object Kotlin {
...
}
它的意思很直接:创建一个类,并且创建一个这个类的对象。这个就是 object
的意思,对象。
另外,你在程序的其他地方要使用这个对象,可以直接用类名来访问:
println(Kotlin.name)
这是什么?这不就是单例吗?对吧。所以在 Kotlin 里要创建单例,不用 Java 那一套复杂的操作(给个图),只要把 class
换成 object
就行了。另外你看,这个单例对象,它的内部字段和方法的使用方式,是不是特别像静态字段和静态方法?
所以其实 object
也是 Java 的 static
的替代品之一。只是不像 Java 那样,你要给每个字段和方法都加上 static
,Kotlin 的 object
是不用加的;不过其实你也没有选择:当你给一个类用了 object
关键字,这个类的所有方法全都相当于静态方法,因为它本质上是个单例对象嘛,所以你没得选,全都是直接用类名访问的。
另外 object
也可以用来创建匿名类对象,不过视频里就不讲了(但给个图)。
我们继续说。object
可以用于写单例,也可以用于写静态的变量和函数对吧?但你如果希望某个 class
不是单例,又要有自己的静态变量和函数,用 object
就不行了。不过你可以在这个 class
的内部写一个 object
:
class A {
object B {
var c
}
}
这样,你就可以把这个内部 object
的属性和函数当做是外部 class
的静态属性和函数来用了:
A.B.c
另外,基于 object
的这种用法, Kotlin 提供了一个更方便的写法:在 object
的左边写上 companion
,就可以省略掉 object
的名字:
class A {
companion object {
var c
}
}
A.c
这个就是 companion object
的作用:省事。从名字来看,它是陪伴着这个类共同前行的一个对象,而实际上它的作用就是让这个类有一个没名字的 object
,简化它的写法。
另外通过这种写法,你在一个 class
内部创建一个没名字的 object
,这样在使用的时候,就可以通过 class
的类名来访问内部 object
的属性和函数,这种用法就跟 Java 的静态变量和静态方法是一样的了。这就是 Java 的静态变量和方法在 Kotlin 中的「等价写法」。
不!
过!
这不是 Kotlin 推荐的做法。在 Kotlin 里有个更简便的东西,叫 top-level declaration
,「顶-层-声明」。其实就是把属性和函数的声明不写在 class
里面,这个在 Kotlin 里是允许的:
package com.hencoder.plus
fun topLevelFuncion() {
}
这样写的属性和函数,不隶属于任何 class
,而是直接隶属于 package
。它和静态变量、静态方法一样是全局的,但用起来更方便:你在其它地方用的时候,就连类名都不用写:
import com.hencoder.plus.topLevelFunction
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()
。
public actual inline fun println(message: Any?) {
System.out.println(message)
}
那么在实际使用的时候,应该用哪种呢?是用 top-level
还是用 companion object
或者 object
?
其实一般来说,如果你想写工具类,那直接创建一个文件,里面全都写成 top-level
functions 就行了;而像 TAG
这种只是针对类的而不是针对外部使用者的属性或者方法,就可以写进 companion object
或者 object
;另外, companion object
和 object
是可以有父类和接口的,所以你利用这点可以对你的全局函数进行一些功能扩展和延伸,具体怎么延伸,用的时候慢慢就懂了。
所以简单的判断原则是:能写在 top-level
就写在 top-level
,而 companion object
或者 object
,看情况按需使用。
在 Java 里面,我们写常量用的是 static final
,而在 Kotlin 里面,有一个专门的关键字用来写常量:const
。
const val PI = 3.14
不过它是针对的「编译期常量」,compile-time constant。
其实「编译期常量」是 Java 里面就有的概念了,它的意思是「编译器在编译的时候就知道这个东西在每个调用处的实际值」,因此可以在编译时直接把这个值硬编码到代码里任何地方。具体来说,就是这个东西不仅要是 static final
,而且类型也只能是基本类型或者 String
。因为这些类型在不重新赋值的情况下,是不能通过调用内部方法来修改内容的。
什么意思呢?比如一个 User 类:
public class User {
public User(int id, String name) {
this.id = id;
this.name = name;
}
int id;
String name;
}
我在一个地方声明了一个 static final
的 User
,它是不能二次赋值的:
static final User user = new User(123, "rengwuxian");
但是你通过访问这个 User 的内部成员,还是可以对它进行修改:
user.name = "zhukai";
这其实并不符合「常量」的概念。而如果把类型限制为基本类型或者 String
,再加上 static final
的限制,就可以确保真的「不可变」了。
在 Kotlin 里,如果你要写一个这样的常量,就可以用 const
来修饰 val
。
Java 的数组到了 Kotlin 里,变成了和集合类一样的泛型式写法:
val strs: Array<String> = arrayOf("a", "b", "c")
具体的使用跟 Java 的数组是一样的:
println(strs[0])
strs[1] = "B"
另外,你也可以用 get()
和 set()
函数:
println(strs.get(0))
strs.set(1, "B")
Kotlin 的 Array
在编译成 java 字节码的时候,依然用的是 Java 的数组,不过因为语言层面改成了泛型实现,因此 Kotlin 的数组失去了协变 (covariance) 的特性。也就是说,在 Kotlin 里,你不能把一个子类的数组对象赋值给一个父类的数组:
val strs: Array<String> = arrayOf("a", "b", "c")
val anys: Array<Any> = strs // ❌
这个在 Java 里面是没问题的:
String[] strs = {"a", "b", "c"};
Object[] objs = strs; // ✅
(愣住)
关于 covariance 的问题,我就不展开说了,这个是跟泛型相关的一个大话题。Kotlin 对数组做出这种改变,不是为了收窄功能,而是为了给数组添加一系列的工具方法:
配图列几个工具方法,比如:
contains()
first()
average()
这样,数组的实用性就大大增加了。
另外,Kotlin 对集合类也进行了重写,创造了自己的一套 List、Set、Map 类型,目的也是给它们扩展功能:
也展示一下
List
的那些类。
在 Kotlin 里要使用 List
,写法大概是这样的:
val strs: List<String> = listOf("a", "b", "c")
跟数组的写法很像哈?
不过 Kotlin 的 List
是不可变的,也就是不可修改:不能添加、修改和删除元素。如果要修改,需要用可变的 List
:
val strs: MutableList<String> = mutableListOf("a", "b", "c")
不可变 List
除了不可变这个限制之外,也多了一个特性:它是 covariant (协变)的。也就是说,你可以把子类的 List
赋值给父类的 List
:
val strs: List<String> = listOf("a", "b", "c")
val anys: List<Any> = strs // ✅
另外,Kotlin 是有类型推断的,所以很多时候,数组和集合的类型也可以不用标明:
val strs = arrayOf("a", "b", "c")
val strs = listOf("a", "b", "c")
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
@hide
:internal
- module 内可见
- 什么是 「module」?
- Java 的 default (package visible):没了
private
和 Java 里private
的区别