Blog Post

Java-to-Kotlin Conversion Best Practices

Michael Kellner
Illustration: Java-to-Kotlin Conversion Best Practices

Sooner or later, presumably every Android developer will run into the situation where they’ll want to convert Java code to Kotlin to take advantage of its powerful features. There’s a feature in Android Studio that helps a lot with this; you can find it in the main menu under Code > Convert Java File to Kotlin File. This gets a lot of the job done, but rest assured, you won’t get away without some manual labor. So this blog post will provide you with an overview of best practices for performing code conversion.

Nullable Types

The most important thing in the process is to be aware of nullable types, which you can assign both normal values and null values to. For more insight into this topic, refer to the Kotlin documentation on null safety.

Information

Read more about this in our Handling Nullability in Your Code blog post.

As long as you’re staying within Kotlin, null safety isn’t much of a problem, because the compiler doesn’t even let you compile if you mix up nullable and non-nullable types.

That said, it’s very easy to mix up the two when, for example, calling a Java function that returns a null value and assigning it to a non-nullable Kotlin field. Because null types in Java are often insufficiently annotated, it’s common to find yourself in situations where you won’t even be expecting them.

💡 Tip: Use non-nullable types wherever possible.

The IDE conversion feature recognizes @Nullable and @NonNull annotations. It also anticipates nullability according to initial values.

Here, you start with Java:

Boolean a;
Boolean b = true;
@Nullable Boolean c = true;
Boolean d = null;

And it’s converted to Kotlin:

var a: Boolean? = null
var b = true
var c: Boolean? = true
var d: Boolean? = null

In the code above, a is just a plain Java Boolean without an initializer and annotation, so the algorithm plays it safe and defaults to nullable. b isn’t annotated, but since it’s initialized with true, the converter assumes it’s a non-nullable type. c is a no-brainer, since it’s annotated as @Nullable (the non-null initializer has no impact here), whereas d isn’t annotated, but the initialization with null implies a nullable type.

If we take a look at a function conversion, it behaves similarly.

Here’s the Java:

@NonNull List<CheckedFoo> checkFoos(List<Foo> foos){
    final ArrayList<CheckedFoo> result = new ArrayList<>();
    if (foos != null) {
        for (final Foo foo: foos) {
            if (foo != null && foo.check()) {
                result.add(new CheckedFoo(foo));
            }
        }
    }

    return result;
}

And here it is converted to Kotlin:

fun checkFoos(foos: List<Foo?>?): List<CheckedFoo> {
    val result = ArrayList<CheckedFoo>()
    if (foos != null) {
        for (foo in foos) {
            if (foo != null && foo.check())
            	result.add(CheckedFoo(foo))
        }
    }
    return result
}

If you look closely, the foos: List<Foo?>? parameter was translated to be nullable, but the contained element type <Foo?> was automatically recognized because of the following:

if (foo != null)
	result.add(foo)

This tells us that we’re expecting nulls in the list.

On the other hand, without this if clause, the converter comes up with the following function signature:

fun checkFoos(foos: List<Foo>?): List<Foo>

So, while the algorithm is pretty smart, I’d highly recommend doing your own due diligence, especially when it comes to collaborations with Java code.

Handling Nullable Properties

In Java, it’s perfectly fine to write something like this:

class Foobar{
    private Foo foo = null;
    void doFoo(){
        if (foo != null)
            foo.bar()
    }
}

In Kotlin, the converter delivers the following:

class Foobar{
    private var foo: Foo? = null;
    fun doFoo(){
        if (foo != null)
        	foo!!.bar()
    }
}

Kotlin considers that foo is mutable and might be changed between the null check and the execution of bar().

Interlude — The !! Operator

The !! operator is used to assert that an expression is non-null. I can’t think of any case where you’d really need it. But for some reason, the conversion feature makes use of it a lot. Usually, the first thing I do is get rid of every instance of it. If you want to force an exception at that point, use the Elvis operator and go down in style.

	foo?.action() ?: throw RuntimeException("Foo, thou must not be null!")

Remember when I mentioned the converter is pretty smart? Well, here, it isn’t really, as the obvious way to go is the safe call operator ?., because it’s not only safer, but it’s also shorter:

fun doFoo() = foo?.action()

The above executes action(), but only if foo != null.

If you want to execute multiple functions on an object without bothering with ?. for each call, help yourself with a scope function like let or run:

foo?.run{
    action1()
    action2()
    action3()
}

One thing I’ve gotten into the habit of doing when working with a nullable property on multiple statements within a function is to get a local non-nullable version of it:

