Question

Java Static Method Equivalents in Kotlin: Companion Objects and Top-Level Functions

javakotlinstaticstatic-methods

Question

Kotlin does not have a static keyword. What is the best way to represent a static Java method in Kotlin, and when should you use a top-level function, a companion object, or @JvmStatic?

Short Answer

Kotlin has no static members because it uses other language features to model shared behavior. You will learn the main alternatives—top-level functions, companion object functions, and singleton object declarations—and how to expose Kotlin APIs conveniently to Java when needed.

Concept

Java uses static to attach a method or field to a class rather than to an individual object.

public class TextUtils {
    public static boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
}

Kotlin separates the different reasons developers use static methods:

  • Top-level functions are functions declared directly in a Kotlin file. They are usually the best choice for general utility behavior that does not belong to a particular class.
  • A companion object holds behavior logically associated with a class, such as factory methods, constants, and parsing methods.
  • An object declaration creates a named singleton. It is useful when shared state or a named service is genuinely needed.
  • @JvmStatic is primarily for Java interoperability. It changes how Java callers access a function; Kotlin callers generally do not need it.

This matters because Kotlin code is designed around clear ownership. Instead of automatically putting every utility into a class merely to obtain static, Kotlin lets a function exist without a containing class.

Mental Model

Think of a Java class with static methods as a noticeboard mounted on a building: everyone uses the same board, and nobody needs to enter the building first.

Kotlin offers several places to put that noticeboard:

  • A top-level function is a public noticeboard in the file itself.
  • A companion object is a noticeboard attached to one specific class.
  • An object declaration is one shared office with its own noticeboard and its own stored information.

Choose the location based on who owns the behavior. A text-formatting helper may belong at the top level. A User factory belongs beside User. A shared application configuration service may need a singleton object.

Take Quiz

Syntax and Examples

1. Top-level function

Declare a function outside any class:

// TextUtils.kt
fun isBlank(value: String?): Boolean {
    return value == null || value.isBlank()
}

fun main() {
    println(isBlank("   ")) // true
}

This is often the most idiomatic Kotlin replacement for a Java utility class containing only static methods. Kotlin code calls it directly as isBlank(...).

On the JVM, Kotlin compiles this function into a generated class based on the file name. From Java, the default call is TextUtilsKt.isBlank(value).

2. Companion object

Use a companion object when the function conceptually belongs to a class:

class User private constructor(val name: String) {
    companion object {
        fun create(name: String): User {
            require(name.isNotBlank()) { "Name cannot be blank" }
            return User(name.trim())
        }
    }
}

fun main() {
    val user = User.create(" Ada ")
    println(user.name) // Ada
}

User.create(...) resembles calling a static factory in Java, but create is actually a function on the companion object.

3. Named singleton object

Use object when you need exactly one shared instance, especially if it contains state:

object IdGenerator {
    private var nextId = 1

    fun newId(): Int = nextId++
}

fun main() {
    println(IdGenerator.newId()) // 1
    println(IdGenerator.newId()) // 2
}

4. @JvmStatic for Java callers

class Token private constructor(val value: String) {
    companion object {
        @JvmStatic
        fun from(value: String): Token = Token(value)
    }
}

Kotlin calls this normally:

val token = Token.from("abc")

With @JvmStatic, Java can also call it in static style:

Token token = Token.from("abc");

Without @JvmStatic, Java accesses the companion explicitly:

Token token = Token.Companion.from("abc");

Step by Step Execution

Consider a class factory implemented with a companion object:

class Temperature private constructor(val celsius: Double) {
    companion object {
        fun fromFahrenheit(fahrenheit: Double): Temperature {
            val celsius = (fahrenheit - 32) * 5 / 9
            return Temperature(celsius)
        }
    }
}

fun main() {
    val temperature = Temperature.fromFahrenheit(68.0)
    println(temperature.celsius)
}

Step by step:

  1. Temperature has a private constructor, so code outside the class cannot write Temperature(20.0).
  2. The companion object provides a class-related creation function named fromFahrenheit.
  3. main calls Temperature.fromFahrenheit(68.0) without first creating a Temperature instance.
  4. The function converts Fahrenheit to Celsius: (68.0 - 32) * 5 / 9, producing 20.0.
  5. The companion object creates and returns Temperature(20.0) because it can access the private constructor.
  6. The program prints 20.0.

The call looks static, but Kotlin models it as a call to the class's companion object.

Real World Use Cases

  • Factory methods: Create objects from alternative input formats, such as Money.fromCents(499) or LocalDate.parse("2025-03-08").
  • Validation helpers: Use top-level functions such as isValidEmail(value) when they are not owned by one domain class.
  • Constants related to a type: Put values such as a maximum allowed length in a companion object.
  • Parsing and serialization: Provide Order.fromJson(json) in a companion object when parsing creates an Order.
  • Shared services: Use an object for small, stateless coordinators or for a deliberately shared component such as an application logger wrapper.
  • Java-compatible libraries: Add @JvmStatic to selected companion functions when Java consumers should call Type.method() rather than Type.Companion.method().
Take Quiz

Real Codebase Usage

A useful decision rule is to start with the least stateful option.

Utility behavior: top-level functions

For behavior that does not need class state and is not strongly owned by a type, use a top-level function:

fun requireNonEmpty(value: String, fieldName: String): String {
    require(value.isNotBlank()) { "$fieldName must not be blank" }
    return value.trim()
}

Top-level functions avoid Java-style StringUtils or ValidationUtils classes that exist only to hold static methods.

