From Java to Kotlin – Part IX: Statics and Companion Objects
Considering a move to Kotlin? Coming from a Java background? In this short series of blog posts, I’ll take a look at familiar, straightforward Java concepts and demonstrate how you can approach them in Kotlin. While many of these points have already been discussed in earlier posts by colleagues, my focus is simple: how you used to do it in Java, and how you do it in Kotlin.
Case 9: You need something that belongs to the class. Not to an instance.
The problem
Sometimes, behavior or data does not belong to an object. It belongs to the type itself.
-
Constants.
-
Factory methods.
-
Utility logic that is conceptually “part of the class”.
In Java, this is easy. Almost too easy.
The Java way
Java has static.
public class User {
public static final int MAX_NAME_LENGTH = 50;
public static User anonymous() {
return new User("Anonymous");
}
private final String name;
public User(String name) {
this.name = name;
}
}
Usage:
User user = User.anonymous();
int max = User.MAX_NAME_LENGTH;
-
Clear.
-
Familiar.
-
Unquestioned.
The Kotlin way
Kotlin does not have static.
Instead, it has companion objects.
class User(val name: String) {
companion object {
const val MAX_NAME_LENGTH = 50
fun anonymous(): User =
User("Anonymous")
}
}
Usage:
val user = User.anonymous()
val max = User.MAX_NAME_LENGTH
-
Looks similar.
-
Feels similar.
-
But it’s not the same thing.
What is a companion object?
A companion object is:
-
A real object
-
Tied to the class
-
Created once
-
Able to implement interfaces
-
Able to be passed around
In other words: - It’s not a language trick. - It’s an object with a name (even if you don’t give it one).
Why not just static?
Because static breaks object composition.
That usually leads to the obvious question: Why?
Object composition works by wiring behavior through replaceable dependencies. Static breaks object composition because it cannot be substituted. Static methods and fields are globally fixed: they cannot be passed around, mocked, or injected. Once you depend on a static, you are hard-wired to that implementation.
Static creates hidden, global dependencies — which is the opposite of composition.
With companion objects, you can:
-
Inject behavior
-
Mock logic in tests
-
Implement interfaces
-
Keep related logic grouped with the type
Things static simply cannot do.
Java interoperability
From Java, a companion object looks like this:
User.Companion.anonymous();
int max = User.Companion.MAX_NAME_LENGTH;
If that feels verbose, Kotlin has you covered:
class User(val name: String) {
companion object {
@JvmStatic
fun anonymous(): User =
User("Anonymous")
}
}
Now Java can call:
User.anonymous();
Why this matters
Kotlin replaces a language keyword with a concept. That concept is more powerful — and more consistent.
You don’t lose statics. You gain objects.
Takeaway
Java answers the question:
“Does this belong to the class?”
Kotlin asks another one:
“What kind of thing is this, really?”
Final note
@JvmStatic tells the Kotlin compiler to generate a real Java static member…
Without it:
class User {
companion object {
fun anonymous(): User = User()
}
}
Java would see: User.Companion.anonymous();
With @JvmStatic Java sees:
User.anonymous(); // generated static method
User.Companion.anonymous(); // still exists
Perhaps interesting to know: during Kotlin Dev Day, JetBrains mentioned that in the future companion objects might get a “static sibling” that works almost like Java statics:
class User(val name: String) {
companion fun anonymous(): User = User("Anonymous")
companion {
fun evenMoreAnonymous(): User = User("Anonymous")
}
}
This is not yet finalized.
JetBrains explained that in 95% of public GitHub code, the “object” part of companion objects is not used.
The idea is to make the object part optional, as many developers find it confusing.
In short: Kotlin is exploring a way to make companion objects simpler and more accessible, while retaining their power.