Kotlin : object、companion object、 @JvmField、 @JvmStatic、顶层函数、单例

object关键字:单例对象

在Kotlin中,object关键字用于定义一个单例对象或一个匿名类。

  • 用途:直接定义一个单例(全局唯一的实例),无需手动实例化。
  • 特点
    • 不能有构造函数(因为实例在定义时直接创建)。
    • 可以直接通过类名访问属性和方法。
  • 示例
object DatabaseManager {
    fun connect() { println("连接数据库") }
}
 
// 使用:
DatabaseManager.connect() // 直接调用,无需 new

companion object:伴生对象

在Kotlin中,class关键字用于定义一个类。类可以包含属性(变量)和方法(函数)。伴生对象(companion object)是Kotlin中的一种特殊结构,它允许我们在类级别定义静态成员(类似于Java中的静态成员)。伴生对象中的成员可以通过类名直接访问,而无需创建类的实例。

  • 用途:在类内部定义“静态成员”(但本质是一个单例对象)。
  • 特点
    • 可以通过类名直接访问其内部的属性和方法。
    • 可以继承接口或实现其他逻辑。
  • 示例
class MyClass {
    companion object {
        const val TAG = "MyClass"
        fun create(): MyClass = MyClass()
    }
}
 
// 使用:
println(MyClass.TAG) // 输出:MyClass
val instance = MyClass.create()

const val

Kotlin中的const val

const val用于声明编译时常量。这意味着常量的值在编译时就已经确定,并且在运行时无法改变。const修饰符只能用于顶层(文件级别)、伴生对象或嵌套类中的属性,并且这些属性必须是val类型,不能是var

Java中的public static final

在Java中,public static final用于声明公共的静态最终变量。这些变量在类加载时初始化,并且在运行时无法修改其值。它们通常用于定义常量,如错误码、配置参数等。

4. 顶层函数(Top-level Functions)

  • 用途:直接在 Kotlin 文件中定义函数,无需包裹在类中。
  • 特点
    • 编译后会被包装到以文件名 + Kt 结尾的类中(如 MyFile.ktMyFileKt)。
    • 在 Java 中可以通过 文件名Kt.函数名() 调用。
  • 示例
// 文件:Utils.kt
fun printHello() {
    println("Hello World!")
}
 
// 使用(Kotlin):
printHello() // 直接调用
 
// 在 Java 中:
UtilsKt.printHello();

综合用法:

// 文件:App.kt
 
// 顶层函数
fun showToast(message: String) {
    println("Toast: $message")
}
 
// 数据类
data class Product(val id: Int, val name: String)
 
// 单例对象(全局管理器)
object NetworkManager {
    fun fetchData() { println("下载数据...") }
}
 
class User {
    companion object Factory {
        fun create(): User = User()
    }
}
 
// 使用:
fun main() {
    showToast("Hello!")        // 顶层函数
    val product = Product(1, "手机") // data class
    NetworkManager.fetchData() // 单例对象
    val user = User.Factory.create() // 伴生对象
}

问题

在 Kotlin 中,object 和 Companion object 都能表示单例对象,为啥还要 Companion object。

objectcompanion object 虽然都涉及单例模式,但它们的 设计意图使用场景 完全不同。以下是它们的核心区别和设计哲学:

1. object:独立的单例对象

  • 作用:定义一个 全局单例对象,直接通过类名访问。
  • 特点
    • 独立于任何类存在(或作为嵌套对象)。
    • 可以继承其他类或实现接口。
    • 适合工具类、全局配置、服务定位器等。
  • 示例
object DatabaseManager {
    fun connect() { /* ... */ }
}
// 使用
DatabaseManager.connect()

2. companion object:类的伴生对象

  • 作用:定义一个 与类绑定的单例对象,用于存放与类相关的“静态”成员。
  • 特点
    • 绑定到类:必须声明在某个类内部。
    • 访问权限:可以访问外部类的私有成员(包括私有构造函数)。
    • 替代 Java 的静态成员:但更灵活(可以实现接口、扩展函数等)。
  • 示例
class MyClass {
    companion object {
        const val TAG = "MyClass"
        fun create(): MyClass = MyClass()
    }
}
// 使用
MyClass.TAG
MyClass.create()

