7.9 单例模式(Singleton)与伴生对象(companion object)

7.9.1 单例模式(Singleton)

单例模式很常用。它是一种常用的软件设计模式。例如,Spring中的Bean默认就是单例。通过单例模式可以保证系统中一个类只有一个实例。即一个类只有一个对象实例。

我们用Java实现一个简单的单例类的代码如下:

  1. class Singleton {
  2. private static Singleton instance;
  3. private Singleton() {}
  4. public static Singleton getInstance() {
  5. if (instance == null) {
  6. instance = new Singleton();
  7. }
  8. return instance;
  9. }
  10. }

测试代码:

  1. Singleton singleton1 = Singleton.getInstance();

可以看出,我们先在单例类中声明了一个私有静态的Singleton instance变量,然后声明一个私有构造函数private Singleton() {}, 这个私有构造函数使得外部无法直接通过new的方式来构建对象:

  1. Singleton singleton2 = new Singleton(); //error, cannot private access

最后提供一个public的获取当前类的唯一实例的静态方法getInstance()。我们这里给出的是一个简单的单例类,是线程不安全的。

7.9.2 object对象

Kotlin中没有 静态属性和方法,但是也提供了实现类似于单例的功能,我们可以使用关键字 object 声明一个object对象:

  1. object AdminUser {
  2. val username: String = "admin"
  3. val password: String = "admin"
  4. fun getTimestamp() = SimpleDateFormat("yyyyMMddHHmmss").format(Date())
  5. fun md5Password() = EncoderByMd5(password + getTimestamp())
  6. }

测试代码:

  1. val adminUser = AdminUser.username
  2. val adminPassword = AdminUser.md5Password()
  3. println(adminUser) // admin
  4. println(adminPassword) // g+0yLfaPVYxUf6TMIdXFXw==,这个值具体运行时会变化

为了方便在REPL中演示说明,我们再写一个示例代码:

  1. >>> object User {
  2. ... val username: String = "admin"
  3. ... val password: String = "admin"
  4. ... }

object对象只能通过对象名字来访问:

  1. >>> User.username
  2. admin
  3. >>> User.password
  4. admin

不能像下面这样使用构造函数:

  1. >>> val u = User()
  2. error: expression 'User' of type 'Line130.User' cannot be invoked as a function. The function 'invoke()' is not found
  3. val u = User()
  4. ^

为了更加直观的了解object对象的概念,我们把上面的object User的代码反编译成Java代码:

  1. public final class User {
  2. @NotNull
  3. private static final String username = "admin";
  4. @NotNull
  5. private static final String password = "admin";
  6. public static final User INSTANCE;
  7. @NotNull
  8. public final String getUsername() {
  9. return username;
  10. }
  11. @NotNull
  12. public final String getPassword() {
  13. return password;
  14. }
  15. private User() {
  16. INSTANCE = (User)this;
  17. username = "admin";
  18. password = "admin";
  19. }
  20. static {
  21. new User();
  22. }
  23. }

从上面的反编译代码,我们可以直观了解Kotlin的object背后的一些原理。

7.9.3 嵌套(Nested)object对象

这个object对象还可以放到一个类里面:

  1. class DataProcessor {
  2. fun process() {
  3. println("Process Data")
  4. }
  5. object FileUtils {
  6. val userHome = "/Users/jack/"
  7. fun getFileContent(file: String): String {
  8. var content = ""
  9. val f = File(file)
  10. f.forEachLine { content = content + it + "\n" }
  11. return content
  12. }
  13. }
  14. }

测试代码:

  1. DataProcessor.FileUtils.userHome // /Users/jack/
  2. DataProcessor.FileUtils.getFileContent("test.data") // 输出文件的内容

同样的,我们只能通过类的名称来直接访问object,不能使用对象实例引用。下面的写法是错误的:

  1. val dp = DataProcessor()
  2. dp.FileUtils.userHome // error, Nested object FileUtils cannot access object via reference

我们在Java中通常会写一些Utils类,这样的类我们在Kotlin中就可以直接使用object对象:

  1. object HttpUtils {
  2. val client = OkHttpClient()
  3. @Throws(Exception::class)
  4. fun getSync(url: String): String? {
  5. val request = Request.Builder()
  6. .url(url)
  7. .build()
  8. val response = client.newCall(request).execute()
  9. if (!response.isSuccessful()) throw IOException("Unexpected code " + response)
  10. val responseHeaders = response.headers()
  11. for (i in 0..responseHeaders.size() - 1) {
  12. println(responseHeaders.name(i) + ": " + responseHeaders.value(i))
  13. }
  14. return response.body()?.string()
  15. }
  16. @Throws(Exception::class)
  17. fun getAsync(url: String) {
  18. var result: String? = ""
  19. val request = Request.Builder()
  20. .url(url)
  21. .build()
  22. client.newCall(request).enqueue(object : Callback {
  23. override fun onFailure(call: Call, e: IOException?) {
  24. e?.printStackTrace()
  25. }
  26. @Throws(IOException::class)
  27. override fun onResponse(call: Call, response: Response) {
  28. if (!response.isSuccessful()) throw IOException("Unexpected code " + response)
  29. val responseHeaders = response.headers()
  30. for (i in 0..responseHeaders.size() - 1) {
  31. println(responseHeaders.name(i) + ": " + responseHeaders.value(i))
  32. }
  33. result = response.body()?.string()
  34. println(result)
  35. }
  36. })
  37. }
  38. }

