【Kotlin学习】类、对象和接口——带非默认构造方法或属性的类、数据类和类委托、object关键字

声明一个带非默认构造方法或属性的类

在java中一个类可以生命一个或多个构造方法,kotlin也一样,但做出了一点修改,它区分了主构造方法(通常是主要而简洁的初始化类的方法,并且在类体外部声明)和从/次构造方法(在类体内部声明)。同样也允许在初始化语句块中添加额外的初始化逻辑

初始化类:主构造方法和初始化语句块

在kotlin基础中我们见过如何声明一个类

image.png
通常来讲,类的所有声明都在花括号中,而这一段被括号围起来的语句块叫作主构造方法。它有两个目的
1.表明构造方法的参数
2.定义使用这些参数初始化的属性

可以编写的用来完成同样事情的最明确的代码

image.png
constructor关键字用来开始一个主构造方法或从构造方法的声明

init关键字用来引入一个初始化语句块,这种语句包含了在类被创建时执行的代码,并会与主构造方法一起使用。因为主构造方法有语法限制,不能包含初始化代码。也可以在一个类中声明多个初始化语句块

在这个例子中不需把初始化代码放在初始化语句块中,因为它可以与name属性的声明结合。如果主构造方法没有注解或可见性修饰符,同样可以去掉constructor关键字

image.png
代码可以通过把val关键字加在参数前的方式来进行简化,这样可以替换掉类中的属性定义

image.png
val意味着相应的属性会用构造方法的参数来初始化

可以像函数参数一样为构造方法参数声明一个默认值

image.png
要创建一个类的实例只需要直接调用构造方法,不需要new关键字

image.png
注意!如果所有的构造方法参数都有默认值,编译器会生成一个额外的无参的构造方法来使用所有的默认值

如果你的类具有一个父类,主构造方法同样要初始化父类。可以通过在基类列表的父类引用中提供父类构造方法参数的方式来做到

image.png

如果没有给一个类声明任何的构造方法,将会生成一个不做任何事情的默认构造方法

image.png

如果继承了User类且没有提供任何的构造方法,必须显式调用父类的构造方法,即使它没有任何参数

image.png
这就是为什么在父类名称后面还需要一个空括号。注意与接口的区别:接口没有构造方法,所以不需要在父类列表中它的名称后加上括号

若想确保你的类不被其他代码实例化,必须把构造方法标记为private

image.png
因为它只有一个private的构造方法,这个类外部的代码不能实例化它

private构造方法的替代方案

在java中通过使用private构造方法禁止实例化这个类来表示这个类是一个静态实用工具成员的容器或者是单例的。在kotlin中可以使用顶层函数标识静态实用工具。要表示单例可以使用对象声明

构造方法:用不同的方式来初始化父类

大多数在java中需要重载构造方法的场景都被kotlin支持参数默认值和参数命名的语法涵盖。
贴士:不要声明多个从构造方法来重载和提供参数默认值,应该直接标明默认值

但还是会有需要多个构造方法的情景,设想一下一个在Java声明的具有两个构造方法的View类,kotlin中相似的声明如下

image.png 这个类没有声明一个主构造方法,但声明了两个从构造方法,从构造方法使用constructor关键字引出。可以声明任意多个从构造方法

若想扩展这个类,可声明同样的构造方法

image.png

也可以使用this关键字,从一个构造方法中调用你自己的类的另一个构造方法

image.png 如果类没有主构造方法,那么每个从构造方法必须初始化基类或者委托给另一个这样做了的构造方法

实现在接口中声明的属性

在kotlin中,接口可以包含抽象属性声明

image.png
这意味着实现User接口的类需要提供一个取得name值的方式。接口本身并不包含任何状态,因此只有实现这个接口的类在需要的情况下会存储这个值。