Creation and parsing: companion objects

Use a companion object for factories, parsers, and type-level configuration:

class ApiKey private constructor(val value: String) {
    companion object {
        fun parse(raw: String): ApiKey {
            require(raw.startsWith("key_")) { "Invalid API key" }
            return ApiKey(raw)
        }
    }
}

This keeps the validation rule close to the type it protects.

Guard clauses and early validation

Static-like functions often validate input before processing it:

fun percentage(part: Int, total: Int): Double {
    require(total > 0) { "Total must be greater than zero" }
    return part.toDouble() / total * 100
}

Singleton objects: use intentionally

An object is a real singleton instance, not merely a namespace. It can retain mutable state, which can make tests depend on execution order. Prefer a top-level function or companion object when no shared state is required.

object FeatureFlags {
    var newCheckoutEnabled: Boolean = false
}

For larger applications, configuration and services are often passed as dependencies instead of being global singletons.

Common Mistakes

Treating object as the default replacement for every static method

This works, but can create an unnecessary singleton:

object MathHelpers {
    fun square(number: Int) = number * number
}

If the function has no state and does not belong to a type, a top-level function is simpler:

fun square(number: Int) = number * number

Adding @JvmStatic when Kotlin is the only caller

class User {
    companion object {
        @JvmStatic
        fun guest() = User()
    }
}

This is not wrong, but it is unnecessary for Kotlin callers. Use @JvmStatic only when Java call syntax or a Java framework specifically benefits from it.

Assuming a companion member is truly a Java static member

Without @JvmStatic, Java must access the companion object:

User user = User.Companion.guest();

If Java must call User.guest(), add @JvmStatic to that companion function.

Putting unrelated utilities into one companion object

Avoid using a random class as a container for unrelated behavior:

class User {
    companion object {
        fun formatCurrency(amount: Double): String = "$amount"
    }
}

Currency formatting is not owned by User; make it a top-level function or place it in a relevant formatter type.

Using mutable singleton state without considering tests

object Session {
    var currentUserId: String? = null
}

This global state can leak between tests or requests. Prefer explicit state passed through function parameters or injected dependencies when possible.

Comparisons

Kotlin approachBest forKotlin callJava callShared mutable state?
Top-level functionGeneral utilities with no clear owning typeformatName(name)FileNameKt.formatName(name) by defaultNo
companion object functionFactories, parsing, and behavior owned by a classUser.create(name)User.Companion.create(name) by defaultPossible, but usually avoid it
Companion function with @JvmStaticCompanion APIs used from JavaUser.create(name)User.create(name)Possible, but usually avoid it
object declarationA deliberate singleton with identity or shared stateIdGenerator.newId()IdGenerator.INSTANCE.newId() by defaultYes

Companion object versus object

A companion object is associated with a class and is commonly used for type-level operations. A standalone object is its own singleton type.

class User {
    companion object {
        fun guest() = User()
    }
}

object AppClock {
    fun currentTimeMillis(): Long = System.currentTimeMillis()
}

Use User.guest() because creating a user belongs to User. Use AppClock.currentTimeMillis() only if a named, replaceable clock service is appropriate for the application.

Take Quiz

Cheat Sheet

  • Kotlin has no static keyword.

  • Use a top-level function for a general, stateless utility.

    fun slugify(text: String): String = text.lowercase().replace(" ", "-")
    
  • Use a companion object for behavior owned by a class.

    class User private constructor() {
        companion object {
            fun guest(): User = User()
        }
    }
    
  • Call companion functions with the class name:

    val user = User.guest()
    
  • Use a standalone object for one shared instance:

    object Counter {
        var value = 0
    }
    
  • Add @JvmStatic inside a companion object only when Java should call User.guest() instead of User.Companion.guest().

  • A top-level function is compiled into a file-based JVM class for Java interoperability, such as HelpersKt by default.

  • Prefer stateless functions. Be cautious with mutable data in object declarations.

FAQ

Does Kotlin have static methods?

No. Kotlin does not use the static keyword. Top-level functions, companion objects, and object declarations cover the common use cases.

What is the most idiomatic Kotlin replacement for a Java static utility method?

Usually a top-level function, especially when the function has no state and does not naturally belong to a class.

When should I use a companion object in Kotlin?

Use it for operations tied to a type, such as fromJson, parse, create, of, and constants related to that class.

Is a Kotlin companion object the same as Java static?

Not exactly. It is an object associated with a class. Kotlin lets you call its members as ClassName.member(), but Java normally sees it as ClassName.Companion.member() unless @JvmStatic is used.

When do I need @JvmStatic?

Use it when Java code, a Java-based framework, or a Java-facing API needs conventional static access to a companion member. Kotlin callers do not need it.

Can an object contain properties and methods?

Yes. An object is a singleton instance, so it can contain functions and properties, including mutable properties. Use mutable global state carefully.

Can I put constants in a companion object?

Yes. For Java-style compile-time constants, use const val with a primitive or String value:

class Limits {
    companion object {
        const val MAX_RETRIES = 3
    }
}
Take Quiz

Mini Project

Description

Build a small ProductCode value type. Product codes must be normalized and validated before an instance is created. The project demonstrates why a companion object is a good replacement for a Java static factory method: creation rules stay next to the type they protect.

Goal

Create validated ProductCode objects with ProductCode.from(rawCode) and add a top-level function for formatting a display label.

Requirements

Create a ProductCode class whose constructor cannot be called directly.

Take Quiz

Keep learning

Related questions

View more