Kotlin 主次构造函数理解

今天在做需求时需要自定义两个 View,在取自定义属性时稍微纠结了一下该放在哪里

  • 是不是该在主构造函数中调用
  • 但是主构造方法不能有方法体
  • 那是不是不应该放在 init 代码块里
  • 但是 init 代码块不是会在类构造前就调用的吗,那怎么取到自定义属性

搞来搞去,越来越乱,和 Java 的用法彻底搞混淆了,说来惭愧,用了 Kotlin 这么多年,还是没有彻底理清这种基础的东西,现在做点笔记记录下

Java 构造函数与 init 代码块

首先直接捋一下 Java 的,相对 Kotlin 的应该比较基础

想要在 Android 项目中直接跑 Java 代码,可以 new module 创建一个 Java or Kotlin Library,选择 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
public class Person {

{
System.out.println("初始化块1");
}

public Person() {
System.out.println("无参构造函数");
}

{
System.out.println("初始化块2");
}

public static void main(String[] args) {
new Person();
}

}

// 执行结果:
初始化块1
初始化块2
无参构造函数

从上面的代码和运行结果可以看出,当创建 Java 对象时:

  • 先执行初始化块;
  • 如果定义多个初始化块,则前面的先执行,后面的后执行;
  • 然后执行构造函数。
  • 虽然可以定义多个初始化块,但是没有意义, 一般合并在一起,代码更加简洁。

static init 代码块

使用 static 修饰符定义的初始化块,称为静态初始化块,也叫类初始化块。普通初始化块是对对象初始化,类初始化块是对类初始化。因此静态初始化块总是比普通初始化块先执行。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class Root {

static {
System.out.println("Root static init");
}

{
System.out.println("Root obj init");
}

public Root() {
System.out.println("Root constructor func");
}

}

class Mid extends Root {

static {
System.out.println("Mid static init");
}

{
System.out.println("Mid obj init");
}

public Mid() {
System.out.println("Mid constructor func");
}

public Mid(String msg) {
this();
System.out.println("Mid has param constructor: " + msg);
}

}

class Leaf extends Mid{

static {
System.out.println("Leaf static init");
}

{
System.out.println("Leaf obj init");
}

public Leaf() {
super("MESSAGE");
System.out.println("Leaf constructor func ");
}

}

public class Test{

public static void main(String[] args){
new Leaf();
}

}

// 执行结果
Root static init
Mid static init
Leaf static init
Root obj init
Root constructor func
Mid obj init
Mid constructor func
Mid has param constructor: MESSAGE
Leaf obj init
Leaf constructor func

从上面的代码和执行结果看:

  • 会先进行类初始化, 然后对象初始化,类初始化全部先执行一遍。先执行父类初始化,然后子类初始化。
  • 对象初始化时,同样先执行父类的初始化块,构造函数,然后执行子类的。

另外,静态初始化块和静态成员变量,初始化的值与代码顺序相对应。如果你先 int a = 5,然后再写 init 块改变它的值为 8,最后输出 a 为 8,反之如果先 init 块中赋值 a = 8,再 int a = 5,那么最后输出 a 为 5

总结

  • 初始化块是构造函数的补充。
  • 初始化块总在构造函数之前执行。
  • 初始化块是一段固定执行的代码, 不接受任何参数, 因此,如果一段初始化代码对所有对象相同, 且不用接收任何参数,则可以把这段初始化代码放在初始化块中。

实际上初始化块是一个假象, 使用 javac 编译 Java 类后, 初始化块消失,被”还原”到构造器中,且位于构造器所有函数的前面。

参考文章

Kotlin 构造函数与 init 代码块

然后再看一下 Kotlin 相关的内容,在 Kotlin 中有两种类型的构造函数,分别是主构造函数(主构造器)和次级构造函数(次级构造器),在 Kotlin 类中只有一个主构造函数,而次级构造函数可以是一个或者多个。

construction 在 Kotlin 中是一个关键字,在 Java 中,构造方法名必须和类名相同,例如文中开头写的 Java 的构造函数;而在 Kotlin 中,是通过 constructor 关键字来标明的,对于主构造函数来说,它的位置在类的标题中声明,而对于次级构造函数来说它的位置在类中。

并且当 constructor 关键字没有注解和可见性修饰符作用于它时,constructor 关键字可以省略。

1
2
3
4
5
6
7
class Person(name: String, age: Int) { ... } // 省略
class Person private constructor(name: String, age: Int) { ... } // private 修饰
class Person @JvmOverloads constructor(name: String, age: Int = 12) { ... } // 有注解

// 如果构造方法中参数用了 val 或 var 修饰,那么他们就是类的属性,否则只是参数
class Person(name: String, age: Int) { ... } // name 和 age 都不是 Person 类的属性
class Person(val name: String, var age: Int) { ... } // name 和 age 都是 Person 类的属性

主构造函数

主构造函数用于初始化类,它在类标题中声明。需要注意的是主构造函数不能包含任何代码,所以这时的初始化代码块本质就是主构造函数的方法体。

当我们定义一个类并没有声明一个主构造函数的时候,Kotlin 会默认为我们生成一个无参的主构造函数,这一点和 Java 一样

次级构造函数

我们可以在同一类中使用主构造函数和次级构造函数。如果一个类有次构造函数,那么这些次构造函数就必须调用主构造函数,方式可以不同:

  1. 无参的主构造函数会被次级构造函数隐式调用,且顺序在次级构造函数之前
  2. 可以使用 this() 对同一个类中的另一个构造函数进行调用
  3. 可以使用 super() 来调用父类的构造函数

总结

Kotlin 的用法和 Java 的有比较大的不同,使用时要注意区分

参考文章

回到问题

梳理完毕,现在我们看回一开始的问题

  • 是不是该在主构造函数中调用
    • 确实是应该在主构造中调用,View 构造完就该读取自定义属性了
  • 但是主构造方法不能有方法体
    • init 就是主构造的方法体
  • 那是不是不应该放在 init 代码块里
    • 应该放在 init 代码块里,因为它就是主构造的方法体
  • 但是 init 代码块不是会在类构造前就调用的吗,那怎么取到自定义属性
    • Java 的会在类构造前调用,但是 Kotlin 的不会,Kotlin 中 init 就是主构造函数的一部分,执行到 init 时,已经跑完主构造函数,它能够取到自定义属性

Kotlin 主次构造函数理解
https://enderhoshi.github.io/2024/03/07/Kotlin 主次构造函数理解/
作者
HoshIlIlI
发布于
2024年3月7日
许可协议