image.png
对于privateUser来说,直接在主构造方法中声明这个属性,这个属性实现了来自于User的抽象属性,所以将其标记为override。对于subscribeingUser来说,name属性通过一个自定义getter实现,这个属性没有一个支持字段来存储它的值,它只有一个getter在每次调用时从email中得到昵称,对于FacebookUser来说,在初始化时把name和值关联,可通过id获取

除了抽象声明外,接口还可以包含具有getter和setter的属性,只要它们没有引用一个支持字段(支持字段需要在接口中存储状态,这是不允许的)

image.png
此时属性没有支持字段,结果值通过每次访问时计算得到。name属性有一个自定义getter,它可以被继承,而email必须重写。 在类中实现的属性具有对支持字段的完全访问权限

通过getter或setter访问支持字段

结合存储值的属性和具有自定义访问器在每次访问时计算值的属性实现一个即可存储值又可以在值被访问和修改时提供额外逻辑的属性。要支持这种情况,需要能从属性的访问器中访问它的支持字段

image.png

使用

image.png
可以像平常一样使用user.address="new value"来修改一个属性的值,这在底层调用了setter。在这里setter被重新定义了,所以打印了输出日志

在setter的函数体中使用了特殊的标识符field来访问支持字段的值,在getter中只能读取值,在setter读写都可

有支持字段的属性和没有的区别 若你显式引用或使用默认的访问器实现,编译器会为属性生成支持字段。若你提供一个自定义访问器实现并没有使用field(若属性是val类型,就是getter,若是可变属性,则是两个访问器),支持字段将不会被呈现出来

修改访问器的可见性

不需要修改访问器的默认实现,但需要修改它的可见性时 访问器的可见性默认与属性的可见性相同,但如果需要可以通过在get和set关键字前放置可见性修饰符的方式来修改它

image.png
这个类用来计算单词加在一起的总长度。持有总长度的属性是public的,因为它时这个类提供给用户的API的一部分。但你要确保它只能在类中被修改,否则外部代码有可能会修改它并存储一个不正确的值。所以你让编译器生成一个默认可见性的getter和一个private setter

编译器生成的方法:数据类和类委托

通用对象方法

1.字符串表示:toString()

kotlin中也提供了一种方式来获取类对象的字符串表示形式,默认来说一个对象的字符串表示形如Client@5e9f23b4,这不是很有用,需重写它

image.png

2.对象相等性:equals()

所有关于Client类的计算都发生在其外部,这个类只用于存储数据。尽管如此还是会偶一些针对这种类行为的需求,例如想要将包含相同数据的对象视为相等

image.png

is检查是java中instanceof的模拟,用来检查一个值是否为一个指定的类型

==表示相等性

在java中可以使用==来比较基本数据类型和引用类型,若应用在基本数据类型上则比较的是值,若应用在引用类型上则比较的是引用

在kotlin中==比较的是两个对象的默认方式,本质上说它就是通过调用equals来比较两个值的。如果equals在你的类中被重写了,就能安全地用==来比较实例。要想进行引用比较,可使用===运算符

3.Hash容器:hashCode()

hashCode方法通常与equals一起被重写

未重写时

image.png

image.png

原因是Client类缺少了hashCode方法,因此它违反了通用的hashCode契约:如果两对象相等,它们必须有着相同的hash值。hashset中值是以一种优化过的方式来比较的:首先比较它们的hash值,然后只有当它们相等时才会去比较真正的值。上一例子中两个不同的实例有着不同的hash值,所以set认为它不包含第二个对象,即使equal返回true,所以如果不遵循规则,hashset不能在这样的对象上正常工作

修正问题

image.png 此时在所有情境下都能按预期工作

数据类:自动生成通用方法的实现

如果想要你的泪是一个方便的数据容器,你要重写toString等三个方法时,在kotlin中不必去生成这些方法,我们可以直接为类添加data修饰符

image.png
现在我们得到了一个重写了所有标准java方法的类
equals用来比较实例
hashCode用来作为例如HashMap这种基于哈希容器的键
toStirng用来为类生成按声明顺序排列的所有字段的字符串表达形式

