How to stop struggling and find a replacement for Java static in Kotlin
\n\nHave you ever rewritten good old Java code in Kotlin and suddenly hit a wall? Everything seems simple, logical, concise… But then you want to create a static method — and Kotlin says: "No, friend, I don't have that". Panic? Don't worry. This question has gathered thousands of views on Stack Overflow, and for good reason: the topic is truly important for everyone transitioning from Java to Kotlin.
\n\nIn this article, we won't just give a dry answer. We'll break down the equivalent of Java static methods in Kotlin from the perspective of performance, optimization, and maintainability. You'll learn how to avoid losing speed and stepping on rakes. Let's go!
\n\nWhy is there no static in Kotlin and why is it needed?
\n\nLet's get straight to the point. In Java, static methods are methods that belong to the class, not to its instance. They live in memory once, don't require creating an object, and are perfect for utility functions, factories, or singletons.
\n\n// Java\npublic class MathUtils {\n public static int square(int x) {\n return x * x;\n }\n}\n\n// Usage\nint result = MathUtils.square(5);\n\n\nIn Kotlin, however, there is no static keyword. Why? Because Kotlin encourages both an object-oriented and functional approach simultaneously. Instead of static, the following are used:
\n\n- \n
- companion object \n
- top-level functions (functions outside a class) \n
- object (singleton object) \n
Each option has its own performance and optimization nuances. Let's break them down in order.
\n\nOption 1: companion object — a direct analog of static?
\n\nThe most common answer you'll find on Stack Overflow is the companion object. It looks like this:
\n\n// Kotlin\nclass MathUtils {\n companion object {\n fun square(x: Int): Int = x * x\n }\n}\n\n// Usage\nval result = MathUtils.square(5)\n\n\nVisually — one-to-one like Java. But under the hood, it's different. companion object is a real object, a subclass of Any. It is created once when the class is loaded and stored in memory as a regular object. This means:
- \n
- Memory: slightly more than Java static (due to object overhead). \n
- Call: the Kotlin compiler generates a static method, but only if you add the
@JvmStaticannotation. \n
@JvmStatic: the magic of interoperability
\n\nIf you write in Kotlin, but your code is called from Java, then without @JvmStatic you'll face an unpleasant surprise:
// Kotlin\nclass MyClass {\n companion object {\n @JvmStatic\n fun greet() = "Hello!"\n }\n}\n\n// Java call\nMyClass.greet(); // Works thanks to @JvmStatic\n// Without @JvmStatic you would have to write MyClass.Companion.greet()\n\n\nFrom a performance perspective, @JvmStatic compiles into a real static method on the JVM. This means you get exactly the same speed as in Java. Without this annotation, the call goes through the Companion object, which introduces a micro-overhead (less than 1 nanosecond, but for high-load systems this can matter).
Option 2: Top-level functions — the Kotlin way
\n\nKotlin allows declaring functions directly in a file, outside a class. This is called a top-level function. The compiler turns them into static methods of a wrapper class (file name + Kt).
\n\n// File: MathUtils.kt\nfun square(x: Int): Int = x * x\n\n// Usage\nval result = square(5)\n\n\nUnder the hood this is:
\n\n// Java (decompiled)\npublic final class MathUtilsKt {\n public static final int squa