科特林:深入研究。 构造函数和初始化程序



早在2017年5月,谷歌宣布Kotlin已成为Android的官方开发语言。 然后,有人第一次听到了这种语言的名称,有人在上面写了很长时间,但是从那一刻起,很明显,每个接近Android开发的人都必须了解它。 紧接着是“最后!”的热烈回应,以及“为什么我们需要一种新的语言?”的可怕愤慨。 Java不能令人满意吗?” 等 等

从那时起已经过去了足够的时间,尽管关于Kotlin是好是坏的争论仍然没有平息,但是关于Android的代码却越来越多。 甚至相当保守的开发人员也正在转向它。 此外,在网络上,您可能会偶然发现以下信息:与Java相比,掌握这种语言后的开发速度提高了30%。

如今,Kotlin已经设法从几种儿童期疾病中恢复过来,并且在Stack Overflow上充满了很多问题和答案。 用肉眼可以看出其优点和缺点。

在这波热潮中,我想到这个主意是要详细分析一门年轻但流行的语言的各个元素。 注意复杂点,并将其与Java进行比较以更清楚和更好地理解。 通过阅读文档可以更深入地了解该问题。 如果本文引起人们的兴趣,那么很可能会为整篇文章奠定基础。 同时,我将从基本的东西开始,但是这隐藏了很多陷阱。 让我们谈谈Kotlin中的构造函数和初始化程序。

与Java中一样,在Kotlin中,通过调用类构造函数来创建新对象(某种类型的实体)。 您还可以将参数传递给构造函数,并且可以有多个构造函数。 如果从外部看这个过程,那么与Java的唯一区别是在调用构造函数时缺少new关键字。 现在,更深入地了解一下课堂内发生的情况。

一个类可以具有主构造函数和辅助构造函数。
使用构造函数关键字声明构造函数。 如果主构造函数没有访问修饰符和注释,则可以省略关键字。
一个类可能没有明确声明的构造函数。 在这种情况下,在类的声明之后没有构造,我们将立即进入类的主体。 如果我们用Java作一个类比,那么这等效于缺少显式的构造函数声明,其结果是在编译阶段将自动生成默认构造函数(不带参数)。 看起来像预期的那样:

class MyClassA 

这等效于以下条目:

 class MyClassA constructor() 

但是,如果以这种方式编写,那么将礼貌地要求您删除不带参数的主构造函数。

主构造函数是在创建对象(如果存在)时始终调用的构造函数。 在考虑到这一点之后,我们将在第二级构造函数中进行更详细的分析。 因此,我们记住,如果根本没有构造函数,那么实际上只有一个(主要的)构造函数,但是我们看不到它。

例如,如果我们希望不带参数的主构造函数不具有公共访问权限,那么与private修改一起,我们将需要使用constructor关键字显式声明它。

