Java To Kotlin Part 5 - The Five Siblings
The five siblings are apply, also, let, run, and with. They act in much the same manner but have a few, sometimes subtle, differences.
This post is going to rely heavily on an understanding of receivers. If you’re unfamiliar with receivers, or just want a refresher, please refer to the following:
- This is a general article on receivers in object oriented programming.
- This contains a particularly insightful StackOverflow answer on receivers in Kotlin.
with, apply, and run act on the receiver inside a block of code by turning the receiver into this. This means that they are very handy for running a set of commands on a single object. let’s use this EmployeeBuilder as an example:
1 | class EmployeeBuilder { |
if we wanted to use EmployeeBuilder to create an Employee, we could do the following:1
2
3
4
5
6
7
8
9
10fun buildEmployee(): Employee {
val employeeBuilder = EmployeeBuilder()
employeeBuilder.number = 13
employeeBuilder.age = 46
employeeBuilder.department = Department.NOT_DEV
employeeBuilder.name = "Jaim"
return employeeBuilder.buildEmployee()
}
1 | fun testBuildEmployee() { |
The setting of parameters in the EmployeeBuilder could be done with less boilerplate by making the EmployeeBuilder the receiver. The following will demonstrate some of the different ways we could do this:
With
The with method is pretty simple. It allows us to use employeeBuilder as a receiver:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16fun buildEmployee(): Employee {
val employeeBuilder = EmployeeBuilder()
with(employeeBuilder) {
number = 319
age = 55
name = "Zanile"
department = Department.DEV
}
return employeeBuilder.buildEmployee()
}
fun testBuildEmployee() {
val employee = buildEmployee()
println(employee.toString())
//Employee(number=319, age=55, department=DEV, name=Zanile)
}
The with statement is a transformer function. This means that it returns whatever the lambda returns. This means we can call buildEmployee() inside the with block and the following result will be returned:1
2
3
4
5
6
7
8
9
10fun buildEmployee(): Employee {
val employeeBuilder = EmployeeBuilder()
return with(employeeBuilder) {
number = 319
age = 55
name = "Zanile"
department = Department.DEV
buildEmployee()
}
}
This is great because we can just in-line the whole function:1
2
3
4
5
6
7
8fun buildEmployee(): Employee =
with(EmployeeBuilder()) {
number = 319
age = 55
name = "Zanile"
department = Department.DEV
buildEmployee()
}
Apply
apply works in almost the same way as with. There are two noticeable differences:
The first difference is that apply is called on the object you want as the receiver, as opposed to with taking the receiver as a parameter:1
2
3
4
5
6
7
8
9fun buildEmployee(): Employee {
val employeeBuilder = EmployeeBuilder().apply {
number = 18
age = 32
name = "Martha"
department = Department.DEV
}
return employeeBuilder.buildEmployee()
}
1 | fun testBuildEmployee() { |
The second difference is that, instead of returning the result like in with, apply returns the object that it was was applied to. This also means that you can’t return Employee from inside the lambda, as it expects an EmployeeBuilder. This is handy, as we can just append the buildEmployee() method onto the end of the apply block:1
2
3
4
5
6
7fun buildEmployee() = EmployeeBuilder()
.apply {
this.number = 18
age = 32
name = "Martha"
department = Department.NOT_DEV
}.buildEmployee()
apply is useful for configuring an object outside of it’s constructor. It’s also useful for general method chaining, especially at any place where you can ignore the return of a method call. For example, apply could be used for setting the attributes of a Calendar class, as it has already been declared and we don’t need to act on anything that results from the setting of the attributes.
When viewing the decompiled Kotlin code, both the standard method, apply, and with generate the same code:1
2
3
4
5
6
7
8public final Employee buildEmployee() {
EmployeeBuilder employeeBuilder = new EmployeeBuilder();
employeeBuilder.setNumber(319);
employeeBuilder.setAge(55);
employeeBuilder.setName("Zanile");
employeeBuilder.setDepartment(Department.DEV);
return employeeBuilder.buildEmployee();
}
This is notable because it means these are purely convenience functions. They generate no additional overhead during runtime, so don’t be shy about using them.
Run
run can be thought of as a middle ground between with and apply. We call run in the same way as apply, by chaining it to the object we want to be the receiver. Like with, run can return an object inside of the lambda:1
2
3
4
5
6
7
8fun buildEmployee() = EmployeeBuilder()
.run {
number = 217
age = 19
name = "John"
department = Department.NOT_DEV
buildEmployee()
}
1 | fun testBuildEmployee() { |
Let
let (and also) binds to the parameter passed into it instead of the receiver. This means that instead of just calling a function belonging to the receiver, we need to call it.[function]():
1 | fun buildEmployee() = EmployeeBuilder() |
1 | fun testBuildEmployee() { |
As can be seen in this sample, let (like with and run) returns the contents of the lambda.
Also
Our final sibling is also. As mentioned earlier, also acts on the parameter instead of the receiver. The difference between let and also is that, like apply, it returns the same object that the lambda was called on:
fun buildEmployee() = EmployeeBuilder().also {
it.number = 2685
it.age = 18
it.name = "Wesley"
it.department = Department.NOT_DEV
}.buildEmployee()
fun testBuildEmployee() {
val employee = builEmployee()
println(employee.toString())
//Employee(number=2685, age=18, department=NOT_DEV, name=Wesley)
}
Examples for Part 5 can be found here.
Further reading:
Here is an in-depth article on these methods.
Here is a handy spreadsheet that summarizes the functions that were covered in this post.