Kotlin has no static keyword. If you’re coming from Java, you usually notice this in the first day or so, often while trying to write a constant or a factory method.
Kotlin uses the companion object instead. It’s an object declaration nested inside a class body, and it belongs to the class itself rather than to any one instance. Android and server-side Kotlin developers put things there wherever Java code would have used a static field or method.
You could call companion members without the explicit Companion qualifier well before Kotlin went stable on February 15, 2016. Early pre-1.0 milestones made you write the qualifier out every time you accessed a companion member. Bumble’s engineering team later described dropping that requirement as one of the changes that made companion objects worth using over plain Java-style statics (Bumble Tech engineering blog, 2021).
What Is a Kotlin Companion Object?

From the outside it looks like a static member. You call it on the class name with no instance needed, the same as a Java static field or method.
Underneath, it’s different. The compiler builds one real singleton instance and hangs the members off it. They aren’t a bundle of detached static values.
Some background numbers help explain why this comes up so often:
- Kotlin first appeared on July 22, 2011, created by JetBrains
- Default identifier when the companion object is left unnamed: Companion
- Latest stable release as of this writing: Kotlin 2.4.20, published September 2026 (JetBrains); Java 26 support on the JVM target arrived with the 2.4.0 release in June 2026
- Over 95 percent of the top 1,000 Android apps on Google Play contain Kotlin code (JetBrains, 2024)
Default Companion Name
If you don’t name it, Kotlin calls it Companion for you.
You’ll rarely type that name. Calling a member through the class name skips the companion identifier entirely. Companion.someFunction() still works from inside Kotlin code, but ClassName.someFunction() is what almost everyone writes. The default only gets replaced when the class body gives the object a name explicitly.
A custom name like Factory is mostly useful when the companion implements an interface and you want the label to say something.
If you’re still getting used to Kotlin as a language, expect this object-first design to show up in a lot more places than companion objects.
Accessing Companion Object Members
You reach companion members through the enclosing class name. You don’t create an instance first.
| What you write | What Kotlin resolves it to |
|---|---|
| ClassName.member() | ClassName.Companion.member() |
Both forms work in Kotlin.
Java is where it gets annoying. Java code calling an unannotated companion object has to use the longer Companion path, which is why the JvmStatic and JvmField annotations exist (more on those further down).
Companion Object vs Object Declaration

The main difference is what it’s attached to. A plain object declaration stands on its own at file or package scope, and you refer to it by its own name. That makes it a good fit for config objects and registries.
Both create a singleton pattern under the hood. There’s exactly one instance, built once and reused everywhere it’s referenced.
The companion version lives inside a class body and you go through the class name to reach it. It can also reach the class’s private members directly, and that ends up mattering more than you’d think once you start writing factories.
My rule is simple. If the singleton has nothing to do with a specific class, use an object declaration. If it’s a factory method or a constant tied to how one class behaves, put it in the companion.
Kotlin’s own singleton classes cover the case where a shared instance isn’t tied to any class. They’re worth a separate look if the difference still isn’t clear.
Companion Object vs Java Static Members

Java static members belong to the class definition. There’s no instance behind them, not even a hidden one. A companion object is an actual object that happens to be tied to a class, and most of the behavioral differences come from that.
| Feature | Companion Object | Java Static Member |
|---|---|---|
| Underlying structure | Singleton instance nested in the class | No instance, belongs to the class definition |
| Default JVM output | Instance method on a nested Companion class | True static method on the class file |
| Interface implementation | Can implement interfaces | Cannot implement interfaces |
| Inheritance | Can extend another class | Not applicable |
So without an annotation, Java doesn’t treat a companion member as static at all. It sees an instance method on a generated Companion class, reached through a field named INSTANCE or Companion.
This comes up more than you might expect, because most Android work has moved to Kotlin. Kotlin’s own documentation says over 50 percent of professional Android developers now use Kotlin as their primary language at work, against roughly 30 percent who still list Java first.
New Android codebases mostly rely on Kotlin’s object model. Teams that still work in both languages run into this exact mismatch, usually in a shared module someone wrote years ago.
The decision to lean on Kotlin over Java for a given Android project often comes down to how much of this interop friction a team is willing to deal with long-term.
How a Companion Object Initializes and Compiles to Bytecode

The companion object is created once, during static initialization of the enclosing class. That happens before any instance of the class exists.
The compiler generates a nested class to hold it and adds a static initializer block, so the singleton gets built the first time the JVM loads the outer class.
Class Loading and Initialization Order
When the JVM loads the class, it runs a static initializer block, and that’s where the single instance gets built. So the companion is ready before the first instance of the class is constructed, and before any static-style access from Java or Kotlin goes through.
A plain object declaration works the same way, except for what triggers it.
Both depend on lazy class initialization on the JVM, so neither one gets built until something in the program actually uses it.
Bytecode Representation
If you decompile the class, you’ll find a nested class named Companion inside the outer class file. The companion’s members sit on it as instance methods and fields.
The outer class holds an INSTANCE or Companion reference that points at the singleton. The nested Companion class gets a private constructor automatically, so nothing else can create a second instance. And by default, companion functions compile as instance methods on that nested class, not static ones. That surprises a lot of people the first time they see it.
You can check this yourself in Android Studio‘s bytecode viewer under Tools, Kotlin, Show Kotlin Bytecode.
I use it more than I’d like to admit. It’s usually the quickest way to settle an argument about what a companion object actually compiles to.
Exposing Companion Object Members to Java with JvmStatic and JvmField
Both annotations tell the compiler to generate a true static member on the JVM, instead of the default instance method or field on the nested Companion class.
Without them, Java has to go through ClassName.Companion to reach anything inside.
JvmStatic
If you put @JvmStatic on a function inside a companion object, the compiler generates it twice. One copy is a static method on the outer class. The other is the original instance method on the Companion class itself.
Nothing changes on the Kotlin side, since ClassName.method() and ClassName.Companion.method() both keep working. In Java, only the static one works as a normal static call, ClassName.method().
This matters most in tests. JUnit 5’s @MethodSource requires its argument provider to be a real static method, so companion functions that supply parameterized test data won’t work at all without @JvmStatic, per Kotlin’s own interop documentation.
JvmField
@JvmField does the same job for properties. No getter or setter is generated, and the backing field is exposed directly as a public static field on the JVM.
Android’s Parcelable interface depends on this.
The CREATOR property inside a companion object gets marked @JvmField so the Android framework can read it as a plain static field, which is what the platform expects.
You don’t need @JvmField for const val declarations inside a companion object. They compile to static final fields automatically, since the compiler already knows the value never changes.
Building Factory Methods with a Companion Object

With a factory method, callers use a named function that returns the instance instead of calling a public constructor. Usually some validation or setup runs first.
It’s the static factory method pattern Effective Java popularized decades ago. Kotlin’s version is a little easier because the companion object can reach the class’s private members directly.
Setting one up goes like this.
- Make the primary constructor private, so nothing outside the class can call it directly
- Add a function inside the companion object, often named create or of
- Run any validation or setup logic inside that function, before the instance gets built
- Return the class instance from the function, calling the private constructor internally
- Call the factory function through the class name, ClassName.create(), instead of a public constructor
Private Constructor Requirement
The pattern only works cleanly when the primary constructor is private. Then callers have to go through the factory function, and validation always runs. If you leave the constructor public, anyone can skip the factory, which makes it pointless.
Kotlin’s constructors take the private modifier the same way regular functions do, so the whole pattern works without extra boilerplate.
Dependency injection frameworks like Dagger and Hilt often work with these companion-based factory functions when a class needs controlled, validated construction instead of a bare constructor call.
Implementing Interfaces Through a Companion Object

You use the same colon syntax a class uses. Once the companion implements an interface, you can pass the companion itself anywhere that interface type is expected.
Java static members can’t do this. They don’t have a type of their own to implement anything with.
You write the interface after the companion object with a colon (companion object : SomeInterface) and override its members inside the body. After that, you can pass ClassName, through its companion object, anywhere an instance of SomeInterface is expected.
The usual reason to do this is an abstract factory setup. A base class defines a factory interface, and each subclass’s companion object implements it with its own creation logic.
You’ll see this pattern all the time in Android development, where a ViewModelProvider.Factory implementation often lives inside a companion object tied to the ViewModel it constructs.
A companion object can extend a class the same way. That’s less common, though. Most of the time you want an interface contract, not shared implementation.
Adding Extension Functions to a Companion Object

Code outside the class can add new members to its companion object without touching the class body. You write the extension against ClassName.Companion, which is the same path a regular companion member resolves to once compiled.
| Where it’s declared | Who can add it |
|---|---|
| Inside the companion object body | Only code with edit access to the class itself |
| As an extension on ClassName.Companion | Any file that imports the extension, including a separate module |
It can be a function or a property. After compiling, it behaves like any other companion member and you can call it through ClassName alone.
The target class does need a companion object already, named or unnamed. If there isn’t one, the extension has nothing to attach to.
Bumble’s engineering team used companion object extension functions to deprecate an old static-style method and roll out its replacement gradually. That let them avoid a disruptive rewrite across shared code (Bumble Tech, 2021).
The same mechanism is part of how to build DSLs in Kotlin, where builder-style calls are often attached to a companion object rather than built into the class itself.
Extension resolution happens at compile time, based on the declared type, not at runtime.
Code compiled against an older version of the class, from before the extension existed, won’t see it.
When to Use a Companion Object Instead of a Top-Level Function

Top-level functions sit outside any class. They compile to a static method on a generated file-facade class, and you call them with no qualifier at all.
With a companion member, you have to type the class name first. In return, the function is tied to that class and has direct access to its private members.
For me, the grouping is the main benefit. Related functions sit under one class name, so autocomplete shows them together. The costs are a singleton instantiation the first time the companion is used, and harder mocking in tests than a plain function.
Top-level functions are cheaper to call, since no singleton instance is involved, and most testing setups can mock them directly. They can’t reach a class’s private members, though. Once a package has a lot of them, they flood the global autocomplete list.
On the mocking side, standard static-mocking calls like mockkStatic routinely fail against a companion object function. The project’s own issue tracker points to mockkObject(ClassName.Companion) as the approach that actually works (MockK, GitHub).
That gap is why mocking in unit tests is a practical factor when choosing between a companion object and a top-level function.
Kotlin’s community discussion threads on the topic come to about the same conclusion.
Global functions clutter the package namespace and slow down autocomplete, so companion object functions are the safer default outside small projects.
When a Companion Object Does Not Apply
You can’t keep per-instance state in a companion object. Its members belong to the one singleton, not to whichever object of the class happens to be calling it.
If a value needs to differ between instances of the same class, use a regular property or a constructor parameter.
Companion objects that need an implementation on every target in Kotlin Multiplatform run into the expect and actual mechanism, which still has real limits.
Expected and actual classes that declare a companion object are still in Beta in Kotlin’s current multiplatform documentation, and JetBrains notes that future releases may still require migration steps.
A plain Java class also can’t be the actual counterpart to an expected class that declares a companion object. Java classes have no companion object of their own to fill that role (Kotlin language design discussion, JetBrains team).
I’d skip the companion object in these cases:
- The value or behavior really needs to vary per instance
- A single stateless helper function would do the job with no tie to any class
- The class targets multiple Kotlin Multiplatform platforms and the companion’s members need a per-platform implementation
- The companion object has turned into a dumping ground for unrelated utility functions instead of logic that belongs to the class
Kotlin’s own coding conventions put the companion object last in the class body.
They also discourage vague file names like Util. That suggests a companion object works best with a narrow scope, not as a general-purpose toolbox (Kotlin documentation).
FAQ on What Is Kotlin Companion Object
Does a companion object cost more at runtime than a Java static member?
Slightly. One singleton instance gets allocated the first time its class is used, and a Java static member doesn’t have that step.
You pay that cost once, not on every call. With @JvmStatic, Java call performance matches a true static method exactly.
Can a Kotlin class have more than one companion object?
No. A class holds exactly one companion object, and the compiler rejects a second.
If you want to group related helpers separately, add named object declarations inside the same class. Each one gets its own name and its own access path.
Can a companion object be inherited by a subclass?
You can reach members of a superclass’s companion object through a subclass name without extra qualification, and Kotlin’s own release history confirms this behavior.
A subclass can declare its own separate companion object, but companion objects don’t override each other polymorphically.
What mistakes happen when companion objects are used as general utility holders?
The companion becomes a hidden dumping ground. Mutable state starts passing as configuration, and classes that have nothing to do with each other end up tightly coupled.
The class’s autocomplete list fills up with functions that have nothing to do with it.
Does a companion object need JvmStatic to be visible from Java at all?
No. Java can already reach a companion member through ClassName.Companion.member() without any annotation.
@JvmStatic only removes the extra Companion step. The call becomes a real static method, and Java calls it the same way it calls its own statics.
What Should You Check First in What Is Kotlin Companion Object?
Start with Java interop. Any member a Java caller reaches across a module boundary needs @JvmStatic or @JvmField. Otherwise the build breaks at the call site instead of failing quietly.
Mocking friction usually shows up next, anywhere tests stub the class. Scope creep comes later, once the companion has slowly turned into a catch-all utility holder and nobody remembers why things were put there.
Making the constructor private means giving up a one-line MyClass() call in exchange for guaranteed validation on every instance. An internal helper class rarely needs that.
Most companion object factory functions exist to return a fully built instance, so Kotlin’s data classes are the logical next topic for anyone putting together the values those functions return.
- Google Play Account Suspended: What to Do - October 5, 2026
- How to Plan a Successful Data Migration Without Disrupting Business Operations - October 5, 2026
- How to Turn On Dark Mode in Notepad++ (Built-In, No Plugin) - October 3, 2026