主构造函数的主要特征是它没有主体,即 不能包含可执行代码。 它只是将参数带入自身,并将其传递到类中以供将来使用。 在语法级别,它看起来像这样:

 class MyClassA constructor(param1: String, param2: Int, param3: Boolean){ // some code } 

以这种方式传递的参数可用于各种初始化,但不能再用于其他初始化。 以纯形式,我们不能在类的工作代码中使用这些参数。 但是,我们可以在此处初始化类的字段。 看起来像这样:

 class MyClassA constructor(val param1: String, var param2: Int, param3: Boolean){ // some code } 

在这里, param1param2可以在代码中用作类的字段,等效于以下内容:

 class MyClassA constructor(p1: String, p2: Int, param3: Boolean){ val param1 = p1 var param2 = p2 // some code } 

好吧,如果您与Java进行比较,它将看起来像这样(顺便说一下,在此示例中,您可以评估Kotlin可以减少多少代码量):

 public class MyClassAJava { private final String param1; private Integer param2; public MyClassAJava(String p1, Integer p2, Boolean param3) { this.param1 = p1; this.param2 = p2; } public String getParam1() { return param1; } public Integer getParam2() { return param2; } public void setParam2(final Integer param2) { this.param2 = param2; } // some code } 

让我们谈谈其他设计师。 它们使人联想到Java中的普通构造函数:它们接受参数,并且可能具有可执行块。 声明其他构造函数时,必须使用builder关键字。 如前所述,尽管可以通过调用其他构造函数来创建对象,但也应在this的帮助下调用主构造函数(如果有)。 在语法级别,其组织如下:

 class MyClassA(val p1: String) { constructor(p1: String, p2: Int, p3: Boolean) : this(p1) { // some code } // some code } 

即 附加的构造函数实际上是主要继承人。
现在,如果我们通过调用其他构造函数来创建对象,则会发生以下情况:

调用其他构造函数;
调用主构造函数;
在主构造函数中初始化类p1的字段;
附加构造函数主体中的代码执行。

这类似于Java中的这种构造:

 class MyClassAJava { private final String param1; public MyClassAJava(String p1) { param1 = p1; } public MyClassAJava(String p1, Integer p2, Boolean param3) { this(p1); // some code } // some code } 

回想一下,在Java中,只能在构造函数主体的开头使用this从另一个构造函数调用另一个构造函数。 在Kotlin中,这个问题是由根本决定的-他们将这样的调用作为构造函数签名的一部分。 以防万一,我注意到禁止直接从附加构造函数的主体中调用任何(主要或附加)构造函数。

附加构造函数应始终引用主构造函数(如果有),但可以间接引用另一个附加构造函数。 最重要的是,在链的末端,我们仍然要重点。 构造函数的触发显然将以设计者彼此翻转的相反顺序发生:

 class MyClassA(p1: String) { constructor(p1: String, p2: Int, p3: Boolean) : this(p1) { // some code } constructor(p1: String, p2: Int, p3: Boolean, p4: String) : this(p1, p2, p3) { // some code } // some code } 

现在的顺序是:

  • 调用具有4个参数的其他构造函数;
  • 调用带有3个参数的其他构造函数;
  • 调用主构造函数;
  • 在主构造函数中初始化类p1的字段;
  • 具有3个参数的构造函数主体中的代码执行;
  • 具有4个参数的构造函数主体中的代码执行。

无论如何,编译器绝不会让我们忘记进入主构造函数的过程。

碰巧一个类没有主构造函数,而它可能有一个或多个其他构造函数。 然后,不需要其他构造函数来引用某人,但是他们也可以引用此类的其他其他构造函数。 早先,我们发现未明确指定的主构造函数是自动生成的,但这适用于类中根本没有构造函数的情况。 如果至少有一个其他构造函数,则不会创建没有参数的主构造函数:

 class MyClassA { // some code } 

我们可以通过调用以下内容来创建类对象:

 val myClassA = MyClassA() 

在这种情况下:

 class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) { // some code } // some code } 

我们只能通过以下调用创建对象:

 val myClassA = MyClassA(“some string”, 10, True) 

与Java相比,Kotlin中没有什么新内容。

顺便说一下,与主要构造函数一样,如果附加构造函数的任务只是将参数传递给其他构造函数,则它可能没有主体。

 class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) : this(p1, p2, p3, "") constructor(p1: String, p2: Int, p3: Boolean, p4: String) { // some code } // some code } 

还值得注意的是,与主构造函数不同,禁止在附加构造函数的参数列表中初始化类字段。
即 这样的记录将无效:

 class MyClassA { constructor(val p1: String, var p2: Int, p3: Boolean){ // some code } // some code } 

另外,值得注意的是,与主要构造函数一样,其他构造函数也可能没有参数:

 class MyClassA { constructor(){ // some code } // some code } 

说到构造函数,不能不提及Kotlin的便捷功能之一-为参数分配默认值的能力。

现在假设我们有一个带有多个构造函数的类,这些构造函数具有不同数量的参数。 我将用Java举例:

 public class MyClassAJava { private String param1; private Integer param2; private boolean param3; private int param4; public MyClassAJava(String p1) { this (p1, 5); } public MyClassAJava(String p1, Integer p2) { this (p1, p2, true); } public MyClassAJava(String p1, Integer p2, boolean p3) { this(p1, p2, p3, 20); } public MyClassAJava(String p1, Integer p2, boolean p3, int p4) { this.param1 = p1; this.param2 = p2; this.param3 = p3; this.param4 = p4; } // some code } 

如实践所示,这种设计非常普遍。 让我们看看如何在Kotlin上写同样的东西:

 class MyClassA (var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){ // some code } 

现在,让我们一起拍一下Kotlin,以了解他削减了多少代码。 顺便说一句,除了减少行数之外,我们还获得了更多订单。 请记住,您一定已经不止一次看到这样的东西:

  public MyClassAJava(String p1, Integer p2, boolean p3) { this(p3, p1, p2, 20); } public MyClassAJava(boolean p1, String p2, Integer p3, int p4) { // some code } 

看到它时,您想找到编写它的人,按一下按钮,将其显示在屏幕上,然后用悲伤的声音问:“为什么?”
虽然您可以在Kotlin上重复此壮举,但不是必需的。

但是,有一个细节是,在Kotlin上使用这种缩写符号的情况下,有必要考虑一下:如果要使用Java中的默认值调用构造函数,则必须在其上添加@JvmOverloads批注:

 class MyClassA @JvmOverloads constructor(var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){ // some code } 

否则,我们会得到一个错误。

现在让我们谈谈初始化器

初始化程序是带有init关键字标记的代码块。 在此块中,您可以执行一些逻辑来初始化类的元素,包括使用主构造函数中的参数值。 我们也可以从该块中调用函数。

Java也有初始化块,但是它们不是同一回事。 在它们中,我们不能像在Kotlin中那样从外部传递值(主构造函数的参数)。 初始化程序与主要构造函数的主体非常相似,在单独的块中取出。 但这是一见钟情。 实际上,这并非完全正确。 让我们做对。

没有主构造函数时,初始化程序也可以存在。 如果是这样,则其代码与所有初始化过程一样,在附加构造函数的代码之前执行。 可以有多个初始化程序。 在这种情况下,其调用顺序将与它们在代码中的位置顺序一致。 还要注意,类字段初始化可以在init块之外进行。 在这种情况下,初始化也根据代码中元素的排列发生,并且在从初始化程序块调用方法时必须将其考虑在内。 如果您不慎处理,则有可能出错。

我将为您提供一些有趣的使用初始化程序的案例。

 class MyClassB { init { testParam = "some string" showTestParam() } init { testParam = "new string" } var testParam: String = "after" constructor(){ Log.i("wow", "in constructor testParam = $testParam") } fun showTestParam(){ Log.i("wow", "in showTestParam testParam = $testParam") } } 

这段代码虽然不是很明显,但却很有效。 如果您看一下,可以看到在声明参数之前发生了将值分配给初始化程序块中的testParam字段的情况。 顺便说一句,这仅在类中有其他构造函数但没有主要构造函数的情况下才有效(如果将init块上方的testParam字段声明提高,则无需构造函数即可工作)。 如果我们用Java反编译此类的字节码,则会得到以下信息:

 public class MyClassB { @NotNull private String testParam = "some string"; @NotNull public final String getTestParam() { return this.testParam; } public final void setTestParam(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.testParam = var1; } public final void showTestParam() { Log.i("wow", "in showTestParam testParam = " + this.testParam); } public MyClassB() { this.showTestParam(); this.testParam = "new string"; this.testParam = "after"; Log.i("wow", "in constructor testParam = " + this.testParam); } } 

在这里,我们看到初始化期间(在init块中或在其外部)对字段的第一次调用等效于Java中通常的初始化。 在初始化过程中,与第一个值赋值相关联的所有其他动作(第一个(第一个值赋值与字段声明结合在一起))都将传递给构造函数。
如果我们进行反编译实验,结果发现如果没有构造函数,则会生成主构造函数,并且所有不可思议的事情都会在其中发生。 如果有多个彼此不引用的构造函数,并且没有主要的构造函数,则在此类的Java代码中,所有后续对testParam字段的值赋值在所有其他构造函数中testParam重复。 如果存在主构造函数,则仅在主构造函数中。 Fuf ...

对于testParam ,最有趣的事情是:将testParam签名从var更改为val

 class MyClassB { init { testParam = "some string" showTestParam() } init { testParam = "new string" } val testParam: String = "after" constructor(){ Log.i("wow", "in constructor testParam = $testParam") } fun showTestParam(){ Log.i("wow", "in showTestParam testParam = $testParam") } } 

我们在代码中的某处调用:

 MyClassB myClassB = new MyClassB(); 

一切都已编译,没有错误,开始了,现在我们看到了日志的输出:

在showTestParam testParam =一些字符串
在构造函数testParam =之后

事实证明,声明为val的字段在代码执行期间更改了该值。 为什么这样 我认为这是Kotlin编译器中的一个缺陷,将来可能无法编译,但是今天一切仍然如此。

从上述情况得出结论,一个人只能建议不要产生初始化块,也不要将它们散布在类中,以避免在初始化过程中重复分配值,只从初始化块中调用纯函数。 所有这些都是为了避免可能的混乱。

这样啊 初始化程序是创建对象时必须执行的特定代码块,而不管使用该对象创建哪个构造函数。

似乎已经整理好了。 考虑构造函数和初始化程序的交互。 在一个类中,一切都非常简单,但是您需要记住:

  • 调用其他构造函数;
  • 调用主构造函数;
  • 按代码中它们的位置顺序初始化类字段和初始化程序块;
  • 附加构造函数主体中的代码执行。

具有继承的案例看起来更有趣。

值得注意的是,由于Object是Java中所有类的基础,因此Kotlin中的Any都是这样。 但是,Any和Object不是同一件事。

开始了解继承的工作方式。 子类与父类一样,可能有也可能没有主构造函数,但是它必须引用父类的特定构造函数。

如果后代类具有主构造函数,则此构造函数必须指向基类的特定构造函数。 在这种情况下,后继类的所有其他构造函数都必须引用其类的主构造函数。

 class MyClassC(p1: String): MyClassA(p1) { constructor(p1: String, p2: Int): this(p1) { //some code } //some code } 

如果后代类没有主构造函数,则每个其他构造函数都必须使用super关键字访问父类的构造函数。 在这种情况下,后继类的其他附加构造函数可以访问父类的不同构造函数:

 class MyClassC : MyClassA { constructor(p1: String): super(p1) { //some code } constructor(p1: String, p2: Int): super(p1, p2) { //some code } //some code } 

另外,不要忘记通过派生类的其他构造函数间接调用父类的构造函数的可能性:

 class MyClassC : MyClassA{ constructor(p1: String): super(p1){ //some code } constructor(p1: String, p2: Int): this (p1){ //some code } //some code } 

如果后代类没有任何构造函数,则只需在后代类的名称之后添加父类的构造函数调用:

 class MyClassC: MyClassA(“some string”) { //some code } 

但是,仍然有一个带有继承的选项,其中不需要引用父类的构造函数。 这样的记录是有效的:

 class MyClassC : MyClassB { constructor(){ //some code } constructor(p1: String){ } //some code } 

但是,只有在父类的构造函数没有参数的情况下,该构造函数才是默认构造函数(主要的还是可选的-没关系)。

现在考虑继承期间初始化程序和构造函数的调用顺序:

  • 调用继承人的其他构造函数;
  • 称呼继承人的主要建设者;
  • 调用父级的其他构造函数;
  • 调用父级的主要构造函数;
  • initinit
  • 执行父级附加构造函数的主体代码;
  • 执行继承人的init块;
  • 执行继承人的附加构造函数的主体代码。

让我们来谈谈与Java的比较,实际上Java没有与Kotlin的主要构造函数类似的东西。 在Java中,所有构造函数都是对等体,可以互相调用,也可以不互相调用。 在Java和Kotlin中,有一个默认的构造函数,它是没有参数的构造函数,但仅在继承时才具有特殊的状态。 这里值得注意以下几点:在Kotlin中进行继承时,我们必须明确告诉后继类使用哪个父类的构造函数-编译器不会让我们忘记它。 在Java中,我们无法明确指出这一点。 注意:在这种情况下,将调用父类的默认构造函数(如果有的话)。

在此阶段,我们将假设我们对设计器和初始化器进行了深入的研究,现在我们几乎了解了它们的所有知识。 我们会稍作休息,朝另一个方向挖!

Source: https://habr.com/ru/post/zh-CN425943/


All Articles