From Java to Kotlin — a practical example of how to convert code

Paul Newport
6 min readJan 21, 2024

--

Converting your code from Java to Kotlin is well worth the effort. The final code can be null safe, and with a bit of tweaking, immutable as well. It should also be a lot more compact, and easier to understand as a result.

In this example a very simple application is converted via IntelliJ’s convert to Kotlin, and then modified to be more idiomatic. As a final step the unit tests are not only converted from Java to Kotlin, but also from Junit to the excellent Kotest framework. When converting code for real there is a decision to be made as to whether to convert test code first or last. There are many conflicting arguments on this, but the important thing to have before any conversion is code with as high test coverage as you can get.

When converting code, don’t forget to move it from the Java source directory to the Kotlin one. At the end of the process the Java source directory should be empty, and it can be deleted.

Here’s the initial Java code:

package pn.medium.conversion;

@Data
public class Person {

private String id;
private String firstName;
private String secondName;
private String lastName;

}

public class PersonRepository {

public List<Person> findAll() {
var person = new Person();
person.setId("1");
person.setFirstName("Fred");
person.setSecondName("Jones");
return List.of(person);
}

public Optional<Person> findPersonById(String id) {
return findAll().stream().filter(person-> Objects.equals(person.getId(), id)).findFirst();
}

public List<Person> getDaves() {

var daves = new ArrayList<Person>();
var dave = new Person();
dave.setId("2");
dave.setFirstName("Dave");
dave.setLastName("Jones");
daves.add(dave);

var dave2 = new Person();
dave2.setId("3");
dave2.setFirstName("Dave");
dave2.setLastName("Smith");
daves.add(dave2);
return daves;
}
}

There’s not a lot to it; a Person class with a zero arg constructor and some getters and setters, and a repository class with methods that return either lists or optionals.

A first cut conversion can be achieved by right-clicking on a class in IntelliJ, and taking the “convert Java file to Kotlin” option.

Here’s what IntelliJ generates when you convert the Person class:

@Data
class Person {
private val id: String? = null
private val firstName: String? = null
private val secondName: String? = null
private val lastName: String? = null
}

This is OK but a bit messy. IntelliJ had no idea as to which properties were nullable, so it had to make all of them nullable. This is one of the biggest issues when converting: how to handle nulls. In the Java world nullability is an afterthought at best, with code often having to indication of nullability, and no checking either way. On an initial conversion, if you want to be completely safe, leave everything as nullable to start with.

In the example above though, we can change first and last name to be not nullable and leave the rest as nullable.

Depending on whether or not the original code used Lombok or not, IntelliJ might also make all the fields vars. Mutability is generally a bad thing in code (I will have an upcoming post on this), so let’s make sure everything is a val, and what’s more, have an all args constructor so you can’t create an instance with only some of the properties set. As the class is a simple one, we’ll add the data keyword to generate convenience methods like toString and hashCode for us. Don’t forget to remove the Lombok annotation as this is now redundant.

data class Person (
val id: String?,
val firstName: String,
val secondName: String?,
val lastName: String
)

Moving on the repository class conversion. We can see by changing Person to be a data class with val rather than var, we can no longer call the default constructor and set properties one by one.

Let’s fix this function first:

fun findAll(): List<Person> {
val person = Person()
person.id = "1"
person.firstName = "Fred"
person.secondName = "Jones"
return java.util.List.of(person)
}

Whilst we’re at it, convert List.of to use Kotlin’s listOf function instead. For these very simple functions, it’s much nicer to get rid of the return as well, and let type inference work out the return type for you as well, resulting in this:

fun findAll() = listOf(Person("1", "Fred", null, "Jones"))

On to the next function:

fun findPersonById(id: String): Optional<Person> {
return findAll().stream().filter {
person: Person -> person.id == id }.findFirst()
}

Here the code is still using Java streams. Java typically has to convert a collection to a stream via the stream() function and will often have a collect on the other end to convert the stream back into a collection. Neither are needed in Kotlin. The code also returns an Optional. As Kotlin has first class support for nulls, we can change the code to return a nullable Person instead. Here’s the revised code:

