定义一个接口:
package main.part4
interface Clickable {
fun click()
}
定义子类
package main.part4
class Button: Clickable {
override fun click() {
println("button be clicked")
}
}
kotlin在类名后使用冒号来代替了java的extends和implements关键字。和java一样,一个类可以实现任意多个接口,但只能继承一个类。
kotlin中,使用override修饰符是强制要求的。这会避免先写出实现方法再添加抽象方法造成的意外重写:你的代码将不能编译,除非你显式地把这个方法标注为override或者重命名它。
接口的方法可以有一个默认实现。与java8不同的是,java8中需要使用default关键字进行标注。而在kotlin中没有特殊的注解,只需要提供一个方法体。
java8实现方式:
package main.part4;
import static java.lang.System.out;
public interface ClickableJava {
default void click() {
out.println("click in java interface");
}
}
kotlin实现方式:
package main.part4
interface Clickable {
fun click()
fun showOff() = println("i'm clickable")
}
package main.part4
interface Focusable {
fun setFocus(b: Boolean) {
println("i ${if (b) "got" else "lose"} focus")
}
fun showOff() = println("i'm focusable")
}
如果需要在你的类中实现这两个接口会发生什么呢?它们每一个都包含了带默认实现的showOff方法,那么子类将会使用哪一个实现呢?答案是任何一个都不会使用,取而代之的是,如果你没有显式实现showOff,将会编译错误:
package main.part4
class Button: Clickable, Focusable {
override fun click() {
println("button be clicked")
}
}
Class 'Button' must override public open fun showOff(): Unit defined in main.part4.Clickable because it inherits multiple interface methods of it
编译器强制要求你提供自己的实现
package main.part4
class Button: Clickable, Focusable {
override fun click() {
println("button be clicked")
}
override fun showOff() {
//使用尖括号加上父类型名称的super表明想要调用哪一个父类的方法
super.showOff()
super.showOff()
}
}
要调用一个继承的实现,可以使用与java相同的关键字:super。但是选择一个特定实现的语法是不同的。在java中可以把基类的名称放在super关键字前面,就像Clickable.super.showOff()这样。在kotlin中需要把基类名称放在尖括号中:super
java中允许你创建任意类的子类并重写任意方法,除非你显式地使用final关键字进行标注。
对基类进行修改会导致子类不正确的行为,这就是所谓的脆弱的基类问题。
java的类和方法默认是open的,而kotlin中默认都是final的。
如果你想允许创建一个类的子类,需要使用open修饰符来标识这个类,此外,需要给每一个可以被重写的的属性或方法添加open修饰符。
package main.part4
//这个类是open的,其他类可以继承它
open class RichButton: Clickable {
//这个函数是final的,不能在子类中重写它
fun disable() {}
//这个函数是open的:可以在子类中重写它
open fun animate() {}
//这个函数重写了一个open函数,并且它本身同样是open的
override fun click() {}
}
注意,你重写了一个基类或者接口的成员,重写了的成员函数同样默认是open的。如果想改变这一行为,阻止你的类的子类重写你的实现,可以显式地将重写的成员标注为final。
class RichButton: Clickable {
final override fun click() {}
}
如果在类名前没有加open关键字,则不能让子类继承它。否则报错提示。
kotlin中也使用abstract声明一个抽象类,这种类是不能实例化的。一个抽象类通常包含一些没有实现并且必须在子类重写的抽象成员。抽象成员始终是open的,所以不需要显式地使用open修饰符。
package main.part4
//抽象类不能创建它的实例
abstract class Animated {
//这个函数时抽象的,它没有实现,必须被子类重写
abstract fun animate()
//抽象类中的非抽象方法并不是默认open的,但是可以标注为open的
open fun stopAnimating() {}
fun animateTwice() {}
}
在接口中,不能使用final、open或者abstract。接口中的成员始终是open的。不能将它声明为final。如果它没有函数体它就是abstract的,但是这个关键字并不是必须得。
可以看到,在接口中使用abstract关键字是冗余的,没必要的。
在接口中使用final关键字是不合适的。
下面是kotlin中类中访问修饰符的含义
总的来说,kotlin中的可见性修饰符与java中的类似。同样可以使用public、protected和private修饰符。但是默认的可见性是不一样的:如果省略了修饰符,声明就是public的。
java 中的访问控制
java中的默认可见性---包私有,在kotlin中并没有使用。kotlin只把包作为在命名空间里组织代码的一种方式使用,并没有将其用作可见性控制。
先来看看java的包私有。访问控制就是对访问权限的控制。java中的访问控制是通过在声明类、属性和方法时加上适当的访问控制修饰符来实现的。java中有四种访问控制级别,但只有三个访问控制修饰符,当不适用任何一种时,所得到的访问控制级别就是第四种—默认级别。对于属性和方法,所有四种访问控制级别都是适用的,但对于类的访问控制,只有默认访问和公开访问两种情况。
如果在类声明时没有加入任何访问修饰符,则表示该类具有默认访问控制级别。具有默认访问控制级别的类只能被同一个包中的类访问。
如果在类声明时加上了public访问修饰符,则表示该类具有公有访问控制级别。具有公有访问控制级别的类可以被所有类访问,而不管它们是否在同一个包中。如果使用的公有类与你正在编写的类位于不同的包中,需要使用import导入这个公有类。
如果类自身对于另一个类是不可见的,则即使将其成员都声明为public,也没有一个成员是可见的。因此只有类可见时,其各个成员的访问控制级别才有意义。
类成员(包括类的属性和方法)有4种访问控制级别,对应访问控制修饰符分别为:public、private、default和protected,其中default表示不加任意一个访问控制修饰符。
假设有文件目录结构如下:
Student和Teacher类分别定义为:
package main.part4.pack1;
class Student {
String name;
int age;
}
package main.part4.pack2;
class Teacher {
String name;
int age;
}
可以看到,Student和Teacher类声明时使用的都是默认访问控制级别。则在Clazz中分别使用Student和Teacher类时,效果如下,Clazz可以访问Student类,但不能访问Teacher类。
假如把Teacher类的包名进行修改,改成和Student的包名一致,Clazz类便不再报错了。
kotlin中的可见性
作为替代方案,kotlin提供了一个新的修饰符,internal,表示“只在模块内部可见”。一个模块就是一组一起编译的kotlin文件。这有可能是一个IDEA模块、一个Eclipse项目、一个Maven或Gradle项目或者一组使用调用Ant任务进行编译的文件。
internal可见性的优势在于它提供了对模块实现细节的真正封装。使用java时,这种封装性很容易被破坏,因为外部代码可以将类定义到与你代码相同的包中,从而得到访问你的包私有声明的权限。
另一个区别就是kotlin允许在顶层声明中使用private可见性,包括类、函数和属性。这些声明就只会在声明它们的文件中可见。这就是另外一种隐藏子系统实现细节的非常有用的方式。
注意, protected 修饰符在 Java Kotlin 中不同的行为。在 Java 中,可以从 同一个包中访问 一个protected 的成员,但是 Kotlin 不允许这样做。 Kotlin 可见性规则非常简单, protected 成员只在类和它的子类中可见。 同样还要注意的是,类的扩展函数不能访问它的 private和 protected 成员。
另一个kotlin和java之间可见性规则的区别就是,在kotlin中一个外部类不能看到其内部(或嵌套)类中的private成员。
像java一样,在kotlin中可以在另一个类中声明一个类。这样做在封装一个辅助类或者把一些代码放在靠近它使用的地方非常有用。区别是kotlin的嵌套类是不能访问外部类的实例(我理解的应该指的是this),除非你特别地做出了要求。
kotlin中没有显式修饰符的嵌套类与java中的static嵌套类是一样的。要把它变成一个内部类来持有一个外部类的引用的话需要使用inner修饰符。
为父类添加一个sealed修饰符,对可能创建的子类做出严格的限制。所有的直接子类必须嵌套在父类中。
//将基类标记为密封的
sealed class Expr {
//将所有可能的类作为嵌套类列出
class Num(val value: Int): Expr()
class Sum(val left: Expr, val right: Expr): Expr()
}
在when表达式中处理所有的sealed类的子类,就不再需要提供默认else分支了。
sealed修饰符隐含这个类是一个open的类。你不再需要显式地添加open修饰符。
当你在when中使用sealed类,并且添加一个新的子类的时候,有返回值的when表达式会导致编译失败,它会告诉你那里的代码必须修改。
声明一个带非默认构造方法或属性的类
在java中一个类可以声明为一个或多个构造方法。kotlin中也是类似的,知识做出了一些修改:区分主构造方法(通常是主要而简洁的初始化类的方法,并且在类体外部声明)和从构造方法(在类体内部声明)。同样也允许在初始化语句块中添加额外的初始化逻辑。
初始化类:主构造方法和初始化语句块
class User(val nickname: String)
这段被括号围起来的语句就叫做主构造方法。它主要有两个目的:表明构造方法的参数,以及定义使用这些参数的初始化的属性。
它等价于下面的写法:
//construct 带一个参数的主构造方法
class User constructor(_nickname: String) {
val nickname: String
//初始化语句块
init {
nickname = _nickname
}
}
关键字constructor用来开始一个主构造方法或从构造方法的声明。init关键字用来引入一个初始化语句块。这个初始化语句块包含了在类被创建时执行的代码,并会与主构造方法一起使用。因为主构造方法有语法限制,不能包含初始化代码,这就是为什么要使用初始化代码块的原因。如果您愿意,也可以在一个类中声明多个初始化代码块。
构造方法参数_nickname中的下划线用来区分属性的名称和构造方法参数的名字。另一个可选的方案是使用同样的名字,通过this来消除歧义,就像java中一样this.nickname=nickname。
上面的写法在编译器中会提示更优美的写法
点击join delearation and assignment即声明并初始化按钮后,自动变成
package main.part4
class User constructor(_username: String) {
private val username:String = _username
}
在这个例子中,不需要把初始化代码放在初始化语句中,因为它可以与nickname属性的声明结合。如果主构造方法没有注解或可见性修饰符,同样可以去掉关键字constructor关键字。
package main.part4
class User(_username: String) {
private val username:String = _username
}
如果属性用相应的构造方法参数来初始化,代码可以用过val关键字加在参数前的方式来进行简化。
package main.part4
class User(val username: String)
所有User类都是等价的,但是最后一个使用了简洁的语法。
可以像使用函数参数一样为构造方法参数声明一个默认值:
package main.part4
class User(val username: String, val isSubscribed: Boolean = true)
要创建一个类的实例,只需要直接调用构造方法,不需要使用new关键字:
val alice = User("Alice")
注意:如果所有的构造方法参数都有默认值,编译器会生成一个额外的不带参数的构造方法来使用所有的默认值。这可以使得kotlin使用库时变得更加简单,因为可以通过无参构造方法来实例化类。
如果你的类具有一个父类,主构造方法同样需要初始化父类。可以通过在基类列表的父类引用中提供父类构造方法参数的方式来做到这一点。
package main.part4
open class User (val username: String)
class TwitterUser(nickname: String): User(nickname)
注意TwitterUser的构造方法中的nickname参数前不需要再使用val了,因为nickname已经在父类中定义了,子类可以直接继承。TwitterUser的构造方法中的nickname参数仅仅代表一个参数,而没有声明一个属性。
如果没有给一个类声明任何构造方法,将会生成一个不做任何事情的默认构造方法:
open class Button
如果继承Button类并且没有提供任何的构造方法,必须显式地调用父类的构造方法,即使它没有任何的参数:
class RadioButton: Button()
这就是为什么在类名后面还需要一个空括号。注意与接口的区别:接口没有构造方法,所以在你实现一个接口的时候,不需要再父类型列表中它的名称后面加上括号。
如果你想确保你的类不被其他代码实例化,必须把构造方法标记为private。
class Secretive private constructor() {}
private构造方法的替代方案
在java中,可以通过使用private构造方法禁止实例化这个类来表示一个更通用的意思:这个类是一个静态实用工具成员的容器或者是单例。
在kotlin中针对这种目的具有内建语言级别的功能。可以使用顶层函数作为静态使用工具。要想表示单例,可以使用对象声明。
在大多数真实场景中,类的构造方法是非常简明的:它要么没有参数或者直接把参数与对应的属性关联。
构造方法:用不同的方式来初始化父类
通常来讲,使用多个构造方法的类在kotlin中不如java常见。大多数Java中需要重载构造方法的场景都被kotlin支持参数默认值和参数命名的语法涵盖了。
但是还是会有需要多个构造方法的场景。最常见的一种就是当你需要扩展一个框架类来提供多个构造方法,以便于通过不同的方式来初始化类的时候。
open class View {
constructor(ctx: Context) {}
constructor(ctx: Context, attr: AttributeSet) {}
}
如果你想扩展这个类,可以声明同样的构造方法:
class MyButton : View {
constructor(ctx: Context): super(ctx) {}
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {}
}
使用super关键字调用了父类对应的构造方法。就像java一样,也可以使用this关键字从一个构造方法中调用你自己类的另一个构造方法。
class MyButton : View {
//委托给另一个构造方法
constructor(ctx: Context): this(ctx, MY_STYLE) {}
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {}
}
如果类没有主构造方法,那么每个从构造方法必须初始化基类或者委托给另外一个这样做了初始化的构造方法。
java的互操作性是你需要使用从构造方法的主要使用场景。但是还有另外一个可能的情况:当你使用不同的参数列表,以多种方法创建类的实例时,使用不同的参数列表。
实现在接口中声明的属性
在kotlin中,接口可以包含抽象属性。
interface User {
val nickname: String
}
这意味着实现User接口的类需要提供一个取得nickname值的方式。接口并没有说明这个值应该存储到一个支持字段还是通过getter来获取。接口本身并不包含任何状态,因此只有实现这个接口的类在需要的情况下会存储这个值。
//主构造方法属性
class PrivateUser(override val nickname: String): User
class SubscribingUser(val email: String): User {
override val nickname: String
get() = email.substringBefore('@') //自定义getter
}
class FacebookUser(val accountId: Int): User {
//属性初始化
override val nickname = getFacebookname(accountId)
}
这些都是User接口的实现类。PrivateUser表示只填写了昵称的用户;SubscribingUser表示显然被迫提供email注册的用户;FacebookUser表示共享了Facebook账号的用户。所有的类都以不同方式实现了接口中的抽象属性。
对于SubscribingUser来说,nickname属性通过一个自定义getter实现。这个属性没有一个支持字段来存储它的值,它只是一个getter在每次调用时从email中得到昵称。
对于FacebookUser来说,在初始化时讲nickname属性与值关联。你使用了被认为可以通过账号ID返回facebook用户昵称的getFacebookName函数。这个函数开销巨大:它要与facebook建立连接来获取想要的数据。这也就是为什么你决定只在初始化阶段调用一次的原因。
请注意nickname在SubscribingUser和FacebookUser中的不同实现。即使它们看起来很相似,然而FacebookUser中的属性有一个支持字段来存储在类初始化时计算得到的数据。
除了抽象属性声明外,接口还可以包含具有getter和setter的属性,只要他们没有引用一个支持字段(支持字段需要在接口中存储状态,而这是不允许的)
interface User {
val email: String
//属性没有支持字段:结果值在每次访问时通过计算得
val nickname: String
get() = email.substringBefore('@')到
}
这个接口包含抽象属性email,同时nickname属性有一个自定义的getter。第一个属性必须在子类中重写,而第二个是可以被继承的。
通过getter或setter访问支持字段
kotlin有三种属性:存储值的属性 、 具有自定义访问器在每次访问时计算值的属性 和 结合前两种情况的一个既可以存储值又可以在值被访问和修改时提供额外逻辑的属性。要支持第三种属性,需要能够从属性访问器中访问它的支持字段。
class User1(val name: String) {
var address: String = "inspecified"
set(value) {
println("""
Address was changed for $name:
"$field" -> "$value".
""".trimIndent())
field = value
}
}
fun main() {
val user = User1("Alice")
user.address = "Qinhou"
}
在setter的函数体中,使用了特殊的标记符field来访问支持字段的值。在getter中,只能读取值(我尝试了发现其实可以修改值);在setter中,技能读取也能修改值。
注意,可以只重定义可变属性的一个访问器,如果getter是默认的并且只返回字段的值,所以没必要重定义它。
修改访问器的可见性
访问器的可见性默认与属性的可见性相同。但是如果需要可以通过在get和set关键字前放置可见性修饰符的方式来修改它。
class LehgthCounter {
var counter: Int = 0
private set //不能在外部修改这个属性
fun addWord(word: String) {
counter += word.length
}
}
这个类用来计算单词加在一起的总长度。持有总长度的属性是public的,因为它是这个类提供给用户的api的一部分。但是你需要保证它只能在类中被修改,否则外部代码有可能修改它并存储一个不正确的值。因此,你让编译器生成一个默认可见性的getter方法,并且将setter的可见性修改为private。
属性的更多话题:
1、在非空属性上使用的lateinit修饰符表明这个属性会将初始化推迟到构造方法被调用过后,这是一些框架的常用用法。
2、惰性初始化属性,作为更通用的委托属性的一部分,为了与java框架的兼容,可以在kotlin中使用注解来模仿java的功能;
3、const修饰符使得注解更加方,并且允许使用基本数据类型或者String的属性作为注解参数。
更多有关构造函数的知识点总结可参考文章:https://www.jianshu.com/p/0dcad0c9d708
==表示相等性
在java中,可以使用==运算符来比较基本数据类型和引用类型。如果应用在基本数据类型上,java的==比较的是值,然而在引用类型上==比较的是引用。 因此,在 Java 中,众所周知的实践是总是调用equals,如果忘记了这样做当然也会导致众所周知的问题。
在kotlin中,==运算符是比较两个对象的默认方式:本质上说它就是通过调用equals来比较两个值的。因此,如果equals在你的类中被重写了,你能够很安全地使用==来比较实例。要想进行引用比较,可以使用===运算符,这与java中的==比较对象的效果是一模一样的。
Hash容器:hashCode()
hashCode方法通常与equals一起被重写。
val processed = hashSetOf(Client("Alice", 342562))
println(processed.contains(Client("Alice", 342562)))
上面的代码你期望返回的结果为true,但实际返回的是false。
原因就是Client类缺少了hashCode方法。因为它违反了通用的hashCode契约:如果两个对象相等,它们必须拥有相同的hash值。processed是一个HashSet。在HashSet中值是以一种优化过的方式来比较的:首先比较它们的hash值,然后只有当它们相等时才会去比较真正的值。上面的代码中Client类的两个不同的实例有着不同的hash值,所以set会认为它不包含第二个对象,即使equals会返回true。因此,如果不遵循规则,HashSet不能在这样的对象上正常工作。
要修复这个问题,可以向类中添加hashcode的实现
数据类:自动生成通用方法的实现
如果你的类添加了data修饰符,必要的方法编译器将为你自动生成好。
data class Client(val name: String, val postalCode)
现在就得到了一个重写了所有标准java方法的类:
equals 用来比较实例
hashCode 用来作为例如HashMap这种基于哈希容器的键
toString 用来为类生成按声明顺序排列的所有字段的字符串表达式
equals和hashCode方法会将所有在主构造方法中声明的属性纳入考虑。生成的equals方法会检测所有的属性值是否相等。hashCode方法会返回一个根据所有属性生成的哈希值。请注意没在主构造器方法中声明的属性将不会加入到相等性检查和哈希值计算中。
数据类和不可变性:copy()方法
请注意,虽然数据类的属性并没有要求是val——同样可以使用var——但还是强烈推荐只使用val,让数据类的实例不可变。如果你想使用这样的实例来作为HashMap或者类似容器的键,这会是必须的要求,因为如果不这样,被用作键的对象在加入容器后被修改了,容器可能会进入一种无效的状态。
不可变对象同样更容易理解,特别是在多线程代码中:一旦一个对象被创建出来,它会一直保持初始状态,也不用担心在你的代码中工作时其他线程修改了对象的值。
为了让使用不可变对象的数据类变得更加容易,kotlin编译器为它们多生成了一个方法:一个允许copy类的实例的方法,并在copy的同时修改某些属性的值。创建副本通常是修改实例的好选择:副本有着单独的生命周期而且不会影响代码中引用原始实例的位置。
class Client(val name: String, val postalCode: Int) {
...
fun copy(name: String = this.name, postalCode: Int = this.postCode)
= Client(name, postalCode)
}
类委托使用"by"关键字
常常遇到一种情况,你需要向其他类添加一些行为,即使它并没有被设计为可扩展的。一个常用的实现方式以装饰器模式闻名。这种模式的本质就是创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个字段保存。与原始类拥有同样行为的方法不用被修改,只需要直接换发到原始类的实例。
这种方式的一个缺点是需要相当多的样板代码。例如,下面就是你需要多少代码来实现一个简单如Collection的接口的装饰器,即使你不需要修改任何的行为:
好消息是kotlin将委托作为一个语言级别的功能做了头等支持。无论什么时候实现一个接口,你都可以使用by关键字将接口的实现委托到另一个对象。下面就是怎样通过推荐的方式来重写前面的例子:
class DelegatingCollection(innerList: Collection = ArrayList()
): Collection by innerList {}
类中所有的方法实现都消失了。编译器会生成它们,并且实现与DelegatingCollection的离职是相似的。因为代码中没有太多有意思的内容,所以当编译器能够自动为你做同样事情的时候就没有必要去手写这些代码。
现在,当你需要修改某些方法的行为时,你可以重写它们,这样你的方法就会被调用而不是使用生成的方法。可以保留感到满意的委托给内部的实例中的默认实现。
来看看怎样使用这种技术来实现一个集合:它可以计算向它添加元素的尝试次数。
正如你所见,通过重写add和addAll方法来计数,并将MutableCollection接口剩下的实现委托给被你包装的容器。
重要的部分是你没有对底层集合的实现方式引入任何的依赖。例如你不用关心集合是不是通过在循环中调用add来实现的addAll。
kotlin中object关键字在多种情况下出现。但是它们都遵循同样的核心理论:这个关键字定义一个类并同时创建一个实例(换句话说就是一个对象)。来看看使用它们的不同场景:
对象声明是定义单例的一种方式;
伴生对象可以持有工厂方法和其他与这个类相关,但是调用时并不依赖类实例的方法。它们的成员可以通过类名来访问;
对象表达式用来替代java的匿名内部类;
在面向对象系统设计中一个相当常见的情形就是只需要一个实例的类。在java中,这通常通过单例模式来实现:定义一个使用private构造方法并且使用静态关键字段来持有这个类仅有的实例。
kotlin通过使用对象声明功能为这一切提供了最高级别的语言支持。对象声明将类声明与该类的单一实例声明结合到了一起。
object Payroll {
val allEmployee = arrayListOf()
fun calculateSalary() {
...
}
}
对象声明通过object关键字引入。
与类一样,一个对象声明也可以包括属性、方法、初始化语句块等的声明。唯一不允许的就是构造方法(包括主、从构造方法)。与普通类的实例不同,对象声明在定义的时候就立即创建了,不需要在代码的其他地方调用构造方法。因此,为对象声明定义一个构造方法是没有意义的。
与变量一样,对象声明允许你使用对象名加.字符的方式来调用方法和访问属性:
Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
对象声明同样可以继承自类和接口。这通常在你使用的框架需要去实现一个接口,但是你的实现并不包含任何状态的时候很有用。例如,java.util.Comparator接口。Comparator的实现接收两个对象并返回一个整数来表示哪个对象更大。比较器通常来说都不存储任何数据,所以通常只需要一个单独的Comparator实例以特定的方式比较对象。这是一个非常完美的对象声明的使用场景。
同样可以在类中声明对象。这样的对象同样只有一个单一实例:它们在每一个容器类的实例中并不具有不同的实例。例如,在类中放置一个用来比较特定对象的比较器是否是合理的。
kotlin中的对象声明被编译成了通过静态字段来持有它的单一实例的类,这个字段名称始终都是INSTANCE。如果在java中实现单例模式,你也许会顺手做同样的事情。因此,要从java代码使用kotlin对象,可以通过访问静态的INSTANCE字段:
CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2)
在这个例子中,INSTANCE字段的类型是CaseInsensitiveFileComparator。
kotlin中的类不能拥有静态成员:Java的static关键字并不是kotlin语言的一部分。作为替代,kotlin依赖包级别函数(在大多数情形下能够替代java的静态方法)和对象声明(在其他情况下替代java的静态方法,同时还包括静态字段)。
大多数情况下,还是推荐使用顶层函数。但是顶层函数不能访问类的private成员,因此如果你需要写一个可以在没有类实例的情况下调用但是需要访问类内部的函数,可以将其写成那个类中的对象声明的成员。这种函数的一个例子就是工厂方法。
在类中对应的对象之一可以使用一个特殊的关键字来标记:companion。如果这样做,就获得了直接通过容器类名称来访问这个对象的方法和属性的能力,不再需要显式地指明对象的名称。
还记得我们承诺过有一个调用private构造方法的好地方吗?那就是伴生对象。
伴生对象可以访问类中所有的private成员,包括private构造方法,它是实现工厂模式的理想选择。
来看看声明了两个构造方法的例子并将其改造成使用在伴生对象中声明的工厂方法。
表示相同逻辑的另一种方法,就是使用工厂方法来创建类的实例,这有很多方面的好处。User实例就是通过工厂方法创建的,而不是通过多个构造方法。
工厂方法是非常有用的。它们可以根据他们的用途来命名。此外,工厂方法能够声明这个方法的类的子类。你还可以在不需要的时候避免创建新的对象(下面实践代码跟这个有出入啊??)。例如,你可以确保每一个email都与一个唯一的User实例对应,并且如果email在缓存中已经存在,那么调用工厂方法时就返回这个存在的实例而不是创建一个新的。
但是如果你需要扩展这样的类,使用多个构造方法也许是一个更好的方案,因为伴生对象成员在子类中不能被重写。
伴生对象是一个声明在类中的普通对象。它可以有名字,实现一个接口或者有扩展函数或属性。
在大多数情况下,通过包含伴生对象的类的名字来引用伴生对象,所以不必关心它的名字。但是如果需要你也可以指明,如同上面代码。如果你省略了伴生对象的名字,默认的名字将会分配给Companion。
1、在伴生对象中实现接口
就像其他对象声明一样,伴生对象也可以实现接口。正如你即将看到的,可以直接将包含它的类的名字当做实现了该接口的对象实例来使用。
假定你的系统中有许多对象,包括Person,你想要提供一个通用的方式来创建所有类型的对象。假如你有一个JSONFactory接口可以从JSON反序列化对象,并且你的系统中所有对象都通过这个工厂来创建。你可以为你的Person类提供一个这种接口的实现。
这时,如果你有一个函数使用抽象方法来加载实体,可以传给它Person对象。
注意:Person类的名称被当作JSONFactory的实例。
2、伴生对象扩展
扩展函数允许你定义可以通过代码库中其他地方定义的类实例调用的方法。但是如果你需要定义可以通过类自身调用的方法,就像伴生对象方法或者java静态方法该怎么办?如果类有一个伴生对象,可以通过在其上定义扩展函数来做到这一点。具体来说,如果类C有一个伴生对象,并且在C.Companion上定义了一个扩展函数func,可以通过C.func()来调用它。
例如,假设你希望你的Person类有一个清晰的关注点分离,这个类本身会是核心业务逻辑模块的一部分,但是你并不想将这个模块与任何特定的数据格式耦合起来。正因为如此,反序列化函数需要定义在模块中用来负责客户端/服务端通信。可以使用扩展函数来做到这一点。注意,你该怎样使用默认名称(Companion)来引用没有显式地定义名字的伴生对象。
你调用fromJSON就好像它是一个伴生对象定义的方法一样,但实际上它是作为扩展函数在外部定义的。正如之前的扩展函数一样,看起来像是一个成员,但实际上并不是。但是请注意,为了能够为你的类定义扩展,必须在其中声明一个伴生对象,即使是一个空的。
object关键字不仅仅能用来声明单例式的对象,还能用来声明匿名对象。匿名对象代替了java中匿名内部类的用法。
除了去掉了对象的名字外,语法与对象声明相同。对象表达式声明了一个类并创建了该类的一个实例,但是并没有给这个类或者实例分配一个名字。通常来说,它们都不需要名字的,因为你会将这个对象用作一个函数调用的参数。如果你需要给对象分配一个名字,可以将其存储到一个变量中:
与java匿名内部类只能扩展一个类或者实现一个接口不同,kotlin的匿名对象可以实现多个接口或者不实现接口。
注意:与对象声明不同的是,匿名对象不是单例的。每次对象表达式被执行都会创建一个新的对象实例。
与java的匿名类一样,在对象表达式中的代码可以访问创建它的函数中的变量。但与java不同,访问并没有被限制在final变量,还可以在对象表达式中修改变量的值。