小编说:构造函数其实并不是一个真正的函数,因为它没有返回值类型,连函数名也被严格约束。构造函数一方面承担为类型分配内存空间的责任,另一方面的作用就是初始化部分字段。本文向大家介绍了Kotlin 中的构造函数声明与调用。 本文选自《揭秘Kotlin编程原理》一书
- 1 Kotlin构造函数
Kotlin作为面向对象的编程语言,也支持为类型声明构造函数。不过Kotlin声明构造函数的方式相比Java有所变化,下面这个示例演示了在Kotlin中声明构造函数的方式。
清单:SharedBike.kt
代码语言:javascript复制功能:Kotlin构造函数声明
fun main(args:Array<String>){
var sharedbike = SharedBike("MB", 188)
println("color=${sharedbike.color}")
}
class SharedBike(manufacturer:String, color:Int){
var manufacturer:String
var color:Int
init{
this.manufacturer = manufacturer
this.color = color
}
}
如本例所示,在类SharedBike(注:共享单车)的类型名称后面紧跟参数列表(本示例包含两个入参),就完成了构造函数定义。类型字段的初始化逻辑被放在init{}块中,init{}块是Kotlin中的语法糖,与Java中的static{}块类似,仅仅是外在的一种语法特性。这么声明之后,在main()测试用例中便能够通过“var sharedbike = SharedBike("MB", 188)”直接调用构造函数。
像这种直接在类型名称之后声明的构造函数,被称为“主构造函数”。之所以被称为主构造函数,其原因应该是这种方式所声明的构造函数,具有无可比拟的特殊性,或者是具有至高无上的地位。众所周知,只要入参数量或入参类型、顺序不同,就可以为一个类型声明多个构造函数。但是Kotlin通过在类型名称之后所声明的构造函数只能有一种,在这种方式下,你不可能同时声明多个构造函数,所以才称其为“主”。主构造函数其实是相对于次构造函数而言的,次构造函数在Kotlin中被叫作“二级构造函数”,下文会讲解它。
乍一看,感觉Kotlin的主构造函数的声明方式,仅仅在形式上与Java或C 的构造函数不同,貌似没啥特别的作用。其实不然,接着往下看。
- 2 简化的主构造函数
Kotlin自始至终秉承“简单至上”的设计宗旨,那么在构造函数的声明上,如何能够简化呢?至少从上一节所介绍的主构造函数的声明方式上,我们没看出来有多么简化,顶多是一种形式上的变化而已。
其实,Kotlin之所以要提供主构造函数的这种声明方式,正是为了极大简化属性的定义和初始化。在上一节所举的SharedBike类型的示例中,主构造函数的入参形式是:
代码语言:javascript复制manufacturer:String, color:Int
现在如果将其进行简单修改,改成如下方式:
代码语言:javascript复制var manufacturer:String, var color:Int
这种修改很简单,在每一个入参名称之前都加上var关键字,但是所起的效果就大不相同了。效果包括两方面。
(1)声明了类属性
在构造函数里通过var manufacturer:String和var color:Int分别声明了两个属性,这样在类型里面就无须再专门声明属性。
(2)声明了一个构造函数
该构造函数包含两个入参,并且在构造函数中完成对类属性的初始化。
使用新的主构造函数来重新定义上一节示例中的SharedBike,就可以简化成下面这种形式。
清单:SharedBike.kt
功能:简化的主构造函数
代码语言:javascript复制class SharedBike(var manufacturer:String, val color:Int){
}
与上一节示例中的SharedBike相比,现在的SharedBike已经简化到只有一行。但是这一行代码却同时为SharedBike类型声明了两个属性,并且在构造函数里完成了初始化逻辑。这一行代码如果使用Java来写,必须这么来编写。
清单:SharedBike.java
功能:演示Java的构造函数
代码语言:javascript复制public class SharedBike{
private String manufacturer;
private Integer color;
public SharedBike3(String manufacturer, Integer color){
this.manufacturer = manufacturer;
this.color = color;
}
}
由此可见,Kotlin的主构造函数相比Java而言,是相当地精简!Java需要若干行才能完成的事,Kotlin一行搞定。
为了验证Kotlin主构造函数的功能,可以通过下面的用例进行测试。
清单:SharedBike.kt
功能:使用Kotlin构造函数
代码语言:javascript复制fun main(args:Array<String>){
var sharedbike = SharedBike("MB", 188)
println("color=${sharedbike.color}")
}
运行这段程序,顺利输出如下结果:
color=188
Kotlin主构造函数的强大是不言而喻的。
上面只使用一行就解决了类属性定义和构造函数声明的问题,但是如果开发者并不希望在构造函数中仅仅只是进行类属性的初始化赋值,还希望干点别的事,例如打印一行日志,怎么办呢?很简单,可以在init{}块中添加构造函数的特殊逻辑。
清单:SharedBike.kt
功能:在构造函数中添加自定义逻辑
代码语言:javascript复制class SharedBike(var manufacturer:String, val color:Int){
init{
println("initialed......")
}
}
fun main(args:Array<String>){
var sharedbike = SharedBike("MB", 188)
println("color=${sharedbike.color}")
}
运行程序,输出结果如下:
initialed...... color=188
根据输出结果可知,Kotlin的确将init{}块中的逻辑添加到了构造函数之中。
- 3 二级构造函数
上一节演示了Kotlin主构造函数的声明方式,直接在类名后面声明即可。既然有“主”,就有“次”。在Kotlin中,所谓的“次”构造函数,有一个专门的称谓,叫作“二级构造函数”。二级构造函数的声明形式如下:
代码语言:javascript复制constructor(param1:dataType1, param2:dataType2, ...)[: delegate]{
[initial express]
}
还是以上一节中的SharedBike为例,为其声明一个二级构造函数。
清单:SharedBike.kt
功能:Kotlin二级构造函数
代码语言:javascript复制class SharedBike(){
constructor(manufacturer:String):this(){
println("manufacturer=$manufacturer")
}
init{
println("initialed......")
}
}
fun main(args:Array<String>){
var sharedbike = SharedBike("mobai")
}
在本示例中,通过constructor关键字为SharedBike类型声明了一个包含一个入参的二级构造函数。声明后,在main()测试用例中便可以通过“var sharedbike = SharedBike("mobai")”调用该构造函数。运行这段程序,输出如下结果:
initialed...... manufacturer=mobai
在本示例中,也许道友注意到所定义的二级构造函数的后面多了一个后缀,即“:this()”,其实这表示构造函数代理,其中的this表示主构造函数。Kotlin规定,每一个二级构造函数都必须要直接或间接代理主构造函数。在本示例中,并未声明主构造函数,这意味着其实声明了一个默认的无参的主构造函数,所以通过constructor关键字定义的二级构造函数必须通过“:this()”来代理默认构造函数。
其实这也正是必须对Kotlin中的源程序中以.kt结尾的类型声明添加括号的原因。在Java中声明一个类,一般形式如下:
代码语言:javascript复制public class SharedBike{
}
而在Kotlin中,却必须在SharedBike类名后面添加括号,如下:
代码语言:javascript复制class SharedBike(){
}
其实这便是Kotlin支持主构造函数的原因,如果类名后面的括号中没有入参,则表示在声明一个默认的、无参的主构造函数。
Kotlin的二级构造函数可以间接代理主构造函数,二级构造函数只需要通过代理其他二级构造函数便能实现间接代理。
清单:SharedBike.kt
功能:二级构造函数间接代理主构造函数
代码语言:javascript复制class SharedBike(){
/** 直接代理主构造函数 */
constructor(manufacturer:String):this(){
println("manufacturer=$manufacturer")
}
/** 通过代理另一个二级构造函数,间接代理主构造函数 */
constructor(manufacturer: String, color:Int): this(manufacturer){
println("manufacturer=$manufacturer, color=$color")
}
init{
println("initialed......")
}
}
本示例中一共声明了两个二级构造函数,其中第2个构造函数代理了第一个构造函数,由于第一个构造函数直接代理了主构造函数,因此第2个构造函数通过代理第一个构造函数,便相当于间接代理了主构造函数。
由于本示例中定义了两种二级构造函数,因此可以分别调用这两种构造函数来实例化SharedBike类型,下面的示例直接使用了第2种构造函数:
代码语言:javascript复制fun main(args:Array<String>){
var sharedbike = SharedBike("ofo", 188)
}
运行该程序,输出如下:
initialed...... manufacturer=ofo manufacturer=ofo, color=188
由输出结果,可以推导出二级构造函数的调用顺序,即Kotlin会先调用被代理的构造函数。
当然,本示例的第2个构造函数也可以直接代理主构造函数,而不必通过第1个二级构造函数进行间接代理,如下面的例子。
清单:SharedBike.kt
功能:二级构造函数间接代理主构造函数
代码语言:javascript复制class SharedBike(){
/** 直接代理主构造函数 */
constructor(manufacturer:String):this(){
println("manufacturer=$manufacturer")
}
/** 第2个二级构造函数仍然直接代理主构造函数 */
constructor(manufacturer: String, color:Int): this(){
println("manufacturer=$manufacturer, color=$color")
}
init{
println("initialed......")
}
}
- 4 C 构造函数与参数列表
Kotlin博采众家编程语言之长,吸收了很多其他语言中的优秀设计,有些吸收是表面形式化的,而有些则是内在机制层面的吸收。在构造函数这方面,二级构造函数的代理语法形式,与C 的构造函数继承语法形式简直惊人地相似!但是很显然,Kotlin中的主构造函数代理并不涉及继承体系,所以在内在机制上与C 完全不同。只能说Kotlin的设计人员可能很喜欢C 中的那种继承机制。先看看C 中的构造函数继承语法。
清单:bike.cpp
功能:C 的构造函数继承
代码语言:javascript复制class Bike
{
public:
Bike()
{
cout<<"基类默认构造函数"<<endl;
}
Bike(int i)
{
cout<<"基类含参构造函数"<<endl;
cout<<i<<endl;
}
virtual ~Bike(){}
};
class SharedBike: public Bike{
public:
SharedBike(int i, int j=0):Bike(j){
cout<<"基类含参构造函数调用"<<endl;
cout<<i<<endl;
}
virtual ~SharedBike(){}
};
在本示例中,SharedBike继承了父类Bike,所以SharedBike类的构造函数可以继承Bike类的构造函数。这种构造函数继承的语法看起来与Kotlin的二级构造函数代理语法简直一模一样,但是功能则完全不同。
二级构造函数在代理时,被代理的构造函数(主构造函数或二级构造函数)中的入参必须在所声明的二级构造函数中的参数列表中定义过,由此可以推断出:
二级构造函数的入参列表集合必须包含但不能等于被代理的构造函数参数列表。
根据这个宗旨,如果开发者未显式定义一个主构造函数(这种情况也可以被认为是开发者显式定义了一个默认的、无参的主构造函数),则不能声明一个无参的二级构造函数,如下面的示例。
清单:SharedBike.kt
功能:重复入参列表的构造函数声明
代码语言:javascript复制class SharedBike(){
constructor():this(){
}
}
本示例并没有为SharedBike类显式定义一个包含入参的主构造函数,这就相当于为SharedBike类定义了一个无参的、默认的构造函数,因此在类内部试图再为类定义一个无参的二级构造函数时,编译器就不干了。
同理,如果主构造函数包含一个入参列表,则二级构造函数的入参列表不能与之重复,以免重复声明。例如下面的示例。
清单:SharedBike.kt
功能:二级构造函数与主构造函数的参数列表
代码语言:javascript复制class SharedBike( manufacturer:String, color:Int){
var manufacturer:String
var color:Int
init{
this.manufacturer = manufacturer
this.color = color
}
constructor(manufacturer: String, color: Int): this(manufacturer, color){
}
}
本示例中的主构造函数包含两个入参,在类内部试图再定义一个包含相同入参列表的二级构造函数时,结果编译也通不过。
从严格意义上来说,上面的推论并不完全正确,并非二级构造函数的参数列表集合只要包含被代理的构造函数的参数列表就行了,因为如果这两个参数列表集合相等,但是入参顺序不同,也是完全可以的。如下面的示例。
清单:SharedBike.kt
功能:二级构造函数与被代理构造函数的参数集合相等但是入参顺序不同
代码语言:javascript复制class SharedBike( manufacturer:String, color:Int){
var manufacturer:String
var color:Int
init{
this.manufacturer = manufacturer
this.color = color
}
constructor(color: Int, manufacturer: String): this(manufacturer, color){
}
}
本示例的主构造函数包含两个入参,而其内部通过constructor关键字定义的二级构造函数也包含同样的两个入参,但是入参类型和顺序都不同,因此编译器不会报错。
- 5 默认构造函数与覆盖
Kotlin与Java一样,如果开发者未定义构造函数,则Kotlin会自动提供一个默认的实现,这种默认的实现即为“默认构造函数”。默认构造函数没有入参,因此在调用时无须传参,例如下面的示例。
清单:SharedBike.kt
功能:默认构造函数
代码语言:javascript复制class SharedBike(){
init{
println("initialed......")
}
}
fun main(args:Array<String>){
var sharedbike = SharedBike()
}
但是如果开发者显式声明一个主构造函数,则默认构造函数会被覆盖,开发者不能再调用无参的构造函数。
清单:SharedBike.kt
功能:默认构造函数覆盖
代码语言:javascript复制class SharedBike(var manufacturer:String, val color:Int){
}
fun main(args:Array<String>){
var sharedbike = SharedBike()
}
在本例中,为SharedBike显式声明了构造函数,结果在main()测试用例中试图直接调用SharedBike()这个无参的构造函数时,编译器便会报错。
根据“二级构造函数必须直接或间接代理主构造函数”的规则,并且二级构造函数的入参列表集合至少不能小于被代理的构造函数的入参列表,因此可以进一步推断出这样一个结论:
只要开发者为一个类定义了带入参的主构造函数,则默认的、无参的构造函数将被完全覆盖,不能再通过调用这种无参的构造函数来实例化类型实例。
- 6 构造函数访问权限与缺省
前面详细分析了Kotlin中的主构造函数和二级构造函数的声明与代理语法,其中主构造函数属于Kotlin中极具创新的一个语法特性,直接声明在类头部分。
其实,在Kotlin中声明构造函数时都需要添加“constructor”这个前缀关键字。而前面章节在声明主构造函数时,都没有加constructor这个关键字,这是因为在特殊的情况下,这个关键字在主构造函数的声明中可以省略。
正是因为在很多情况下,主构造函数的声明中的constructor关键字都是可以省略的,所以这给类的定义带来了便利。如果主构造函数声明的constructor关键字不能省略,则即便定义一个简单的类型,也必须写成如下形式:
代码语言:javascript复制class SharedBike constructor(){
/** 直接代理主构造函数 */
constructor(manufacturer:String):this(){
println("manufacturer=$manufacturer")
}
init{
println("initialed......")
}
}
这里没有为SharedBike类声明主构造函数,但是也可以理解成为其声明了一个默认的、不含参的主构造函数,因此,必须加上constructor这个关键字。
这里所谓的“特殊情况”便是指下面这种情况:
当主构造函数没有注解或可见性说明时。
相反,当主构造函数包含可见性说明时,或者包含注解时,constructor关键字不可省略。例如下面的例子。
清单:SharedBike.kt
功能:主构造函数的可见性声明
代码语言:javascript复制class SharedBike private constructor( manufacturer:String, color:Int){
var manufacturer:String
var color:Int
init{
this.manufacturer = manufacturer
this.color = color
}
constructor(color: Int, manufacturer: String): this(manufacturer, color){
println("constructor...")
}
}
在本例中,主构造函数被声明为private类型,这时就必须添加constructor关键字。
注意在本示例中,主构造函数被声明为private级别的访问权限,所以无法再通过以下语句实例化SharedBike类:
代码语言:javascript复制var sharedbike = SharedBike("ofo", 188)
但是本示例中的二级构造函数并没有被添加private关键字来修饰,因此其默认拥有public级别的访问权限,所以可以通过如下语句来实例化SharedBike类:
代码语言:javascript复制var sharedbike = SharedBike(188, "mobai")
除了主构造函数可以使用private来限制其访问级别外,二级构造函数也可以。例如上面的示例,改成下面这种格式。
清单:SharedBike.kt
功能:二级构造函数的可见性声明
代码语言:javascript复制class SharedBike private constructor( manufacturer:String, color:Int){
var manufacturer:String
var color:Int
init{
this.manufacturer = manufacturer
this.color = color
}
private constructor(color: Int, manufacturer: String): this(manufacturer, color){
println("constructor...")
}
}
修改后的SharedBike类连二级构造函数也都被声明为private类型,限制无论如何都不能通过调用任何构造函数来实例化SharedBike类型。