fun findPersonById(id: String): Person? =
findAll().firstOrNull { person: Person -> person.id == id }

For clarity the return type of Person? has been left in, although it’s not actually needed. Kotlin has also provided a handy firstOrNull function, that is the equivalent of the Java filter and findFirst. Such combined functions are very common, so keep a look at for suggestions from IntelliJ here.

On to the last function:

fun findDaves(): List<Person> {
val daves = ArrayList<Person>()
val dave = Person()
dave.id = "2"
dave.firstName = "Dave"
dave.lastName = "Jones"
daves.add(dave)

val dave2 = Person()
dave2.id = "3"
dave2.firstName = "Dave"
dave2.lastName = "Smith"
daves.add(dave2)
return daves
}

There are compile errors here, due to the conversion of Person into a data class. This is fixed easily enough, but there’s another issue. The code is creating an ArrayList, which is mutable, and adding in items one by one. This goes against Kotlin’s preference for immutable by default, so this can be fixed by simply using the listOf function, which creates an immutable list.

fun findDaves(): List<Person> = listOf(
Person("2", "Dave", null, "Jones"),
Person("3", "Dave", null, "Smith"),
)

Note the comma on the end of Dave Smith’s entry. In Kotlin you can have trailing commas on lists; this means that if we ever want to add another entry, we only have to add one line, rather than add one and change the previous one.

After the main code has been converted, it’s time to look at the test code. Here’s what it looks like in Java, using Junit5.


@Getter
@Setter
@AllArgsConstructor
@ToString
class Data {
private String input;
private Boolean found;
}

class PersonRepositoryTest {

PersonRepository personRepository = new PersonRepository();

public static List<Data> data() {
return List.of(new Data("1",true),new Data("2",false));
}

@Test
void findAll() {
var actual = personRepository.findAll();

var person = new Person();
person.setId("1");
person.setFirstName("Fred");
person.setSecondName("Jones");
var expected = List.of(person);
Assertions.assertIterableEquals(actual, expected);
}

@Test
void findDaves() {
var actual = personRepository.findDaves();
assertEquals(actual.size(),2);
}

@ParameterizedTest
@MethodSource("data")
void findById(Data data) {
assertEquals( personRepository.findPersonById(data.getInput()).isPresent(),data.getFound());
}
}

Junit5 relies heavily on annotations. For data driven tests the method providing the data either needs to be static, or the class needs to annotated with @TestInstance(PER_CLASS). In the Kotlin world, an excellent alternative is the Kotest test framework, and its associated assertions, which tend to be either extension or infix functions, making them a lot more readable. With Kotest there’s no need for static code or annotations; the framework is nicely functional.

Here’s the conversion. If you are coding in IntelliJ, make sure to download the Kotest plugin so you can run the tests in the IDE. For gradle the tests will run just like existing Junit tests.


class PersonRepositoryTest : FunSpec({

data class Data(val input: String, val found: Boolean)

val personRepository = PersonRepository()

test("Find all") {
val actual = personRepository.findAll()

val expected = listOf(Person("1", "Fred", null, "Jones"))
actual shouldBe expected
}

test("Find Daves") {
val actual = personRepository.findDaves()
actual.shouldHaveSize(2)
}

context("Test finding by ID") {
withData(Data("1", true), Data("2", false)) {
personRepository.findPersonById(it.input) shouldBe it.found
}
}
}
)

After converting the project, the code is in a much better state. It’s null safe, and there is no longer any mutabilty in the code. It’s also almost half the size compared to the original. Once you’ve done an initial conversion you can go ahead and take advantage of all the great features of Kotlin, without being restrained by whatever JDK version that you are currently tied to.

The converted code for this project is in GIT here:

https://github.com/frayneposset/Medium

Good luck in your conversions !

--

--

Paul Newport
Paul Newport

Written by Paul Newport

Software developer since the stone age. Kotlin and Spring fan.

No responses yet