About this codelab
1. Welcome!
In this codelab, you'll learn how to convert your code from Java to Kotlin. You'll also learn what the Kotlin language conventions are and how to ensure that the code you're writing follows them.
This codelab is suited to any developer that uses Java who is considering migrating their project to Kotlin. We'll start with a couple of Java classes that you'll convert to Kotlin using the IDE. Then we'll take a look at the converted code and see how we can improve it by making it more idiomatic and avoid common pitfalls.
What you'll learn
You will learn how to convert Java to Kotlin. In doing so you will learn the following Kotlin language features and concepts:
- Handling nullability
- Implementing singletons
- Data classes
- Handling strings
- Elvis operator
- Destructuring
- Properties and backing properties
- Default arguments and named parameters
- Working with collections
- Extension functions
- Top-level functions and parameters
let
,apply
,with
, andrun
keywords
Assumptions
You should already be familiar with Java.
What you'll need
2. Getting set up
Create a new project
If you're using IntelliJ IDEA, create a new Java project with Kotlin/JVM.
If you're using Android Studio, create a new project with no Activity.
The code
We'll create a User
model object and a Repository
singleton class that works with User
objects and exposes lists of users and formatted user names.
Create a new file called User.java
under app/java/<yourpackagename> and paste in the following code:
public class User {
@Nullable
private String firstName;
@Nullable
private String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
Depending on your project type, import androidx.annotation.Nullable
if you use an Android project, or org.jetbrains.annotations.Nullable
otherwise.
Create a new file called Repository.java
and paste in the following code:
import java.util.ArrayList;
import java.util.List;
public class Repository {
private static Repository INSTANCE = null;
private List<User> users = null;
public static Repository getInstance() {
if (INSTANCE == null) {
synchronized (Repository.class) {
if (INSTANCE == null) {
INSTANCE = new Repository();
}
}
}
return INSTANCE;
}
// keeping the constructor private to enforce the usage of getInstance
private Repository() {
User user1 = new User("Jane", "");
User user2 = new User("John", null);
User user3 = new User("Anne", "Doe");
users = new ArrayList();
users.add(user1);
users.add(user2);
users.add(user3);
}
public List<User> getUsers() {
return users;
}
public List<String> getFormattedUserNames() {
List<String> userNames = new ArrayList<>(users.size());
for (User user : users) {
String name;
if (user.getLastName() != null) {
if (user.getFirstName() != null) {
name = user.getFirstName() + " " + user.getLastName();
} else {
name = user.getLastName();
}
} else if (user.getFirstName() != null) {
name = user.getFirstName();
} else {
name = "Unknown";
}
userNames.add(name);
}
return userNames;
}
}
3. Declaring nullability, val, var and data classes
Our IDE can do a pretty good job of automatically refactoring Java code into Kotlin code but sometimes it needs a little help. We'll do this first and then go through the refactored code to understand how and why it has been refactored this way.
Go to the User.java
file and convert it to Kotlin: Menu bar -> Code -> Convert Java File to Kotlin File.
If your IDE prompts for correcting after conversion, press Yes.
You should see the following Kotlin code: :
class User(var firstName: String?, var lastName: String?)
Note that User.java
was renamed to User.kt
. Kotlin files have the extension .kt.
In our Java User
class we had two properties: firstName
and lastName
. Each had a getter and setter method, making its value mutable. Kotlin's keyword for mutable variables is var
, so the converter uses var
for each of these properties. If our Java properties had only getters, they would be immutable and would have been declared as val
variables. val
is similar to the final
keyword in Java.
One of the key differences between Kotlin and Java is that Kotlin explicitly specifies whether a variable can accept a null value. It does this by appending a `?
` to the type declaration.
Because we marked firstName
and lastName
as nullable, the auto-converter automatically marked the properties as nullable with String?
. If you annotate your Java members as non-null (using org.jetbrains.annotations.NotNull
or androidx.annotation.NonNull
), the converter will recognize this and make the fields non-null in Kotlin as well.
The basic refactoring is already done. But we can write this in a more idiomatic way. Let's see how.
Data class
Our User
class only holds data. Kotlin has a keyword for classes with this role: data
. By marking this class as a data
class, the compiler will automatically create getters and setters for us. It will also derive the equals()
, hashCode()
, and toString()
functions.
Let's add the data
keyword to our User
class:
data class User(var firstName: String, var lastName: String)
Kotlin, like Java, can have a primary constructor and one or more secondary constructors. The one in the example above is the primary constructor of the User class. If you're converting a Java class that has multiple constructors, the converter will automatically create multiple constructors in Kotlin as well. They are defined using the constructor
keyword.
If we want to create an instance of this class, we can do it like this:
val user1 = User("Jane", "Doe")
Equality
Kotlin has two types of equality:
- Structural equality uses the
==
operator and callsequals()
to determine if two instances are equal. - Referential equality uses the
===
operator and checks if two references point to the same object.
The properties defined in the primary constructor of the data class will be used for structural equality checks.
val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false
4. Default arguments, named arguments
In Kotlin, we can assign default values to arguments in function calls. The default value is used when the argument is omitted. In Kotlin, constructors are also functions, so we can use default arguments to specify that the default value of lastName
is null
. To do this, we just assign null
to lastName
.
data class User(var firstName: String?, var lastName: String? = null)
// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("John", "Doe")
Function parameters can be named when calling functions:
val john = User(firstName = "John", lastName = "Doe")
As a different use case, let's say that the firstName
has null
as its default value and lastName
does not. In this case, because the default parameter would precede a parameter with no default value, you would have to call the function with named arguments:
data class User(var firstName: String? = null, var lastName: String?)
// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")
5. Object initialization, companion object and singletons
Before going forward, make sure that your User
class is a data
class. Let's convert the Repository
class to Kotlin. The automatic conversion result should look like this:
import java.util.*
class Repository private constructor() {
private var users: MutableList<User?>? = null
fun getUsers(): List<User?>? {
return users
}
val formattedUserNames: List<String?>
get() {
val userNames: MutableList<String?> =
ArrayList(users!!.size)
for (user in users) {
var name: String
name = if (user!!.lastName != null) {
if (user!!.firstName != null) {
user!!.firstName + " " + user!!.lastName
} else {
user!!.lastName
}
} else if (user!!.firstName != null) {
user!!.firstName
} else {
"Unknown"
}
userNames.add(name)
}
return userNames
}
companion object {
private var INSTANCE: Repository? = null
val instance: Repository?
get() {
if (INSTANCE == null) {
synchronized(Repository::class.java) {
if (INSTANCE == null) {
INSTANCE =
Repository()
}
}
}
return INSTANCE
}
}
// keeping the constructor private to enforce the usage of getInstance
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users = ArrayList<Any?>()
users.add(user1)
users.add(user2)
users.add(user3)
}
}
Let's see what the automatic converter did:
- An
init
block was added (Repository.kt#L50) - The
static
field is now part of acompanion object
block (Repository.kt#L33) - The list of
users
is nullable since the object wasn't instantiated at declaration time (Repository.kt#L7) - The
getFormattedUserNames()
method is now a property calledformattedUserNames
(Repository.kt#L11) - The iteration over the list of users (that was initially part of
getFormattedUserNames(
) ) has a different syntax than the Java one (Repository.kt#L15)
Before we go further, let's clean up the code a bit. We can see that the converter made our users
list a mutable list that holds nullable objects. While the list can indeed be null, let's say that it can't hold null users. So let's do the following:
- Remove the
?
inUser?
within theusers
type declaration getUsers
should returnList<User>?
The auto-converter also unnecessarily split into 2 lines the variable declarations of the users variables and of the ones defined in the init block. Let's put each variable declaration on one line. Here's how our code should look like:
class Repository private constructor() {
private var users: MutableList<User>? = null
fun getUsers(): List<User>? {
return users
}
val formattedUserNames: List<String?>
get() {
val userNames: MutableList<String?> =
ArrayList(users!!.size)
for (user in users) {
var name: String
name = if (user!!.lastName != null) {
if (user!!.firstName != null) {
user!!.firstName + " " + user!!.lastName
} else {
user!!.lastName
}
} else if (user!!.firstName != null) {
user!!.firstName
} else {
"Unknown"
}
userNames.add(name)
}
return userNames
}
companion object {
private var INSTANCE: Repository? = null
val instance: Repository?
get() {
if (INSTANCE == null) {
synchronized(Repository::class.java) {
if (INSTANCE == null) {
INSTANCE =
Repository()
}
}
}
return INSTANCE
}
}
// keeping the constructor private to enforce the usage of getInstance
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users = ArrayList<Any?>()
users.add(user1)
users.add(user2)
users.add(user3)
}
}
Init block
In Kotlin, the primary constructor cannot contain any code, so initialization code is placed in init
blocks. The functionality is the same.
class Repository private constructor() {
...
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users = ArrayList<Any?>()
users.add(user1)
users.add(user2)
users.add(user3)
}
}
Much of the init
code handles initializing properties. This can also be done in the declaration of the property. For example, in the Kotlin version of our Repository
class, we see that the users property was initialized in the declaration.
private var users: MutableList<User>? = null
Kotlin's static
properties and methods
In Java, we use the static
keyword for fields or functions to say that they belong to a class but not to an instance of the class. This is why we created the INSTANCE
static field in our Repository
class. The Kotlin equivalent for this is the companion object
block. Here you would also declare the static fields and static functions. The converter created and moved the INSTANCE
field here.
Handling singletons
Because we need only one instance of the Repository
class, we used the singleton pattern in Java. With Kotlin, you can enforce this pattern at the compiler level by replacing the class
keyword with object
.
Remove the private constructor and the companion object and replace the class definition with object Repository
.
object Repository {
private var users: MutableList<User>? = null
fun getUsers(): List<User>? {
return users
}
val formattedUserNames: List<String>
get() {
val userNames: MutableList<String> =
ArrayList(users!!.size)
for (user in users) {
var name: String
name = if (user!!.lastName != null) {
if (user!!.firstName != null) {
user!!.firstName + " " + user!!.lastName
} else {
user!!.lastName
}
} else if (user!!.firstName != null) {
user!!.firstName
} else {
"Unknown"
}
userNames.add(name)
}
return userNames
}
// keeping the constructor private to enforce the usage of getInstance
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users = ArrayList<Any?>()
users.add(user1)
users.add(user2)
users.add(user3)
}
}
When using the object
class, we just call functions and properties directly on the object, like this:
val users = Repository.users
Destructuring
Kotlin allows destructuring an object into a number of variables, using a syntax called destructuring declaration. We create multiple variables and can use them independently.
For example, data classes support destructuring so we can destructure the User
object in the for
loop into (firstName, lastName)
. This allows us to work directly with the firstName
and lastName
values. Let's update the for
loop like this:
for ((firstName, lastName) in users!!) {
val name: String?
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
} else {
name = lastName
}
} else if (firstName != null) {
name = firstName
} else {
name = "Unknown"
}
userNames.add(name)
}
6. Handling nullability
When converting the Repository
class to Kotlin, the automatic converter made the list of users nullable, because it wasn't initialized to an object when it was declared. For all the usages of the users
object, the not-null assertion operator !!
is used. It converts any variable to a non-null type and throws an exception if the value is null. By using !!
, you're risking exceptions being thrown at runtime.
Instead, prefer handling nullability by using one of these methods:
- Doing a null check (
if (users != null) {...}
) - Using the elvis operator
?:
(covered later in the codelab) - Using some of the Kotlin standard functions (covered later in the codelab)
In our case, we know that the list of users doesn't need to be nullable, since it's initialized right after the object is constructed, so we can directly instantiate the object when we declare it.
When creating instances of collection types, Kotlin provides several helper functions to make your code more readable and flexible. Here we're using a MutableList
for users
:
private var users: MutableList<User>? = null
For simplicity, we can use the mutableListOf()
function, provide the list element type, remove the ArrayList
constructor call from the init
block, and remove the explicit type declaration of the users
property.
private val users = mutableListOf<User>()
We also changed var into val because users will contain an immutable reference to the list of users. Note that the reference is immutable, but the list itself is mutable (you can add or remove elements).
With these changes, our users
property is now non-null, and we can remove all the unnecessary !!
operator occurrences.
val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
...
}
Also, since the users variable is already initialized, we have to remove the initialization from the init
block:
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users.add(user1)
users.add(user2)
users.add(user3)
}
Since both lastName
and firstName
can be null
, we need to handle nullability when we build the list of formatted user names. Since we want to display "Unknown"
if either name is missing, we can make the name non-null by removing ?
from the type declaration.
val name: String
If the lastName
is null, name
is either the firstName
or "Unknown"
:
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
} else {
name = lastName
}
} else if (firstName != null) {
name = firstName
} else {
name = "Unknown"
}
This can be written more idiomatically by using the elvis operator ?:
. The elvis operator will return the expression on its left hand side if it's not null, or the expression on its right hand side, if the left hand side is null.
So in the following code user.firstName
is returned if it is not null. If user.firstName
is null, the expression returns the value on the right hand , "Unknown"
:
if (lastName != null) {
...
} else {
name = firstName ?: "Unknown"
}
7. String templates and if expression
Kotlin makes working with String
s easy with String templates. String templates allow you to reference variables inside string declarations.
The automatic converter updated the concatenation of the first and last name to reference the variable name directly in the string by using the $
symbol and put the expression between { }
.
// Java
name = user.getFirstName() + " " + user.getLastName();
// Kotlin
name = "${user.firstName} ${user.lastName}"
In code, replace the String concatenation with:
name = "$firstName $lastName"
In Kotlin if
, when
, for
, and while
are expressions—they return a value. Your IDE is even showing a warning that the assignment should be lifted out of the if
:
Let's follow the IDE's suggestion and lift the assignment out for both if
statements. The last line of the if statement will be assigned. Like this, it's clearer that this block's only purpose is to initialise the name value:
name = if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName
}
} else {
firstName ?: "Unknown"
}
Next, we'll get a warning that the name
declaration can be joined with the assignment. Let's apply this as well. Because the type of the name variable can be deduced, we can remove the explicit type declaration. Now our formattedUserNames
looks like this:
val formattedUserNames: List<String?>
get() {
val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
val name = if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName
}
} else {
firstName ?: "Unknown"
}
userNames.add(name)
}
return userNames
}
8. Operations on collections
Let's take a closer look at the formattedUserNames
getter and see how we can make it more idiomatic. Right now the code does the following:
- Creates a new list of strings
- Iterates through the list of users
- Constructs the formatted name for each user, based on the user's first and last name
- Returns the newly created list
val formattedUserNames: List<String>
get() {
val userNames = ArrayList<String>(users.size)
for ((firstName, lastName) in users) {
val name = if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName
}
} else {
firstName ?: "Unknown"
}
userNames.add(name)
}
return userNames
}
Kotlin provides an extensive list of collection transformations that make development faster and safer by expanding the capabilities of the Java Collections API. One of them is the map
function. This function returns a new list containing the results of applying the given transform function to each element in the original list. So, instead of creating a new list and iterating through the list of users manually, we can use the map
function and move the logic we had in the for
loop inside the map
body. By default, the name of the current list item used in map
is it
, but for readability you can replace it
with your own variable name. In our case, let's name it user
:
val formattedUserNames: List<String>
get() {
return users.map { user ->
val name = if (user.lastName != null) {
if (user.firstName != null) {
"${user.firstName} ${user.lastName}"
} else {
user.lastName ?: "Unknown"
}
} else {
user.firstName ?: "Unknown"
}
name
}
}
To simplify this even more, we can remove the name
variable completely:
val formattedUserNames: List<String>
get() {
return users.map { user ->
if (user.lastName != null) {
if (user.firstName != null) {
"${user.firstName} ${user.lastName}"
} else {
user.lastName ?: "Unknown"
}
} else {
user.firstName ?: "Unknown"
}
}
}
9. Properties and backing properties
We saw that the automatic converter replaced the getFormattedUserNames()
function with a property called formattedUserNames
that has a custom getter. Under the hood, Kotlin still generates a getFormattedUserNames()
method that returns a List
.
In Java, we would expose our class properties via getter and setter functions. Kotlin allows us to have a better differentiation between properties of a class, expressed with fields, and functionalities, actions that a class can do, expressed with functions. In our case, the Repository
class is very simple and doesn't do any actions so it only has fields.
The logic that was triggered in the Java getFormattedUserNames()
function is now triggered when calling the getter of the formattedUserNames
Kotlin property.
While we don't explicitly have a field corresponding to the formattedUserNames
property, Kotlin does provide us an automatic backing field named field
which we can access if needed from custom getters and setters.
Sometimes, however, we want some extra functionality that the automatic backing field doesn't provide. Let's go through an example below.
Inside our Repository
class, we have a mutable list of users which is being exposed in the function getUsers()
which was generated from our Java code:
fun getUsers(): List<User>? {
return users
}
The problem here is that by returning users
any consumer of the Repository class can modify our list of users - not a good idea! Let's fix this by using a backing property.
First, let's rename users
to _users
. Now add a public immutable property that returns a list of users. Let's call it users
:
private val _users = mutableListOf<User>()
val users: List<User>
get() = _users
With this change, the private _users
property becomes the backing property for the public users
property. Outside of the Repository
class, the _users
list is not modifiable, as consumers of the class can access the list only through users
.
Full code:
object Repository {
private val _users = mutableListOf<User>()
val users: List<User>
get() = _users
val formattedUserNames: List<String>
get() {
return _users.map { user ->
if (user.lastName != null) {
if (user.firstName != null) {
"${user.firstName} ${user.lastName}"
} else {
user.lastName ?: "Unknown"
}
} else {
user.firstName ?: "Unknown"
}
}
}
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
_users.add(user1)
_users.add(user2)
_users.add(user3)
}
}
10. Top-level and extension functions and properties
Right now the Repository
class knows how to compute the formatted user name for a User
object. But if we want to reuse the same formatting logic in other classes, we need to either copy and paste it or move it to the User
class.
Kotlin provides the ability to declare functions and properties outside of any class, object, or interface. For example, the mutableListOf()
function we used to create a new instance of a List
is defined directly in Collections.kt
from the Standard Library.
In Java, whenever you need some utility functionality, you would most likely create a Util
class and declare that functionality as a static function. In Kotlin you can declare top-level functions, without having a class. However, Kotlin also provides the ability to create extension functions. These are functions that extend a certain type but are declared outside of the type. As such, they have an affinity to that type.
The visibility of extension functions and properties can be restricted by using visibility modifiers. These restrict the usage only to classes that need the extensions, and don't pollute the namespace.
For the User
class, we can either add an extension function that computes the formatted name, or we can hold the formatted name in an extension property. It can be added outside the Repository
class, in the same file:
// extension function
fun User.getFormattedName(): String {
return if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
}
// extension property
val User.userFormattedName: String
get() {
return if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
}
// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName
We can then use the extension functions and properties as if they're part of the User
class.
Because the formatted name is a property of the user and not a functionality of the Repository
class, let's use the extension property. Our Repository
file now looks like this:
val User.formattedName: String
get() {
return if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
}
object Repository {
private val _users = mutableListOf<User>()
val users: List<User>
get() = _users
val formattedUserNames: List<String>
get() {
return _users.map { user -> user.formattedName }
}
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
_users.add(user1)
_users.add(user2)
_users.add(user3)
}
}
The Kotlin Standard Library uses extension functions to extend the functionality of several Java APIs; a lot of the functionalities on Iterable
and Collection
are implemented as extension functions. For example, the map
function we used in a previous step is an extension function on Iterable
.
11. Scope functions: let, apply, with, run, also
In our Repository
class code, we are adding several user objects to the _users
list. These calls can be made more idiomatic with the help of scope functions.
To execute code only in the context of a specific object, without needing to access the object based on its name, Kotlin created 5 scope functions: let
, apply
, with
, run
and also
. Short and powerful, all of these functions have a receiver (this
), may have an argument (it
) and may return a value. You would decide which one to use depending on what you want to achieve.
Here's a handy cheat sheet to help you remember this:
Since we're configuring our _users
object in our Repository
, we can make the code more idiomatic by using the apply
function:
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
_users.apply {
// this == _users
add(user1)
add(user2)
add(user3)
}
}
12. Wrap up
In this codelab, we covered the basics you need to start refactoring your code from Java to Kotlin. This refactoring is independent of your development platform and helps to ensure that the code you write is idiomatic.
Idiomatic Kotlin makes writing code short and sweet. With all the features Kotlin provides, there are so many ways to make your code safer, more concise, and more readable. For example, we can even optimize our Repository
class by instantiating the _users
list with users directly in the declaration, getting rid of the init
block:
private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
We covered a large array of topics, from handling nullability, singletons, Strings, and collections to topics like extension functions, top-level functions, properties, and scope functions. We went from two Java classes to two Kotlin ones that now look like this:
User.kt
class User(var firstName: String?, var lastName: String?)
Repository.kt
val User.formattedName: String
get() {
return if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
}
object Repository {
private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
val users: List<User>
get() = _users
val formattedUserNames: List<String>
get() = _users.map { user -> user.formattedName }
}
Here's a TL;DR of the Java functionalities and their mapping to Kotlin:
Java | Kotlin |
|
|
|
|
|
|
Class that just holds data |
|
Initialization in the constructor | Initialization in the |
| fields and functions declared in a |
Singleton class |
|
To find out more about Kotlin and how to use it on your platform, check out these resources:
- Kotlin Koans
- Kotlin Tutorials
- Developing Android apps with Kotlin - free course
- Kotlin Bootcamp for Programmers
- Kotlin for Java developers - free course in Audit mode