Why Kotlin Companion Objects Replace Java Static Fields: JVM Compilation Deep Dive
Kotlin companion objects serve as the idiomatic replacement for Java static members because the Kotlin compiler automatically lowers them to static fields and methods in JVM bytecode while providing object-oriented syntax and inheritance capabilities.
The Kotlin programming language deliberately omits the static keyword found in Java, opting instead for companion objects—singleton objects tied to a class that provide similar functionality. According to the JetBrains/kotlin repository source code, when targeting the JVM, the compiler performs sophisticated lowering passes to transform these companion objects into the static fields and methods that the JVM expects, ensuring seamless interoperability with Java code.
What Is a Kotlin Companion Object?
A companion object is a singleton object declared inside a class using the companion object syntax. Unlike Java static members, companion objects are real objects that can inherit from other classes, implement interfaces, and have their own state.
When you declare a companion object in Kotlin, the compiler creates exactly one instance of this object per class, similar to how Java static members belong to the class rather than instances. However, the key difference lies in the compilation strategy: while Java static members exist at the language level, Kotlin companion objects are lowered to static fields during compilation to satisfy JVM requirements.
How the Kotlin Compiler Transforms Companion Objects to Static Fields
The Kotlin JVM backend implements companion object lowering through several specialized compiler passes. These transformations ensure that Kotlin source code using companion objects generates bytecode that matches the static member pattern expected by Java callers.
Generating the Companion Static Field Instance
The compiler first generates a static field to hold the singleton instance of the companion object. In compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmInnerClassesSupport.kt at lines 60-63, the compiler creates a static field named Companion (or the custom name if specified) that stores the singleton instance.
This static field allows Java code to access the companion object instance via ClassName.Companion, while Kotlin code can access members directly using the class name as a qualifier.
Moving Fields to the Outer Class
When the language feature ProperVisibilityForCompanionObjectInstanceField is enabled, the compiler moves non-static fields declared inside a companion object into the outer class. This optimization occurs in compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/MoveCompanionObjectFieldsLowering.kt at lines 31-36.
By moving these fields to the outer class, the compiler reduces indirection—Java code can access these fields as true static fields of the containing class rather than accessing them through the companion object instance.
Handling @JvmStatic Annotation
For members annotated with @JvmStatic, the compiler generates additional static methods in the outer class that delegate to the companion object. This transformation is implemented in compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmStaticAnnotationLowering.kt at lines 43-56.
When you annotate a companion object function with @JvmStatic, the compiler emits:
- A static method in the outer class with the same signature
- An instance method in the companion object that contains the actual implementation
- The static method delegates to the singleton instance
This allows Java code to call ClassName.methodName() directly, matching the expected static method invocation pattern.
Intrinsic Companion Objects
For built-in types like primitive wrappers, the compiler uses intrinsic companion objects defined in compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmCachedDeclarations.kt at lines 369-393. These intrinsic companions map to existing Java static members (like Integer.MIN_VALUE) rather than generating new companion object instances, optimizing the bytecode for standard library types.
Practical Example: Companion Object vs. Java Static
Consider a factory pattern implementation using Kotlin companion objects and how it compiles to JVM bytecode equivalent to Java static members.
class User private constructor(val name: String) {
companion object {
const val DEFAULT_NAME = "Anonymous"
@JvmStatic
fun create(name: String = DEFAULT_NAME): User = User(name)
fun createWithDefault(): User = User(DEFAULT_NAME)
}
}
When compiled to JVM bytecode, this generates the following structure accessible from Java:
// Java usage
User user = User.create(); // Direct static method call
String defaultName = User.DEFAULT_NAME; // Static field access
User$Companion companion = User.Companion; // Access to companion instance
User user2 = User.Companion.createWithDefault(); // Instance method via companion
The compiler generates these specific bytecode elements as implemented in the JetBrains/kotlin repository:
- A static field
Companionholding the singleton instance (fromJvmInnerClassesSupport.kt) - A static field
DEFAULT_NAMEmoved to the outer class (fromMoveCompanionObjectFieldsLowering.kt) - A static method
createdelegating to the companion (fromJvmStaticAnnotationLowering.kt) - An instance method
createWithDefaultremaining in the companion object class
Summary
- Kotlin companion objects replace Java static members by providing singleton objects tied to classes, offering object-oriented capabilities like inheritance while compiling to equivalent JVM bytecode.
- The Kotlin JVM backend transforms companion objects into static fields and methods through specialized lowering passes in files like
JvmInnerClassesSupport.ktandJvmStaticAnnotationLowering.kt. - @JvmStatic annotation generates true static methods in the containing class, enabling Java code to call Kotlin companion members using familiar static syntax.
- Const vals and optimized field movements ensure that companion object fields compile to efficient static field access without runtime indirection when possible.
Frequently Asked Questions
Is a Kotlin companion object exactly the same as Java static members?
No, while they compile to similar JVM bytecode, companion objects are actual singleton instances that can implement interfaces, inherit from classes, and have their own lifecycle. Java static members belong to the class itself and cannot form inheritance hierarchies. The Kotlin compiler lowers companion objects to static fields for JVM interoperability, but at the source level they provide more flexibility than Java statics.
When should I use @JvmStatic in a companion object?
Use @JvmStatic when you need Java code to call companion object members as if they were static methods of the containing class. Without this annotation, Java code must access members through the Companion instance (e.g., User.Companion.create()). With @JvmStatic, Java code can use the cleaner syntax User.create(). The compiler implements this in JvmStaticAnnotationLowering.kt by generating a static bridge method in the outer class.
Can companion objects inherit from other classes?
Yes, unlike Java static contexts, companion objects can explicitly extend classes and implement interfaces. You declare this using standard object declaration syntax: companion object : SomeInterface { ... }. This allows you to share implementation between companion objects or treat the companion as a specific type. The compiler handles this inheritance while still generating the necessary static field infrastructure for JVM compatibility.
Does using companion objects impact runtime performance?
No, companion objects do not introduce significant runtime overhead compared to Java static members. The Kotlin compiler optimizes access to companion object members through several mechanisms: const val declarations compile to true static constants, the ProperVisibilityForCompanionObjectInstanceField feature moves fields directly to the containing class, and @JvmStatic generates direct static method calls. The singleton instance is created lazily and cached, resulting in performance characteristics equivalent to Java static initialization.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →