“object” 关键字:将声明一个类与创建一个实例结合起来
Kotlin中的object关键字在多种情况下出现,但是他们都遵循同样的核心理念:这个关键字定义一个类并同时创建一个实例(换句话说就是一个对象)让我们看看使用场景:
- 对象声明是定义单例的一种方式
- 伴生对象可以持有工厂方法和其他与这个类有关,但在调用时并不依赖类实例的方法。它们的成员可以通过类名来访问
- 对象表达式用来替代Java的匿名内部类
1 对象声明:创建单例易如反掌
在面向对象系统设计中一个相当常见的情形只需要一个实例的类。在Java中,这通常通过单例模式来实现,定义一个使用private构造方法并且用静态字段来持有这个类的实例。
在Kotlin中,使用对象声明功能为这一切提供了更高级的语言支持。对象声明将类声明与类的单一实例声明结合到了一起。
例如,可以使用一个对象声明来表示一个公司的工资单,你也许不会有多个工资单,所以用一个对象来表示看起来是明智的。
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary() { // 展示所有员工工资单
for (people in Payroll.allEmployees) {
println("name:${people.name} salary;${people.salary}")
}
}
}
Payroll.allEmployees.add(Person("klaus1", 500))
Payroll.allEmployees.add(Person("klaus2", 600))
Payroll.allEmployees.add(Person("klaus3", 700))
Payroll.calculateSalary()
>>>
name:klaus1 salary;500
name:klaus2 salary;600
name:klaus3 salary;700
- 一个对象声明可以非常高效地以一句话来定义一个类和一个该类的变量
- 与类一样,一个对象声明也可以包含属性、方法、初始化语句块等声明。唯一不允许的就是构造方法。
- 与普通的实例不同,对象声明在定义的时候就立即创建了,不需要在代码的其他地方调用构造方法。所以为对象声明定义构造方法是没有意义的
对象声明同样可以继承自类和接口,这通常在你使用的框架需要去实现一个接口,并且你的实现并不包含任何状态的时候很有用,比如实现比较器Comparator。比较器通常来说都不存储任何数据,所以通常只需要一个单独Comparator实例来以特定的方式比较对象。这是一个非常完美的对象声明使用场景
object CaseInsensitiveStringComparator : Comparator<String> {
override fun compare(o1: String, o2: String): Int {
return o1.compareTo(o2)
}
}
println(CaseInsensitiveStringComparator.compare("c","b"))
val listString = listOf<String>("a","c","b","g","f")
val sortedWith = listString.sortedWith(CaseInsensitiveStringComparator)
println(sortedWith)
sortWith函数,返回一个比较器比较过的列表
使用嵌套类实现Comparator
data class Person(val name: String, val salary: Int) {
object SalaryComparator : Comparator<Person> {
override fun compare(o1: Person, o2: Person): Int {
return o1.salary.compareTo(o2.salary)
}
}
}
val persons = listOf<Person>(Person("klaus", 300),Person("Bob", 100), Person("Alice", 200))
println(persons.sortedWith(Person.SalaryComparator))
>> [Person(name=Bob, salary=100), Person(name=Alice, salary=200), Person(name=klaus, salary=300)]
在Java中使用Kotlin对象
Kotlin中的对象声明被编译成了通过静态字段来持有它的单一实例,这个字段始终是INSTANCE,
Java中访问
CaseInsensitiveStringComparator.INSTANCE.compare(str1,str2)
2 伴生对象:工厂方法和静态成员的地盘
Kotlin中的类不能拥有静态成员:Java的static关键字并不是Kotlin语言的一部分。作为替代,Kotlin依赖包级别函数(在大多数情形下能够替代Java的静态方法)和对象声明(在其他情况下替代Java的静态方法,同时还包括静态字段)。
在大多数情况下,还是推荐使用顶层函数。单是顶层函数不能访问类的private成员。
在类中定义对象之一可以使用一个特殊的关键字来标记:companion 如果这样做,就获得了直接通过容器类名称来访问这个对象的方法和属性的能力,不再需要显式地指明对象的名称,最终的语法看起来就像在Java中调用静态方法一样
class A {
companion object {
fun bar() {
println("companion object called!")
}
}
}
A.bar()
定义一个拥有多个从构造方法的类
class User {
val nickname: String
constructor(email: String) { //从构造方法
nickname = email.substringBefore('@')
}
constructor(facebookID: Int) { //从构造方法
nickname = getFacebookName(facebookID)
}
}
表示相同逻辑的另一种方法,就是使用工厂方法来创建类的实例,这有多方面的好处。User实例就是通过工厂方法创建的,而不是通过多个构造方法。
class User private constructor(val nickname: String) {
companion object {
fun newSubcribingUser(email: String) = User(email.substringBefore('@'))
fun newFacebookUser(facebookID:Int) = User(getFacebookName(facebookID))
}
}
val user1 = User.newFacebookUser("klaus@qq.com")
val user2 = User.newSubcribingUser(1)
3 作为普通对象使用的伴生对象
伴生对象是一个声明在类中的普通对象。它可以有名字,实现一个接口或者扩展函数或属性
声明一个命名伴生对象
class User(val name: String) {
companion object GsonUtil {
fun fromJson(jsonString: String) = Gson().fromJson<User>(jsonString, User::class.java)
}
}
val user = User.GsonUtil.fromJson("{name:'klaus'}")
//val user = User.fromJson("{name:'klaus'}")
println(user.name)
>> klaus
在大多数情况下通过类名来引用伴生对象,所以不用关心它的名字。但是也可以指明。如果你省略了名字,默认的名字将会是Companion。接下来当我们讲到伴生对象扩展时,你将会看到使用这名字的一些例子。
在伴生对象中实现接口
interface JSONFactory<T> {
fun fromJson(jsonString: String): T
}
class User(val name: String) {
companion object GsonUtil : JSONFactory<User> { //实现接口的伴生对象
override fun fromJson(jsonString: String) = Gson().fromJson<User>(jsonString, User::class.java)
}
}
这时 定义一个函数
fun loadFromJSON(factory: JSONFactory<User>,jsonStr:String): User {
return factory.fromJson(jsonStr)
}
val user = loadFromJSON(User, "{name:'klaus'}")
println(user.name)
>> klaus
注意 ,User类的名字被当作JSONFactory的实例。
在Java中如何调用伴生对象,如果伴生对象没有命名
User.Companion.fronJson(""),
如果有名字,那就用名字代替Companion
伴生对象扩展
就像在3.3节看到的那样,扩展函数允许你定义代码库中其他地方定义的方法,但是如果你想定义通过类自身调用的方法,就像伴生对象或者Java静态方法该怎么办?如果类有一个伴生对象,可以通过在其上定义扩展函数来做到这一点。具体来说,如果类C有一个伴生对象,并且在C.Companion上定义了一个扩展函数func,那么可以通过C.func()来调用。
/**
* 扩展User的伴生对象,可以直接用类名调用
* 伴生对象名字是GsonUtil,如果没有命名,则用Companion代替
*/
fun User.GsonUtil.toJson(user: User) = Gson().toJson(user)
val jsonStr = User.toJson(User("Alice"))
println(jsonStr)
>> {"name":"Alice"}
你调用toJson方法,就好像它是一个伴生对象定义的方法一样,但是实际上它是作为扩展函数在外部定义的。,如果你想为你的类定义扩展,必须在其中定义一个伴生对象,即使它是空的。
4 对象表达式:改变写法的匿名内部类
object 关键字不仅仅能用来声明单例式的对象,还能用来声明匿名对象。匿名对象替代了Java中匿名内部类的用法。例如,让我们来看看怎样将一个典型的Java匿名内部类用法-事件监听-转换成Kotlin:
实现一个按钮的点击事件
button.setOnClickListener(
object : View.OnClickListener { //声明一个继承自OnClickListener的匿名对象
override fun onClick(v: View?) {
TODO("not implemented")
}
}
)
除了去掉对象的名字外,语法和对象声明相同。
对象表达式声明了一个类并创建了改类的一个实例,但是并没有给这个实例分配名字。通常来说它是不需要名字的,因为你会将这个对象作为一个参数来使用。如果你需要给对象分配名字,可以将其存储到一个变量中:
val onClickListener = object : View.OnClickListener{
override fun onClick(v: View?) {
TODO("not implemented")
}
}
与Java匿名内部类不同,Kotlin匿名对象可以实现多个接口或者不实现接口。
注意:与对象声明不同,匿名对象不是单例的。每次对象表达式被执行都会创建一个新的对象实例。
匿名内部类访问局部变量
与Java不同,访问局部变量并没有被限制在final变量,还可以修改变量的值。例如:记录按钮的点击次数。
var clickCount: Int = 0
button.setOnClickListener(
object : View.OnClickListener {
override fun onClick(v: View?) {
clickCount++
}
}
)