构造函数

我们先来看一段 Java 代码:

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
public class Test {
public int id;
public String name;

{
System.out.println("before Test");
}

public Test(int id, String name) {
System.out.println("on Test");
this.id = id;
this.name = name;
}

{
System.out.println("after Test");
}

public static void main(String[] args) {
new Test(1, "test");
}
}

//运行结果
before Test
after Test
on Test

这里可以得到以下结论:

  • Java 中可以使用 {} 在类初始化的同时做一些初始化工作
  • 不管 {} 的位置如何,它们都先于类的构造函数之前执行

那么,在 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
class Test {
val id: Int
val name: String

init {
println("before Test")
}

constructor(id: Int, name: String) {
this.id = id
this.name = name
println("on Test")
}

init {
println("after Test")
}
}

fun main() {
Test(1, "name")
}

//运行结果
before Test
after Test
on Test

Kotlin 中这样写可以达成和刚才一样的效果,这里可以得出以下结论:

  • Kotlin 中构造函数的方法名统一都是:constructor
  • Kotlin 中如果需要使用 init 前缀的 {} 来做一些初始化工作

主构造函数

我们先来看下主构造函数的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test constructor(id: Int, name: String) {
val content = name

init {
println("name = $name")
}
}

fun main() {
Test(1, "name")
}

//运行结果
name = name

从形式上来看,constructor 构造函数被我们移动到了类名之后,并且类的属性初始化器和 init 代码块中可以使用主构造器中的参数,但是普通函数中无法使用。

这种写法叫做 [主构造函数 primary constructor],一个类最多只能有一个主构造方法,也可以没有。之前的构造函数写法被称为 [次构造函数 secondary constructor],次构造函数可以有多个。主构造函数不能包含任何代码,初始化的代码可以放到 init 前缀的 {} 中。

主构造器有以下两个性质:

  • 必须性:当有主构造器存在的时候,次构造器都需要委托给主构造器
  • 第一性:类初始化过程中,首先执行的就是主构造器

关于类初始化过程中各模块的执行顺序,可以看下面这个例子:

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
class Test constructor(id: Int, name: String) {
val content = name.also { println("content = $name") }

init {
println("init1")
}

init {
println("init2")
}

constructor(id: Int, name: String, other: String) : this(id, name) {
println("secondary constructor")
}
}

fun main() {
Test(1, "name", "other")
}

//运行结果
content = name
init1
init2
secondary constructor

初始化过程执行顺序如下:

  1. 类的主构造函数
  2. 类的属性初始化器和 init 初始化块按照书写顺序
  3. 类的次构造函数

初始化过程执行顺序解释:

  1. 类的属性初始化器和 init 初始化块实际上会成为主构造器的一部分
  2. 当有主构造函数存在的时候,次构造函数需要委托主构造函数。委托给主构造函数会成为次构造函数的第一条语句。
  3. 当没有主构造函数存在的时候,这种委托还是会隐式发生,并且仍然会执行属性初始化器和 init 初始化块。

默认情况下,主构造函数的 constructor 是可以省略的,上面的代码可以这么写:

1
class Test(id: Int, name: String)

以下两种情况,constructor 不可以省略:

  • 主构造函数上使用可见性修饰符
  • 主构造函数上使用注解

主构造函数中声明属性

刚才主构造函数的参数只能在属性初始化器和 init 初始化块中使用,但是只要给它们加上 val 或者 var 关键字就可以像普通属性一样。也就是说下面的 Kotlin 代码和 Java 代码是等价的。

1
2
3
class Test(var id: Int, var name: String) {
//noop
}
1
2
3
4
5
6
7
8
9
public class Test {
public int id;
public String name;

public Test(int id, String name) {
this.id = id;
this.name = name;
}
}

当然,除了 valvar 关键字,还可以添加可见性修饰符、注解等。

下面代码中是主构造函数在继承过程中可能出现的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
open class Test constructor(private var id: Int, private var name: String) {
init {
println("Test init: id = $id name = $name")
}
}

class NewTest(var id: Int, var name: String) : Test(id, name) {
init {
println("NewTest init: id = $id name = $name")
}
}

fun main() {
NewTest(1, "test")
}

//运行结果
Test init: id = 1 name = test
NewTest init: id = 1 name = test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class Test constructor(private var id: Int, open var name: String) {
init {
println("Test init: id = $id name = $name")
}

protected abstract var temp: Int
}

class NewTest(var id: Int, override var name: String, override var temp: Int) : Test(id, name) {
init {
println("NewTest init: id = $id name = $name")
}
}

fun main() {
NewTest(1, "hhh", 2)
}

//运行结果
Test init: id = 1 name = null
NewTest init: id = 1 name = hhh

Object

如果想直接通过类名调用方法,在 Java 中需要使用 static 修饰待访问的属性或者方法。在 Kotlin 中可以使用 object 来完成这个功能。

1
2
3
4
5
6
7
8
9
object Default {
fun test() {
//nop
}
}

fun main() {
Default.test()
}

那么 object 的原理是什么呢?可以反编译成字节码再转换成 Java 代码看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class Default {
@NotNull
public static final Default INSTANCE;

public final void test() {
}

private Default() {
}

static {
Default var0 = new Default();
INSTANCE = var0;
}
}

可以得出结论:

  • object 意味着创建一个类,并且创建一个这个类的对象:INSTANCE,也就是单例模式
  • object 创建单例的模式是饿汉式,所以是线程安全的

Java 代码访问 object,需要这样访问:Default.INSTANCE.test()。可以通过给属性或者方法添加 JvmStatic 注解让 Java 代码可以直接访问。

companion object

object 修饰的对象中的属性和方法都是静态的,如果只想要一部分属性和方法是静态的可以内部嵌套 object

1
2
3
4
5
6
7
8
class A {
object B {
var c = 0
}
}

//使用
A.B.c

类中嵌套的对象可以用 companion 修饰,companion 意味着伴生、伴随,表示修饰的对象和外部类绑定,一个类中最多只可以有一个伴生对象。这样的好处是:

  • 外部调用的时候可以省略对象名
  • companion 修饰时,对象名称可以省略
1
2
3
4
5
6
7
8
class A {
companion object {
var c = 0
}
}

//使用
A.c

top-level property / function

除了 object 这种,Kotlin 还有更方便的东西:[top-level declaration 顶层声明],就是把属性和方法不写在 class 中:

1
2
3
4
5
6
package com.xl.test

//属于 package,不在 class/object 内
fun test() {
//nop
}