Equivalent Implementation of Java Static Methods in Kotlin: In-depth Analysis of Companion Objects

Nov 11, 2025 · Programming · 14 views · 7.8

Keywords: Kotlin | Java Static Methods | Companion Objects | @JvmStatic Annotation | Language Design

Abstract: This article provides a comprehensive exploration of various approaches to implement Java static method equivalents in Kotlin, with a primary focus on the core concepts and usage of companion objects. Through detailed code examples and comparative analysis, it elucidates the differences between companion objects and Java static methods in terms of syntax, invocation methods, and underlying implementation. The article also introduces optimization techniques such as @JvmStatic annotation and named companion objects, while explaining the language design philosophy behind Kotlin's choice of companion objects over the static keyword from the perspective of inheritance and interface implementation advantages.

Introduction

During the migration from Java to Kotlin, developers frequently encounter the fundamental question of how to handle Java static methods. The Kotlin language design does not include the static keyword, which necessitates a reconsideration of implementation approaches for class-level methods and properties. This article systematically analyzes various methods for implementing Java static functionality in Kotlin, based on highly-rated Q&A from Stack Overflow and official documentation.

Basic Concepts of Companion Objects

Companion objects serve as the primary mechanism in Kotlin for implementing class-level methods and properties. By declaring a companion object within a class, we create a singleton instance associated with the class, whose methods and properties can be accessed directly through the class name.

The basic declaration of a companion object is as follows:

class Foo {
    companion object {
        fun a(): Int = 1
    }
}

In Kotlin code, we can directly invoke companion object methods through the class name:

val result = Foo.a()

Considerations for Java Interoperability

While the invocation method in Kotlin is concise, additional steps are required when calling from Java code. Java code must access companion object methods through the Companion identifier:

int result = Foo.Companion.a();

This invocation method is also valid in Kotlin but is generally not the preferred approach.

Optimizing Java Interoperability: @JvmStatic Annotation

To improve the calling experience for Java code, Kotlin provides the @JvmStatic annotation. When this annotation is used, the compiler generates genuine Java static methods:

class Foo {
    companion object {
        @JvmStatic
        fun a(): Int = 1
    }
}

After adding the annotation, Java code can directly invoke methods through the class name without the Companion identifier:

int result = Foo.a();

This approach maintains the same invocation method in Kotlin code while providing better cross-language compatibility.

Named Companion Objects

In addition to using the default Companion name, we can assign custom names to companion objects:

class Foo {
    companion object Blah {
        fun a(): Int = 1
    }
}

When calling from Java code, the specified name must be used:

int result = Foo.Blah.a();

This approach maintains the same invocation method in Kotlin while providing clearer semantic expression.

Analysis of Language Design Philosophy

Kotlin's choice of companion objects over the static keyword reflects profound design considerations. Companion objects are essentially singleton objects, which maintains consistency in Kotlin's object-oriented design. Unlike Java static methods, companion objects can:

This design makes companion objects more flexible and powerful than Java static methods. For example, we can create a companion object that supports logging functionality:

abstract class Log {
    val LOG: Logger
    
    init {
        val cls = this::class
        // Logic to obtain the accompanied class
        val accompanied = Class.forName(cls.java.name.substringBeforeLast('$'))
        LOG = LoggerFactory.getLogger(accompanied)
    }
    
    inline fun linfo(msg: () -> String) {
        if (LOG.isInfoEnabled) LOG.info(msg())
    }
}

class Test {
    companion object : Log()
    
    fun someMethod() {
        linfo { "starting some method" }
    }
}

Performance Considerations

While companion objects offer additional functionality, some performance trade-offs are necessary. Accessing companion object methods typically involves slight performance overhead compared to calling Java static methods, due to object instance lookup. However, in most application scenarios, this difference is negligible.

For performance-sensitive scenarios, consider using top-level functions as an alternative:

// File-level function
fun createFoo(): Foo = Foo()

Best Practice Recommendations

Based on practical development experience, we recommend:

  1. For pure Kotlin projects, prioritize using companion objects without annotations
  2. For projects requiring deep integration with Java code, use the @JvmStatic annotation
  3. When clear semantic expression is needed, consider using named companion objects
  4. For simple utility functions, consider using top-level functions

Conclusion

Kotlin provides richer and more consistent functionality than Java static methods through the companion object mechanism. Although initially appearing more complex than Java's static keyword, companion objects demonstrate clear advantages in object-oriented design, inheritance support, and language consistency. By appropriately using the @JvmStatic annotation and named companion objects, we can maintain Kotlin's design philosophy while providing excellent Java interoperability.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.