equals和hashCode方法会将所有在主构造方法中声明的属性纳入考虑。生成的equals会检测所有的属性的值是否相等。hashCode返回一个根据所有属性生成的哈希值

数据类和不可变性:copy()方法 虽然数据类的属性同样可以使用var,但强烈推荐只使用只读属性,让数据类的实例不可变。若你想使用这样的实例作为键,那这是必需要求。为了让使用不可变对象的数据变得更容易,编译器为它们多生成了一个方法:一个允许copy类的实例的方法,并在copy的同时修改某些属性的值。创建副本通常是修改实例的好选择,副本有着单独的生命周期且不会影响代码中引用原始实例的位置。

手动实现copy()

image.png

测试及结果

image.png

image.png

类委托:使用by关键字

当你扩展一个类并重写某些方法时,代码就变得依赖继承的那个类的实现细节了。当系统不断演进且基类的实现被修改或者新方法被提娜佳进去后,你做出的关于类行为的假设会失效,代码也许最后以不正确的行为告终。

kotlin为了解决这些问题把类默认视作final,这确保了只有那些设计成可扩展的类可以被继承,当使用这样的类时,会看见它是开放的,就会注意这些修改要与派生类兼容。但我们常常需要向其他类添加一些行为,即使它并没有被设计为可扩展的。一个常用的实现方式以装饰器模式文明。这种模式的本质就是创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个字段保存。与原始类拥有同样行为的方法不用被修改,只需要直接转发到原始类的实例,但这样需要相当多的样板代码。

而kotlin在什么时候实现一个接口,都可以用by关键字把接口的实现委托到另一个对象

未使用by关键字前

image.png 使用by关键字后

image.png
类中所有的方法实现都消失了,编译器会生成它们,并且实现与未使用前相似,所以没必要手写代码当编译器能帮你做同样事情时。当你要修改某些方法的行为时可以重写它们

object关键字:将声明一个类与创建一个实例结合起来

object关键字在多种情况下出现
1.对象声明是定义单例的一种方式
2.伴生对象可以持有工厂方法和其他与这个类相关,但在调用时并不依赖类实例的方法,它们的成员可以通过类名来访问
3.对象表达式用来替代java的匿名内部类

对象声明:创建单例

单例模式:定义一个使用provate构造方法并且用静态字段来持有这个类仅有的实例
kotlin通过对象生命与该类的单一实例声明结合到了一起

使用一个对象声明一个组织的工资单

image.png
对象声明通过object关键字引入,一个对象声明可以非常高效地以一句话来定义一个类和一个该类的变量

与类一样,一个对象声明也可以包含属性、方法、初始化语句块等的声明。唯一不允许的就是构造方法(包括主从构造方法)

与普通类实例不同,对象声明在定义的时候就立即创建了,不需要在代码的其他地方调用构造方法,因此为其定义一个构造方法是没有意义的

与变量一样,对象声明允许你使用对象加.字符的方式来调用方法和访问属性

image.png

对象声明同样可以继承自类和接口,这通常在你使用的框架需要去实现一个接口,但你的实现并不包含任何状态的时候很有用

单例和依赖注入

在大型软件系统中使用对象声明也并不总是理想的,它们在少部分只有少量依赖或没有依赖的代码中好用,但在与系统中其他部分有非常多交互的大型组件中却不然,因为你对对象实例化没有任何控制,并且不能通过构造方法指定特定的参数。这意味着你不能在单元测试或软件系统的不同配置中替换掉对象自身的实现,或对象依赖的其他类,如果你需要那种能力,需要将依赖注入框架如Guice

同样可以在类中声明对象,这样的对象同样只有一个单一实例:它们在每个容器类的实例中并不具有不同的实例,如:在类中放置一个用来比较特定对象的比较器

image.png

在java中使用kotlin对象