测试代码:

  1. val url = "http://www.baidu.com"
  2. val html1 = HttpUtils.getSync(url) // 同步get
  3. println("html1=${html1}")
  4. HttpUtils.getAsync(url) // 异步get

7.9.4 匿名object

还有,在代码行内,有时候我们需要的仅仅是一个简单的对象,我们这个时候就可以使用下面的匿名object的方式:

  1. fun distance(x: Double, y: Double): Double {
  2. val porigin = object {
  3. var x = 0.0
  4. var y = 0.0
  5. }
  6. return Math.sqrt((x - porigin.x) * (x - porigin.x) + (y - porigin.y) * (y - porigin.y))
  7. }

测试代码:

  1. distance(3.0, 4.0)

需要注意的是,匿名对象只可以用在本地和私有作用域中声明的类型。代码示例:

  1. class AnonymousObjectType {
  2. // 私有函数,返回的是匿名object类型
  3. private fun privateFoo() = object {
  4. val x: String = "x"
  5. }
  6. // 公有函数,返回的类型是 Any
  7. fun publicFoo() = object {
  8. val x: String = "x" // 无法访问到
  9. }
  10. fun test() {
  11. val x1 = privateFoo().x // Works
  12. //val x2 = publicFoo().x // ERROR: Unresolved reference 'x'
  13. }
  14. }
  15. fun main(args: Array<String>) {
  16. AnonymousObjectType().publicFoo().x // Unresolved reference 'x'
  17. }

跟 Java 匿名内部类类似,object对象表达式中的代码可以访问来自包含它的作用域的变量(与 Java 不同的是,这不限于 final 变量):

  1. fun countCompare() {
  2. var list = mutableListOf(1, 4, 3, 7, 11, 9, 10, 20)
  3. var countCompare = 0
  4. Collections.sort(list, object : Comparator<Int> {
  5. override fun compare(o1: Int, o2: Int): Int {
  6. countCompare++
  7. println("countCompare=$countCompare")
  8. println(list)
  9. return o1.compareTo(o2)
  10. }
  11. })
  12. }

测试代码:

  1. countCompare()
  2. countCompare=1
  3. [1, 4, 3, 7, 11, 9, 10, 20]
  4. ...
  5. countCompare=17
  6. [1, 3, 4, 7, 9, 10, 11, 20]

7.9.5 伴生对象(companion object)

Kotlin中还提供了 伴生对象 ,用companion object关键字声明:

  1. class DataProcessor {
  2. fun process() {
  3. println("Process Data")
  4. }
  5. object FileUtils {
  6. val userHome = "/Users/jack/"
  7. fun getFileContent(file: String): String {
  8. var content = ""
  9. val f = File(file)
  10. f.forEachLine { content = content + it + "\n" }
  11. return content
  12. }
  13. }
  14. companion object StringUtils {
  15. fun isEmpty(s: String): Boolean {
  16. return s.isEmpty()
  17. }
  18. }
  19. }

一个类只能有1个伴生对象。也就是是下面的写法是错误的:

  1. class ClassA {
  2. companion object Factory {
  3. fun create(): ClassA = ClassA()
  4. }
  5. companion object Factory2 { // error, only 1 companion object is allowed per class
  6. fun create(): MyClass = MyClass()
  7. }
  8. }

一个类的伴生对象默认引用名是Companion:

  1. class ClassB {
  2. companion object {
  3. fun create(): ClassB = ClassB()
  4. fun get() = "Hi, I am CompanyB"
  5. }
  6. }

我们可以直接像在Java静态类中使用静态方法一样使用一个类的伴生对象的函数,属性(但是在运行时,它们依旧是实体的实例成员):

  1. ClassB.Companion.index
  2. ClassB.Companion.create()
  3. ClassB.Companion.get()

其中, Companion可以省略不写:

  1. ClassB.index
  2. ClassB.create()
  3. ClassB.get()

当然,我们也可以指定伴生对象的名称:

  1. class ClassC {
  2. var index = 0
  3. fun get(index: Int): Int {
  4. return 0
  5. }
  6. companion object CompanyC {
  7. fun create(): ClassC = ClassC()
  8. fun get() = "Hi, I am CompanyC"
  9. }
  10. }

测试代码:

  1. ClassC.index
  2. ClassC.create()// com.easy.kotli.ClassC@7440e464,具体运行值会变化
  3. ClassC.get() // Hi, I am CompanyC
  4. ClassC.CompanyC.index
  5. ClassC.CompanyC.create()
  6. ClassC.CompanyC.get()

伴生对象的初始化是在相应的类被加载解析时,与 Java 静态初始化器的语义相匹配。

即使伴生对象的成员看起来像其他语言的静态成员,在运行时他们仍然是真实对象的实例成员。而且,还可以实现接口:

  1. interface BeanFactory<T> {
  2. fun create(): T
  3. }
  4. class MyClass {
  5. companion object : BeanFactory<MyClass> {
  6. override fun create(): MyClass {
  7. println("MyClass Created!")
  8. return MyClass()
  9. }
  10. }
  11. }

测试代码:

  1. MyClass.create() // "MyClass Created!"
  2. MyClass.Companion.create() // "MyClass Created!"

另外,如果想使用Java中的静态成员和静态方法的话,我们可以用:

@JvmField注解:生成与该属性相同的静态字段
@JvmStatic注解:在单例对象和伴生对象中生成对应的静态方法