虽然objectcompanion object都表示单例,但它们的设计目标不同:

特性objectcompanion object
作用域全局或嵌套在类中必须属于某个类
访问外部类私有成员不能可以
初始化时机首次访问时外部类加载时(与类一起初始化)
典型用途工具类、服务、全局配置工厂方法、静态常量、类相关逻辑
  • object:用于独立的单例对象,作用域不绑定任何类。
  • companion object:用于与类绑定的单例对象,提供类级别的“静态”成员,同时支持更灵活的访问和继承。

Kotlin 通过区分两者,既保持了代码的语义清晰性,又提供了比 Java 静态成员更强大的功能。这是 Kotlin 设计哲学的一部分:通过明确的语法表达意图,同时避免 Java 静态成员的局限性。

关键区别

  1. 访问权限
  • companion object可以访问外部类的私有成员(如私有构造函数),这是object做不到的。
  • 示例:实现工厂模式时,通过companion object调用私有构造函数:
class User private constructor(val name: String) {
    companion object {
        fun create(name: String): User {
            return User(name) // 访问私有构造函数
        }
    }
}
  1. 作用域绑定
    • companion object 是类的一部分,逻辑上更紧密。例如,MyClass.Companion 的存在是为了明确这些成员属于 MyClass
    • object 是独立的,不依赖任何类。
  2. 与 Java 的互操作性
    • companion object 的成员在 Java 中可以通过 MyClass.Companion.getX() 调用,但 Kotlin 提供了语法糖,允许直接写 MyClass.x(类似静态成员)。
    • 普通的 object 在 Java 中需要通过 MyObject.INSTANCE 访问。

Kotlin 的伴生对象(Companion Object)与 Java 的static变量之间的差异

1.Java 的静态变量(static)

在 Java 中:

  • 静态变量属于类本身(而非对象实例),可以直接通过类名访问(如ClassName.staticVar)。
  • 静态方法可以直接访问静态变量,但**非静态方法(实例方法)**也能访问静态变量,因为实例方法属于对象,而对象可以访问类的静态成员。
public class MyClass {
    static int myStaticVar = 42;
 
    public void instanceMethod() {
        // 实例方法可以访问静态变量
        System.out.println(myStaticVar);
    }
 
    public static void staticMethod() {
        // 静态方法可以访问静态变量
        System.out.println(myStaticVar);
    }
}

2.Kotlin 的伴生对象(Companion Object)

Kotlin 没有static关键字,而是通过伴生对象实现类似功能。但伴生对象本质上是类的单例实例

  • 伴生对象中的成员(变量或函数)不是静态的,而是属于一个单例对象。
  • 在 Java 中调用时,伴生对象的成员会被编译成静态成员(通过Companion类),但 Kotlin 中可以直接通过类名访问。
class MyClass {
    companion object {
        val myVar = 42
    }
 
    fun instanceMethod() {
        // 实例方法可以访问伴生对象的成员
        println(myVar) // 等同于 MyClass.Companion.myVar
    }
}

3.为什么onCreate可以访问伴生对象的context

  • onCreate是实例方法,但它属于SunnyWeatherApplication类的实例。
  • 伴生对象的成员(如context)可以通过类名直接访问(如SunnyWeatherApplication.context),但本质上它们是伴生对象实例的成员
  • 在实例方法中,可以直接访问伴生对象的成员(无需类名前缀),因为 Kotlin 隐式处理了这一点。

4.Java 视角的等价代码

  • 虽然onCreate是实例方法,但它可以访问Companion单例的成员,因为Companion是静态的。
  • Kotlin 在语法层面隐藏了这些细节,使得伴生对象的成员看起来像是静态的。
public class SunnyWeatherApplication extends Application {
    private static volatile SunnyWeatherApplication.Companion Companion;
 
    // 伴生对象内部的单例实例
    public static final class Companion {
        private Context context;
 
        public final Context getContext() { return context; }
        public final void setContext(Context value) { context = value; }
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
        // 通过 Companion 单例设置 context
        Companion.setContext(getApplicationContext());
    }
}

更新: 2025-03-24 11:57:36
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/em0a9gtznexdg0sf