kotlin对象声明被编译成了通过静态字段来持有它的单一实例的类,这个字段名字始终是INSTANCE。要从java中访问kotlin对象可以通过访问静态的INSTANCE字段:Comparator.INSTANCE.compare(x1,x2);,字段的类型是Comparator

伴生对象:工厂方法和静态成员的地盘

kotlin类不能拥有静态成员,作为替代,kotlin依赖包级别函数(在大多数情况下能替代java的静态方法)和对象声明(在其他情况下替代java的静态方法,还包括静态字段)。在大多数情况下推荐使用顶层函数,但顶层函数不能访问类的private成员。如果你需要写一个可以在没有类实例的情况下调用但是需要访问类内部的函数,可以将其写成那个类中的对象声明的成员。这种函数的例子是工厂方法

在类中定义的对象之一可以使用companion关键字标记,这样做获得了直接通过容器类名称来访问这个对象的方法和属性的能力,不在需要显式指明对象的名称,最终的语法像java中静态方法的调用

image.png
伴生对象是调用private构造方法的好地方,它可以访问类中所有private成员,包括private构造方法,它是实现工厂模式的理想选择。

例子

image.png

使用工厂方法来替代构造方法

image.png
此时可以通过类名调用companion object的方法

image.png
在这里工厂方法能返回声明这个方法的类的子类,就像例子中的两个子类。你还可以在不需要的时候避免创建新的对象,你可以确保每一个email都与一个唯一的User实例对应,并且如果email在缓存中已经存在,那么在调用工厂方法时就会返回这个存在的实例而不是创建一个新的。如果你需要扩展这样的类,使用多个构造方法可能更好,因为伴生对象成员在子类中不能被重写

作为普通对象使用的伴生对象

伴生对象是一个生命在类中的普通对象,它可以有名字,实现一个接口或者扩展函数或者属性

声明一个命名了的伴生对象

image.png 在大多数情况下,通过包含伴生对象的类的名字引用伴生对象,所以不必关心它的名字,如果需要也可以指明如上图。如果省略了名字,默认分配为Companion

在伴生对象中实现接口

可以直接将包含它的类的名字当作实现了该接口的对象实例来使用。假定你的系统中有许多种对象,包括Person,你想要提供一个通用的方式来创建所有类型的对象。假设有一个JSONFactory接口可以从JSON反序列化对象,并且你的系统中的所有对象都通过这个工厂创建,可以为Person类提供这么一个接口实现

image.png

image.png
此时如果你有一个函数使用抽象方法来加载实体,可以传给它Person对象。

伴生对象扩展

如果你需要定义可以通过类自身调用的方法,就像伴生对象方法或者java静态方法该怎么办?如果类有一个伴生对象,可以通过在其上定义扩展函数来做到这一点。如果类C有一个伴生对象,并且在C.Companion上定义了一个扩展函数func,可以通过C.func()来调用它。

为伴生对象定义一个扩展函数

image.png 为了能够为你的类定义扩展,必须在其中声明一个伴生对象,即使是空的

对象表达式:改变写法的匿名内部类

object关键字不仅能用来声明单例式的对象,还能用来声明匿名对象。

使用匿名对象来实现事件监听器

image.png
除了去掉了对象的名字外,语法是与对象声明相同的。对象表达式声明了一个类并创建了该类的一个实例,但没有给对象分配名字

如果需要分配一个名字可以存储到一个变量中

image.png
kotlin的匿名对象可以实现多个接口或者不实现接口,并且它不是单例的,每次对象表达式被执行都会创建一个新的对象实例。在对象表达式中的代码可以访问创建它的函数中的变量,并且不限制在final变量,还可以在对象表达式中修改变量的值

从匿名对象访问局部变量

image.png
对象表达式在需要在匿名对象中重写多个方法时是最有用的

猜你喜欢

转载自juejin.im/post/7080194445030719502
今日推荐