1. Overview
In this tutorial, we’ll learn about Kotlin’s companion object and how to access them from Java using some simple examples. We’ll also see the decompiled Kotlin bytecode – the equivalent Java code that the compiler generates – for each of the examples.
If using IntelliJ, we can access the generated code from, Tools → Kotlin → Show Kotlin Bytecode → Decompile. It’s okay to skip the decompiled code during the initial reading of this article.
2. Declaring a companion object
In Kotlin, we can easily create a thread-safe singleton using the object declaration. If we declare the object inside a class, we have an option to mark it as a companion object. In terms of Java, the members of the companion object can be accessed as static members of the class. Marking an object as a companion allows us to omit the object’s name while calling its members.
Let’s see a quick example that shows how to declare a companion object in Kotlin:
class MyClass {
companion object {
val age: Int = 22
}
}
In the above sample, we have declared a companion object. Note that we’ve omitted its name.
3. Accessing Properties of companion object from Java
As Kotlin is 100% interoperable with Java, there’s also a provision to access properties of companion objects as static fields from a Java class.
3.1. @JvmField Annotation
We can mark a property in a Kotlin class with the @JvmField annotation to let the Kotlin compiler know that we want to expose it as a static field:
class FieldSample {
companion object{
@JvmField
val age : Int = 22
}
}
Now, let’s access it from a Java class:
public class Main {
public static void main(String[] args) {
System.out.println(FieldSample.age);
}
}
In the above example, we marked the version property with the @JvmField annotation. Then, we were able to access it in our Java class directly. Note that if we just see the Java code, then it appears as if we’re accessing a static field of AppDatabase.
Let’s see the decompiled Kotlin bytecode for the FieldSample class:
public final class FieldSample {
@JvmField
public static int age = 22;
@NotNull
public static final FieldSample.Companion Companion = new FieldSample.Companion((DefaultConstructorMarker)null);
public static final class Companion {
private Companion() {
}
...
}
}
We can see from the above snippet that the Kotlin compiler generates a static field for the property marked with the @JvmField annotation.
3.2. Special Case – lateinit Modifier
Variables defined as lateinit are special because when defined as part of a companion object, a static backing field is created for each of them. This static backing field has the same visibility as that of the lateinit variable. So, if we define a public lateinit variable, we can access it in our Java code.
Let’s write a quick Kotlin class that declares both private and public lateinit variables:
class LateInitSample {
companion object{
private lateinit var password : String
lateinit var userName : String
fun setData(pair: Pair<String,String>){
password = pair.first
userName = pair.second
}
}
}
As shown in the following Java snippet, we can only access the lateinit variable that we declared as public:
public class Main {
static void callLateInit() {
System.out.println(LateInitSample.userName);
//System.out.println(LateInitSample.password); compilation error
}
}
Let’s see an excerpt of the decompiled Kotlin bytecode for the LateInitSample class:
public final class LateInitSample {
private static String password;
public static String userName;
@NotNull
public static final LateInitSample.Companion Companion = new LateInitSample.Companion((DefaultConstructorMarker)null);
public static final class Companion {
@NotNull
public final String getUserName() {
String var10000 = LateInitSample.userName;
if (var10000 == null) {
Intrinsics.throwUninitializedPropertyAccessException("userName");
}
return var10000;
}
public final void setUserName(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
LateInitSample.userName = var1;
}
public final void setData(@NotNull Pair pair) {
Intrinsics.checkNotNullParameter(pair, "pair");
LateInitSample.password = (String)pair.getFirst();
((LateInitSample.Companion)this).setUserName((String)pair.getSecond());
}
private Companion() {
}
}
}
From the generated code above, we can see that the compiler generates static fields for the lateinit variables. The visibility is the same as that of the actual fields.
3.3. Special Case – const Modifier
We can define a compile-time constant in Kotlin by applying the const keyword. If we’ve defined a constant in Java, we know that all such fields are static final. Kotlin’s const is equivalent to a Java static final field. Let’s see sample code to define a const variable in a companion object and how to access it in Java:
class ConstSample {
companion object{
const val VERSION : Int = 100
}
}
public class Main {
static void callConst() {
System.out.println(ConstSample.VERSION);
}
}
Let’s see the decompiled Kotlin bytecode for the ConstSample class:
public final class ConstSample {
public static final int VERSION = 100;
@NotNull
public static final ConstSample.Companion Companion = new ConstSample.Companion((DefaultConstructorMarker)null);
public static final class Companion {
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
As we can see, the compiler internally converts the VERSION field into a static final field in Java.
4. Accessing the companion object’s Methods
To access a companion object’s methods from Java, we need to mark the methods with the @JvmStatic annotation. Let’s see an example:
class MethodSample {
companion object {
@JvmStatic
fun increment(num: Int): Int {
return num + 1
}
}
}
public class Main {
public static void main(String[] args) {
MethodSample.increment(1);
}
}
This annotation tells the Kotlin compiler to generate a static method in the enclosing class. Let’s see an excerpt of the decompiled Kotlin bytecode:
public final class MethodSample {
@NotNull
public static final MethodSample.Companion Companion = new MethodSample.Companion((DefaultConstructorMarker)null);
@JvmStatic
public static final int increment(int num) {
return Companion.increment(num);
}
public static final class Companion {
@JvmStatic
public final int increment(int num) {
return num + 1;
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
In the above snippet, we can see that, internally, the Kotlin compiler is calling the Companion.increment() method from the enclosing class’s generated static increment method.
5. Conclusion
In this article, we saw how to use the members of a Kotlin companion object from Java. We also saw the generated bytecode to get a better understanding of these features.
The code samples are also available over on GitHub.