Java To Kotlin Part 4 - Loops, Switches, and Sealed Classes
Introduction
Part 4 covers basic loop designs, and some interesting types, like ranges. Part 4 also dives into switches and demonstrates just how much more powerful they are when compared to Java’s bland offering. Finally, we`ll look into sealed classes, one of Kotlin`s more advanced features.
Expected outcome of Part 4:
once you`ve read through Part 4, you should be able to utilize loops and switches to their full potential. You should also possess a good understanding of sealed classes.
Loops
Some data is needed to demonstrate loops. Let`s use this:1
2
3
4
5
6
7
8
9private fun createEmployees() = listOf(
Employee(0, 31, Department.DEV, "Bongani"),
Employee(1, 66, Department.NOT_DEV, "Busiswe"),
//note the two null names. One uses the default field value, which is null:
Employee(2, 17, Department.DEV, null),
Employee(3, 61, Department.DEV),
Employee(4, 27, Department.NOT_DEV, "Susan"),
Employee(5, 66, Department.NOT_DEV, "Bill")
)
Kotlin`s for each method works the same as Java:1
2
3
4
5
6
7
8
9
10
11fun forEachLoop() {
val employees = createEmployees()
employees.forEach
println("${it.name} is present")
}
//Bongani is present
//Busiswe is present
//null is present
//null is present
//Susan is present
//Bill is present
Ranges
Should you need a more traditional loop (for instance, if you need to break), you can do this:1
2
3
4
5
6
7
8
9
10
11fun traditionalJavaLoop() {
val employees = createEmployees()
for(counter in 0 .. employees.size -1)
println("${employees.get(counter).name} is present")
}
//Bongani is present
//Busiswe is present
//null is present
//null is present
//Susan is present
//Bill is present
This (..) is known as a range in Kotlin. It accepts any number, including Int, Long, Double, Float, Boolean, Char (not exactly a number, but it works), etc. You can even create a custom range if you like. Should you need to count skip numbers (counter+2), the step command can be added:1
2
3
4
5
6
7
8
9fun traditionalJavaLoopStep2() {
val employees = createEmployees()
//the range can be replaced with "until". "Until: is part of a future post.
for(counter in 0 .. employees.size -1 step 2)
println("${employees.get(counter).name} is present")
}
//Bongani (0) is present
//null (2) is present
//Susan (4) is present
Ranges and Progressions
Using the ranges example, we can simplify the code with the until function:1
2
3
4
5
6
7
8
9
10
11fun untilLoop() {
val employees = createEmployees()
for(counter in 0 until employees.size)
println("${employees.get(counter).name} is present")
}
//Bongani is present
//Busiswe is present
//null is present
//null is present
//Susan is present
//Bill is present
downTo is the reverse of until:1
2
3
4
5
6
7
8
9
10
11fun downToLoop() {
val employees = createEmployees()
for(counter in employees.size -1 downTo 0)
println("${employees.get(counter).name} is present")
}
//Bill is present
//Susan is present
//null is present
//null is present
//Busiswe is present
//Bongani is present
“when”
Kotlin’s equivalent of the case statement is really fun to work with. Thanks to the way it was designed, it can make use of a surprisingly large range of values Here`s the basic syntax:1
2
3
4
5
6
7
8
9
10
11fun basicWhen() {
val employees = createEmployees()
employees.forEach {
when (it.name) {
"Bongani" -> println("${it.name} is in charge")
null -> println("Employee ${it.number} is a number, not a free man")
//replaces "default" case
else -> println("${it.name} is present")
}
}
}
The output is pretty much what`s expected of a case statement:1
2
3
4
5
6Bongani is in charge
Busiswe is present
Employee 2 is a number, not a free man
Employee 3 is a number, not a free man
Susan is present
Bill is present
This can be further refined by extracting println() to outside of the when:1
2
3
4
5
6
7
8
9
10
11
12
13fun printWhen() {
val employees = createEmployees()
employees.forEach {
println(
when (it.name) {
"Bongani" -> "${it.name} is in charge"
null -> "Employee ${it.number} is a number, not a free man"
//replaces "default" case
else -> "${it.name} is present"
}
)
}
}
Using when as an expression like this is strongly recommended over a more traditional-appearing switch-like when. This is because compile-time checking will enforce that all cases are covered by the statement.
Let’s make Busiswe and Susan say something different. We`ll do this by combining two cases using a comma:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17fun multiValueWhen() {
val employees = createEmployees()
employees.forEach {
println(when (it.name) {
"Bongani" -> "${it.name} is in charge"
null -> "Employee ${it.number} is a number, not a free man"
"Busiswe", "Susan" -> "${it.name} here"
else -> "${it.name} is present"
})
}
}
//Bongani is in charge
//Busiswe here
//Employee 2 is a number, not a free man
//Employee 3 is a number, not a free man
//Susan here
//Bill is present
Using ranges in “when”
Ranges work the same way in loops as they do in when:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18fun ranges() {
val employees = createEmployees()
employees.forEach {
println(when (it.age) {
in 0..17 -> "${it.name} is Underage!"
in 18..63 -> "${it.name} is of Working age"
64 -> "${it.name} is Ready to retire"
in 65..1000 -> "${it.name} is Past retirement age"
else -> "${it.name} is Employee is either unborn or improbably old"
})
}
}
//Bongani is of Working age
//Busiswe is Past retirement age
//null is Underage!
//null is of Working age
//Susan is of Working age
//Bill is Past retirement age
The when can also return data if needed:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15fun printStatus() {
val employees = createEmployees()
employees.forEach {
println(getStatus(it))
}
}
private fun getStatus(it: Employee): String =
when (it.age) {
in 0..17 -> "${it.name} is Underage!"
in 18..63 -> "${it.name} is of Working age"
64 -> "${it.name} is Ready to retire"
in 65..1000 -> "${it.name} is Past retirement age"
else -> "${it.name} is Employee is either unborn or improbably old"
}
Smart Casting With “when”
Smart casting can be really powerful when used in a case statement:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21private fun smartCastingWithWhen(someObject: Any?) {
println(when (someObject) {
is String -> "SomeObject is a String of value \"$someObject\""
is Int -> "SomeObject is an Int of $someObject"
is Employee -> "SomeOject is an Employee named ${someObject.name}"
else -> "we\`re not sure what the object is"
})
}
fun testSmartCasting() {
smartCastingWithWhen(5)
smartCastingWithWhen("Some String")
smartCastingWithWhen(5L)
smartCastingWithWhen(Employee(7, 55, Department.DEV, "Priscilla"))
smartCastingWithWhen(null)
}
//SomeObject is an Int of 5
//SomeObject is a String of value "Some String"
//we\`re not sure what the object is
//SomeOject is an Employee named Priscilla
//we\`re not sure what the object is
Sealed Classes
Sealed classes are, in essence, enums on steroids. The sealed class type extends the enum class type, making them a sort of hybrid between an enum and a normal class. Instead of declaring enum values, you declare a class, which extends the sealed class:1
2
3
4sealed class WorkType(val typeName: String) {
class DevWork(typeName: String, val nearlyDone: Boolean) : WorkType(typeName)
class Other(typeName: String) : WorkType(typeName)
}
As can be seen in the above example, the child classes of WorkType do not need the same method signature as the parent (think normal Kotlin inheritance), so long as they call Kotlin`s equivalent of super. In addition, being an enum/class hybrid, sealed classes also keep state like a normal class. This means an instance of a sealed class will maintain it`s data integrity.
We`ll use the following data as an example:1
2
3
4
5
6
7
8
9
10
11
12data class EmployeeDoingWork(val number: Int, val age: Int, val department: Department, val name: String, val workType: WorkType)
private fun createEmployeesDoingWork() = listOf(
EmployeeDoingWork(0, 31, Department.DEV, "Bongani",
WorkType.DevWork("coding", false)),
EmployeeDoingWork(1, 66, Department.NOT_DEV, "Busiswe",
Accounting("financial reports", 7)),
EmployeeDoingWork(4, 27, Department.NOT_DEV, "Susan",
HumanResources("firing", "Bill", false)),
EmployeeDoingWork(5, 66, Department.NOT_DEV, "Bill",
WorkType.Other("cleaning"))
)
Sealed classes work particularly well with when. Bare in mind that the case statement needs to be complete (or contain an else “default” case), or it could cause problems. A good way to enforce this is to use when as an expression instead of a statement, like in the above examples. Let`s do some work:1
2
3
4
5
6
7
8
9fun doSomeWork() {
val employees = createEmployeesDoingWork()
employees.forEach {
println(when (it.workType) {
is WorkType.DevWork -> "${it.name} is ${it.workType.typeName}. Nearly done? ${it.workType.nearlyDone}"
else -> "${it.name} is ${it.workType.typeName}"
})
}
}
As of Kotlin 1.1, child classes of a sealed class do not need to be nested inside the sealed class. They can lie in the same file:1
2class HumanResources(typeName: String, val victimName: String, val fireTime: Boolean) : WorkType(typeName)
class Accounting(typeName: String, val reportsToCreate: Int) : WorkType(typeName)
Be aware that this is discouraged. It prevents the sealed class from resembling an enum and it makes it easy to forget certain cases can be when coding.
After expanding the when, everything works just the same:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15fun doSomeWork() {
val employees = createEmployeesDoingWork()
employees.forEach {
println(when (it.workType) {
is WorkType.DevWork -> "${it.name} is ${it.workType.typeName}. Nearly done? ${it.workType.nearlyDone}"
is WorkType.Other -> "${it.name} is ${it.workType.typeName}"
is Accounting -> "${it.name} is responsible for ${it.workType.reportsToCreate} ${it.workType.typeName}"
is HumanResources -> "is ${it.name} ${it.workType.typeName} poor ${it.workType.victimName} yet? ${it.workType.fireTime}"
})
}
}
//Bongani is coding. Nearly done? false
//Busiswe is responsible for 7 financial reports
//is Susan firing poor Bill yet? false
//Bill is cleaning
You`ve reached the end of part 4. Knowledge of loops and enums should have primed you for the hard-to-grasp, but really useful sealed class knowledge.
Examples for Part 4 can be found here.
Further Reading
A good example of when being used in a state machine can be found here.