Java To Kotlin Part 2 - Classes And Methods
Introduction
Part 1 showed how to declare and extend classes, create constructors, and create methods.
Part 2 will build on Part 1. Even though it compiles to the JVM, Kotlin works a little differently to Java. It has a few different types of classes, which are useful for certain situations. It also has some really nifty built-in features.
Expected outcome of Part 2:
This should give a good understanding of the different types of class/object available to a Kotlin developer, and how they can all co-exist in the same file. This section also covers some basic Kotlin method features, like default arguments and overriding classes.
This simple Employee Java class will be used as an example of what`s possible:
1 | public class Employee { |
This is what the equivalent Kotlin class looks like (Excluding the last four methods, as it`s not a data class):1
2
3
4
5class Employee(var number: Int,
var age: Int,
var department: Department,
//default value var. Will explain later
var name: String = "Unknown Name")
Add the Department enum:
1 | enum class Department { DEV, NOT_DEV } |
(Enums in Kotlin differ from Java only by the class keyword)
The Kotlin compiler will add invisible getters and setters to a class. This is explained below.
Data Classes
The Employee class can be refined further as a data class:1
2
3
4
5data class Employee(
val number: Int,
val age: Int,
val department: Department,
val name: String? = null)
Data classes exist for one single purpose: To store data. By design, data classes are final and cannot be extended.
Data classes also come with a few extra built-in methods:1
2
3
4copy()
equals()
hashCode()
toString()
(This will be covered a bit later)
When compared to an identically functional Java class, a Kotlin class is a lot more compact. There`s virtually no boilerplate to type, and it comes with some nifty methods. Imagine all the time saved when writing a whole module! Add to that all the time saved in not having to maintain all that boilerplate code…
A brief note on Getters and Setters
In a class, when a property is created, Kotlin generates invisible getters and setters. They`re considered invisible because we invoke the property instead of the getter/setter. This gets translated to a getter/setter call. When accessing a Kotlin property in Java, the getter/setter is called as per normal. An advantage of this system is that, if a property needs to be mutated during get/set, the property can be made private and a custom getter/setter can be added.
Multiple public classes in a single file
This is a good place to point out that Kotlin supports multiple public classes in a single file (So does Java, just with non-public classes*):
1 | package examples |
*Something many developers don`t know is that Java supports multiple classes in a single file. The only difference between Java and Kotlin is that Java only allows a single public class in a file.
Best practices:
As with all systems, multiple classes in one file can be abused. According to the Kotlin Style Guide, if a file contains multiple classes, the name of the file should describe the contents of a file. This implies that only classes and functions with similar functionality should be placed in the same file together.
copy()
As mentioned earlier, data classes contain a built-in copy() method:1
2
3
4
5
6val dave = Employee(7, 23, Department.NOT_DEV, "Dave")
println(dave) //”Employee(number=7, age=23, department=NOT_DEV, name=Dave)”
val alsoDave = dave.copy()
println(alsoDave) //”Employee(number=7, age=23, department=NOT_DEV, name=Dave)”
println(dave == alsoDave) //true
The copy method does more than just making a carbon copy:1
2
3val john = dave.copy(number = 8, name = "John") //named arguments are covered a bit later
println(john) //”Employee(number=8, age=23, department=NOT_DEV, name=John)”
println(dave == john) //false
This allows us to make copies of immutable classes, while giving us the ability to alter the data.
Constructors with default arguments
Both class constructors and method signatures can take default values as part of their arguments. For instance, in Employee, the name parameter is nullable and the default value is null. We can, therefore, instantiate this class in two different ways:1
2val bongani = Employee(4, 38, Department.DEV, "Bongani")
println(bongani) //”Employee(number=4, age=38, department=DEV, name=Bongani)”
1 | val namelessEmployee = Employee(4, 38, Department.DEV) |
Let`s use another example:1
2
3
4
5
6
7
8
9data class Vehicle(val model: String,
val make: String,
val year: Int,
var registration: String = "Unregistered",
var ownerName: String? = null)
val vehicle = Vehicle("Tesla", "Moonshot", 2020)
println(vehicle)
//"Vehicle(model=Tesla, make=Moonshot, year=2020, registration=Unregistered, ownerName=null)"
Methods work identically (And it should as class constructors are methods):1
2
3
4fun doStuff(id: Int, age: Int = 0, name: String = "not yet named", occupation: String? = null){}
doStuff(0, 14, "Cecilia", "Telemarketer")
doStuff(0, 1)
Default arguments are most useful in situations where a variable should not be null and can take a default value.
Overriding methods with default values
When overriding methods that contain default values, the parent`s default values can be omitted:1
2
3
4
5
6
7
8open class Vehicle(val model: String,
val make: String,
open var year: Int,
var registration: String = "Unregistered",
var ownerName: String? = null)
class Moonshot(override var year: Int) : Vehicle("Tesla", "Moonshot", year)
//no registrationName or ownerName
Constructors and methods with named arguments
This is another of Kotlin’s interesting features. It has a way to call a method with args in a different order. The trick is to name them (And IntelliJ`s auto-complete helps with this):1
Employee(name = "Sally", age = 38, department = Department.NOT_DEV, number = 4)
1 | createEmployee(number = 4, department = Department.NOT_DEV, name = "Sally", age = 38) |
Mixing named args and default values in a method signature can unlock some really useful and powerful options for smart, clean code.
It`s also useful to note that one can also keep the args in order..
Single-line methods
Any single-method can be condensed into an one line method. This looks like an assignment. For instance:1
2
3fun isEmployeeADeveloper(employee: Employee): Boolean {
return Department.DEV == employee.department
}
can be converted to:1
fun isEmployeeADeveloper(employee: Employee) = Department.DEV == employee.department
Companion Objects
Companion objects are a type of singleton that gets added to a class. They act like a container for methods and properties:
1 | class EmployeeManager { |
Accessing a method in a companion object from outside of it`s class works the same way as Java statics, except for the fact that companion objects are singletons.1
val isDev = EmployeeManager.verifyIsDev(employee)
Functions outside of classes
In the beginning, Kotlin can feel a little strange to a Java developer. For instance, it allows multiple classes to be added to a single file. To make things even crazier, one can add functions to a file… outside of a class:
1 | package examples |
These functions can be used in other files. They can be imported on a per-function basis:1
2
3
4
5
6
7
8
9package functions
import playlistWriter.createEmployee
class TestingExternalFunctions{
fun accessEmployeeFunction() {
createEmployee(8, 64, "Sally", true)
}
}
Package-level functions can also be imported on a per-package basis:1
import examples.*
Package-level functions are always static.
This concludes Part 2. The above points should point you in the right direction to mastering Kotlin. As usual, a smart IDE should help you to improve on these points.
Examples for Part 2 can be found here.
The Employee java class example can be found here.
The external function imports example can be found here.