
Kotlin Under the Hood: Exploring Objects, Companion Objects, and Annotations: @JvmStatic, @JvmField, @JvmOverloads
Kotlin Under the Hood: Exploring Objects, Companion Objects, and Annotations: @JvmStatic, @JvmField, @JvmOverloads êŽë š

Hello! In this article, we will explore the inner workings of objects and companion objects in Kotlin, along with the annotations@JvmStatic
,@JvmField
, and@JvmOverloads
.
Info
I previously published an article on how Kotlinâs constructors and init blocks function under the hood, which you can read here:
Before diving into the inner workings, letâs first understand what an object and a companion object are.
Object
An object in Kotlin is primarily used to create singleton behavior, but it also serves other purposes, such as defining factory methods and creating anonymous objects.
- An object can be defined inside a class or outside of it, meaning it can be placed anywhere in the code.
- *An object is instantiated lazily, meaning it is created only when accessed for the first time.*Weâll explore how this works when we dive deeper.
object Utils {
fun getVersion(): String {
return "1.0.0"
}
}
In this example, we define a singleton object namedUtils
. ThegetVersion()
function returns a version.
Companion Object
A companion object is tied to a class in Kotlin, allowing us to define static members and methods similar to those in Java.
- A companion object can only be defined within classes.
- The companion object is instantiated as soon as the containing class is loaded, meaning it is created even if we havenât accessed the companion object.
- You can omit the name for a companion object; if you do, it will default to the name
Companion
.
class Settings {
companion object Utils {
fun getVersion(): String {
return "1.0.0"
}
}
}
You can call the method in two ways:
- With the name of the companion object:
Settings.Utils.getVersion()
- Without specifying the name:
Settings.getVersion()
If you do not give a name to the companion object, you can access it using:
Settings.Companion.getVersion()
- Or simply:
Settings.getVersion()
Decoding Object & Companion Object
Now, letâs take a closer look at how it all works under the hood. To gain deeper insights, we can use IntelliJ IDEAâs decompilation feature. By navigating toTools -> Kotlin -> Kotlin Bytecodeand selectingDecompile, we can view the underlying Java code generated from our Kotlin constructs.
Letâs see what happens when we create an object in Kotlin:
object Utils {
fun getVersion(): String {
return "1.0.0"
}
}
Hereâs the underlying Java code generated from this Kotlin code:
public final class Utils {
@NotNull
public static final Utils INSTANCE;
@NotNull
public final String getVersion() {
return "1.0.0";
}
private Utils() {
}
static {
Utils var0 = new Utils();
INSTANCE = var0;
}
}
Iâve simplified it by removing assertions and other metadata for clarity.
So, what do we observe here?
- The Kotlin
object
declaration translates to afinal
Java class,Utils
. - A static variable,
INSTANCE
, holds the single instance of theUtils
class, adhering to the singleton design pattern. - The private constructor prevents external instantiation, ensuring that the only way to access the instance is through
INSTANCE
. - Inside the static block, a new instance of
Utils
is created and assigned toINSTANCE
, ensuring that this instance is created only once. - Also, we have the
getVersion()
method.
Note
In Java, a static{}
block is known as a static initialization block. It allows you to execute a block of code when the class is loaded, before any instances of the class are created or any static methods are called.
Now, letâs see what happens when we create companion objects. Consider the following code:
class Setting {
object Utils {
fun getVersion(): String {
return "1.0.0"
}
}
companion object MyUtils {
fun generateUniqueId(): String {
return UUID.randomUUID().toString()
}
}
}
Hereâs the Java code generated from this Kotlin code:
public final class Setting {
@NotNull
public static final MyUtils MyUtils = new MyUtils((DefaultConstructorMarker) null);
public static final class Utils {
@NotNull
public static final Utils INSTANCE;
@NotNull
public final String getVersion() {
return "1.0.0";
}
private Utils() {
}
static {
Utils var0 = new Utils();
INSTANCE = var0;
}
}
public static final class MyUtils {
@NotNull
public final String generateUniqueId() {
String var10000 = UUID.randomUUID().toString();
return var10000;
}
private MyUtils() {
}
// $FF: synthetic method
public MyUtils(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
So, whatâs happening?
- First, the
MyUtils
class is instantiated when theSetting
class is loaded, not when itâs accessed. However, we can observe that the instance of theUtils
object is created only when it is accessed for the first time. This is why it is said that the companion object is instantiated eagerly when the containing class is loaded, while the regular object is instantiated lazily. - The
Utils
object behaves like a singleton, similar to the previous example. - The companion object is represented as a static inner class.
- The
private MyUtils()
constructor ensures that this class cannot be instantiated from outside. - The synthetic method is added by the compiler to handle certain use cases in Kotlin.
@JvmStatic
This annotation tells the Kotlin compiler to generate an additional static method for a function or static getter/setter methods for a property.
- It only works in companion objects or object declarations.
- It is useful when we want to access Kotlin method/property from Java code.
- The annotation only affects how the code is compiled to bytecode; it has no effect on how the code behaves in Kotlin itself.
Letâs take our previous example and see how to call thegetVersion()
method ofUtils
from Java without using@JvmStatic
.
In Kotlin, you can call it directly like this:
val version = Setting.Utils.getVersion()
However, in Java, if you try to do the same:
String version = Setting.Utils.getVersion(); // This will give an error
Youâll encounter an error. Instead, you need to access the method like this:
String version = Setting.Utils.INSTANCE.getVersion();
Why Does This Work Differently?
As we previously discussed regarding the under-the-hood code of objects, when we create an object in Kotlin, a final class is generated in Java for that object. This class contains a static variable,INSTANCE
, which holds the single instance of theUtils
class & compiler generates a private constructor to prevent instantiation from outside the class, ensuring that the only way to access this instance is throughINSTANCE
. Therefore, in Java, we need to callINSTANCE
if we do not use@JvmStatic
.
Now, letâs use@JvmStatic
in our previous example:
class Setting {
object Utils {
@JvmStatic
fun getVersion(): String {
return "1.0.0"
}
}
companion object MyUtils {
@JvmStatic
fun generateUniqueId(): String {
return UUID.randomUUID().toString()
}
}
}
Accessing Methods from Java:
With@JvmStatic
, we can now access these methods directly without needing to reference an instance variable:
- To get the version from the
Utils
object, you can simply call:
String version = Setting.Utils.getVersion();
- To generate a unique ID from the companion object, you can call:
String uid = Setting.generateUniqueId();
// You can also specify the companion objectâs name if you want:
String uid = Setting.MyUtils.generateUniqueId();
Now you might wonder how the underlying code looks.
Whatâs the observation?
- An extra static method is created in the parent class for the companion object (
generateUniqueId()
). - The method in the
Utils
object is treated differently. Instead of creating an extra static method, thegetVersion()
method is made static.
@JvmField
Using@JvmField
tells the Kotlin compiler not to create getters and setters for a property. Instead, it allows you to access the property directly like a regular field in Java.
Letâs explore this with an example.
class Utils {
val version: String = "1.0.0"
}
In Kotlin, you can access theversion
property easily:
val version = Utils().version
However, if you try to access it from Java like this:
Utils utils = new Utils();
String version = utils.version; // This will give you an error!
Youâll encounter a compilation error. Why is that? Letâs take a look under the hood.
Underlaying java code:
public final class Utils {
@NotNull
private final String version = "1.0.0";
@NotNull
public final String getVersion() {
return this.version;
}
}
Here, you can see that Kotlin generates a private field forversion
and provides a public getter method. Since the field is private, Java cannot access it directly.
Now, letâs see what happens when we apply@JvmField
to our property:
class Utils {
@JvmField
val version: String = "1.0.0"
}
Now you can access theversion
property from Java.
Utils utils = new Utils();
String version = utils.version; // This works!
Decompiled code with@JvmField
, it looks like this:
public final class Utils {
@JvmField
@NotNull
public final String version = "1.0.0";
}
version
is now a public field, with no generated getter. This makes it accessible directly from Java.
@JvmOverloads
It instructs the compiler to generate multiple overloads of a function based on its default parameter values.
class Repository {
fun getData(category: String = "default", page: Int = 1, includeTranslation: Boolean = false) {
// some code
}
}
In Kotlin, you can call thegetData()
function in different ways:
val repo = Repository()
repo.getData() // Uses all default values
repo.getData(page = 2) // Uses default for category and includeTranslation
repo.getData(includeTranslation = true) // Uses default for category and page
However, when you try to call this function from Java, youâll run into a problem:
Repository repo = new Repository();
repo.getData(); // This will give you an error!
Underlaying Java code:
public final class Repository {
public final void getData(@NotNull String category, int page, boolean includeTranslation) {
Intrinsics.checkNotNullParameter(category, "category");
}
// ... synthetic method
public static void getData$default(Repository var0, String var1, int var2, boolean var3, int var4, Object var5) {
if ((var4 & 1) != 0) {
var1 = "default";
}
if ((var4 & 2) != 0) {
var2 = 1;
}
if ((var4 & 4) != 0) {
var3 = false;
}
var0.getData(var1, var2, var3);
}
}
In this case, the method is generated only with all parameters. Java doesnât support default parameters, so you need to provide values for all of them.
Info
Kotlin generates a synthetic method to handle default parameters, but this method isnât accessible from Java. This synthetic method, likegetData$default
, uses bitwise operations to determine which default values to apply based on the parameters provided.
Now, letâs see what happens when we apply@JvmOverloads
:
class Repository {
@JvmOverloads
fun getData(category: String = "default", page: Int = 1, includeTranslation: Boolean = false) {
// some code
}
}
Underlaying Java code:
public final class Repository {
@JvmOverloads
public final void getData(@NotNull String category, int page, boolean includeTranslation) {
}
@JvmOverloads
public final void getData(@NotNull String category, int page) {
getData$default(this, category, page, false, 4, null);
}
@JvmOverloads
public final void getData(@NotNull String category) {
getData$default(this, category, 0, false, 6, null);
}
@JvmOverloads
public final void getData() {
getData$default(this, null, 0, false, 7, null);
}
}
- Kotlin generates additional overloaded versions of the
getData()
method. - Itâs important to note that thereâs no overload for just the
String
andboolean
parameters.As per the documentation, if a method has N parameters and M of them have default values, Kotlin generates M overloads. These overloads progressively omit parameters from the end. - In this case, since
includeTranslation
is the last parameter with a default value, Kotlin generates overloads that skip directly toString
andInt
, but you wonât see an overload for justString
andboolean
becauseInt
is in between. - Thatâs why when you try to call
getData(String, Boolean)
from Java, it will not work.
Working Combinations from Java:
repo.getData(); // â
repo.getData("default"); // Calls with page = 1, includeTranslation = false â
repo.getData("default", 2); // Calls with includeTranslation = false â
repo.getData("default", 2, true); // â
repo.getData("default", false); // This won't compile â
Thatâs it for today!
Thanks for reading this blog! đ If you want to explore more âunder the hoodâ insights and deep dives into Kotlin, be sure to follow me for future updates and posts.
Feel free to connect with me on:
Info
This article is previously published on proandroiddev.com
