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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class Employee {
private int number;
private int age;
private Department department;
private String name;

public Employee(int number, int age, Department department, String name) {
this.number = number;
this.age = age;
this.department = department;
this.name = name;
}

public Employee(int number, int age, Department department) {
this.number = number;
this.age = age;
this.department = department;
}

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Department getDepartment() {
return department;
}

public void setDepartment(Department department) {
this.department = department;
}

//other methods that the Kotlin compiler automatically adds to data classes:
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (other instanceof Employee) {
Employee employee = (Employee) other;
return number == employee.getNumber()
&& age == employee.getAge()
&& department.equals(employee.getDepartment())
&& name.equals(employee.getName());
}
return false;
}

@Override
public int hashCode() {
return Objects.hash(number, age, department, name);
}

public String toString() {
return "Employee(number=" + this.number
+ ", age=" + this.age
+ ", department=" + this.department
+ ", name=" + this.name + ")";
}

public final Employee copy(Employee employee) {
return new Employee(employee.number, employee.age, employee.department, employee.name);
}
}

enum Department { DEV, NOT_DEV }

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
5
class 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
5
data 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
4
copy()
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package examples

//imports here

data class Employee(val number: Int, val age: Int, val department: Department, val name: String? = null)

enum class Department { DEV, NOT_DEV }

class EmployeeTest {
@Test
fun createEmployee() {
val employee = Employee(4, 33, Department.DEV, "Alice Jacobs")
assertNotNull(employee)
}
}

*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
6
val 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
3
val 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
2
val bongani = Employee(4, 38, Department.DEV, "Bongani")
println(bongani) //”Employee(number=4, age=38, department=DEV, name=Bongani)”

1
2
val namelessEmployee = Employee(4, 38, Department.DEV)
println(namelessEmployee) //"Employee(number=4, age=38, department=DEV, name=null)"

Let`s use another example:

1
2
3
4
5
6
7
8
9
data 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
4
fun 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
8
open 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
3
fun 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
2
3
4
5
6
7
8
class EmployeeManager {
val employees = ArrayList<Employee>()
fun canGrantSudoRights(employee: Employee) = verifyIsDev(employee)
companion object {
val devOrdinal = Department.DEV.ordinal
fun verifyIsDev(employee: Employee): Boolean = devOrdinal == employee.department.ordinal
}
}

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
2
3
4
5
package examples

fun createEmployee(number: Int, age: Int, employeeName: String?, isDev: Boolean = true): Employee {
return Employee(number, age, if (isDev) Department.DEV else Department.NOT_DEV, employeeName)
}

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
9
package 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.