fun workWithFoo(){
	val nnFoo = foo ?: return	// Get a non-`null` `foo` or leave.

	if (nnFoo.action1()){
		doSomething();
		nnFoo.action1()
		soSomethingElse();
		nnFoo.action2()
	}
	nnFoo.done();
}

In the code above, I only need to check nullability once, and the code is ultimately more elegant.

Read-Only Properties

Read-only properties can turn into pitfalls, so choose wisely how you initialize them. For example, the following is initialized when the containing class is created and sticks with the returned value of formatCurrentTime:

val currentTime: String = formatCurrentTime()

The following is initialized when your code first accesses currentTime and sticks with that value:

val currentTime: String by lazy{ formatCurrentTime() }

And finally, the code below is actually an alias for the function call, so every time currentTime is accessed, formatCurrentTime() is executed:

val currentTime: String get() = formatCurrentTime()

More than once, I forgot the get() and was wondering why my property didn’t return a proper value.

Functional Interfaces

Using Java single abstract method (SAM) interfaces in Kotlin is a neat way to get rid of boilerplate code for anonymous interface implementation by just writing the implementation as a lambda expression.

This has also been possible for Kotlin interfaces since version 1.4. If you have interfaces with exactly one abstract method, define them as functional interfaces, as the converter won’t consider that they might be converted to functional interfaces already. So, if you’re converting an eligible Java interface, prefix it with fun.

Let’s assume we have the following Kotlin code:

fun interface Foo{
	fun bar(s: String)
}

fun callFoo(f: Foo){
	f.bar("hello World")
}

In the old style, we’d implement the interface like this:

callFoo(object : Foo {
    override bar(s: String){
        print(s)
    }
})

Meanwhile, a functional interface allows this:

callFoo{ print(it) }

It also allows this:

callFoo(::print)

The latter two examples show how you can shorten your code by using a functional interface instead of a regular one, which results in improved readability.

Kotlin Standard Library

You get standard operations for looping, finding, filtering, and transforming for free with Kotlin. So, use the standard library wherever you can — especially when working with collections — as there’s almost no use case that hasn’t already been taken care of.

Naturally, there are way too many collection operations to go into detail about here, but the collection operations overview documentation is a good starting point for learning more.

Here’s a small example of how the function from the first example could be rewritten:

fun checkFoos(foos: List<Foo?>?): List<CheckedFoo> =
    foos?.mapNotNull {
        it?.takeIf { it.check() }?.let { CheckedFoo(it) }
    } ?: emptyList()

The code above produces the same results as the code from the first example. However, this one uses functions from the Kotlin standard library and is shorter.

Preserving Your Git History

In addition to all the converting, don’t forget about what happens to the repository due to the code migration. Unfortunately, Android Studio’s implementation of the conversion feature is flawed when it comes to Git. The problem is that it deletes the original Java file and creates a new Kotlin file. Boom, Git history gone.

The proper way would be to rename the file using the following:

git mv file.java file.kt

There are two simple ways you can do this within Android Studio.

No Extra Tools Needed

The first way requires no extra tools. You need to:

  • Open the Java file in Android Studio.

  • Copy its contents to the clipboard.

  • Right-click the Editor tab of the file, choose Rename File…, and change the extension from .java to .kt. This will properly rename the file in Git.

  • Eventually paste the clipboard’s contents over the content in the editor. Android Studio will recognize it as Java code and ask if you want to convert it to Kotlin.

Conversion Plugin

The second way is using a handy IDE plugin named Multiple File Kotlin Converter that’s available within the IDE Plugin Marketplace (File > Settings > Plugins). After installation, it’ll also show up in the Code menu.

Under the hood, this plugin uses the already-known conversion feature from Android Studio, but it does the proper renaming operation on Git before doing the actual code transformation.

Final Words

This article is intended to help you get started with Java-to-Kotlin conversions. However, it’s not a complete guide, as there’s so much more you can do with Kotlin — like using extensions or concurrency with coroutines, for starters. However, with some of these tips in mind, you’ll have more context when doing conversions and when taking that first step toward runnable and more concise Kotlin code.

Share Post
Free 60-Day Trial Try PSPDFKit in your app today.
Free Trial

Related Articles

Explore more
PRODUCTS  |  Android • Releases

Android 2024.1 Update: Advanced Content Editing and Digital Signatures, Plus Expanded Jetpack Compose Support

TUTORIALS  |  Android • How To

How to Persist Zoom While Scrolling through a Document Using the PSPDFKit Android Library

CUSTOMER STORIES  |  Case Study • React Native • iOS • Android

Case Study: How Trinoor Uses PSPDFKit to Drive Operational Excellence with Flexible, Mobile Applications for the Energy and Utilities Market