Chapters

Hide chapters

Kotlin Apprentice

Third Edition · Android 11 · Kotlin 1.4 · IntelliJ IDEA 2020.3

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Intermediate Topics

Section 4: 9 chapters
Show chapters Hide chapters

22. Conventions & Operator Overloading
Written by Irina Galata

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Kotlin is known for its conciseness and expressiveness, which allows you to do more while using less code. Support for user-defined operator overloading is one of the features that gives Kotlin, and many other programming languages, this ability. In this chapter, you’ll learn how to efficiently manipulate data by overloading operators.

What is operator overloading?

Operator overloading is primarily syntactic sugar, and it allows you to use various operators, like those used in mathematical calculations (e.g., +, -, *, +=, >=, etc.) with your custom-defined data types. You can create something like this:

val fluffy = Kitten("Fluffy")
val snowflake = Kitten("Snowflake")

takeHome(fluffy + snowflake)

You have two instances of the Kitten class, and you can take them both home using the + operator. Isn’t that nice?

Getting started

For this tutorial, imagine that you run a startup IT company and that you want to build an application to manage your employee and department data.

private val departments: ArrayList<Department> = arrayListOf()
val employees: ArrayList<Employee> = arrayListOf()
class Employee(
  val company: Company,
  val name: String,
  var salary: Int
)
fun main(args: Array<String>) {
  // your company
  val company = Company("MyOwnCompany")

  // departments
  val developmentDepartment = Department("Development")
  val qaDepartment = Department("Quality Assurance")
  val hrDepartment = Department("Human Resources")

  // employees
  var Julia = Employee(company, "Julia", 100_000)
  var John = Employee(company, "John", 86_000)
  var Peter = Employee(company, "Peter", 100_000)

  var Sandra = Employee(company, "Sandra", 75_000)
  var Thomas = Employee(company, "Thomas", 73_000)
  var Alice = Employee(company, "Alice", 70_000)

  var Bernadette = Employee(company, "Bernadette", 66_000)
  var Mark = Employee(company, "Mark", 66_000)
}

Using conventions

The ability to use overloaded operators in Kotlin is an example of what’s called a convention. In Kotlin, a convention is an agreement in which you declare and use a function in a specific way, and the prototypical example is being able to use the function with an operator.

Unary operator overloading

You’re likely familiar with unary operators in programming languages — +a, --a or a++, for example. If you want to use an increment operator like ++ on your custom data type, you need to declare the inc() function as a member of your class, either inside the class or as an extension function. Here, you’ll create a function to give your employees a raise. Add the following function to your Employee class:

operator fun inc(): Employee {
  salary += 5000
  println("$name got a raise to $$salary")
  return this
}
++Julia // now Julia's salary is 105_000
Julia = Julia.inc();
operator fun dec(): Employee {
  salary -= 5000
  println("$name's salary decreased to $$salary")
  return this
}
--Peter // now Peter's salary is 95_000

Binary operator overloading

Similarly, you can use binary operators to combine your custom data types with other values in some kind of meaningful way. An example for our Employee class is for employee raises and pay cuts of a specified amount.

operator fun plusAssign(increaseSalary: Int) {
  salary += increaseSalary
  println("$name got a raise to $$salary")
}

operator fun minusAssign(decreaseSalary: Int) {
  salary -= decreaseSalary
  println("$name's salary decreased to $$salary")
}
Mark += 2500
Alice -= 2000
Mark.plusAssign(2500);
Alice.minusAssign(2000);
operator fun plusAssign(department: Department) {
  departments.add(department)
}

operator fun minusAssign(department: Department) {
  departments.remove(department)
}
operator fun plusAssign(employee: Employee) {
  employees.add(employee)
  println("${employee.name} hired to $name department")
}

operator fun minusAssign(employee: Employee) {
  if (employees.contains(employee)) {
    employees.remove(employee)
    println("${employee.name} fired from $name department")
  }
}
company += developmentDepartment
company += qaDepartment
company += hrDepartment

developmentDepartment += Julia
developmentDepartment += John
developmentDepartment += Peter

qaDepartment += Sandra
qaDepartment += Thomas
qaDepartment += Alice

hrDepartment += Bernadette
hrDepartment += Mark

qaDepartment -= Thomas
company.plusAssign(developmentDepartment);
company.plusAssign(qaDepartment);
company.plusAssign(hrDepartment);

developmentDepartment.plusAssign(Julia);
developmentDepartment.plusAssign(John);
developmentDepartment.plusAssign(Peter);

qaDepartment.plusAssign(Sandra);
qaDepartment.plusAssign(Thomas);
qaDepartment.plusAssign(Alice);

hrDepartment.plusAssign(Bernadette);
hrDepartment.plusAssign(Mark);

qaDepartment.minusAssign(Thomas);

Handling collections

Operator overloading is also quite helpful for when working with collections. For example, if you want to be able to access an employee by its index within a department, declare the following get() operator function in the Department class:

operator fun get(index: Int): Employee? {
  return if (index < employees.size) {
    employees[index]
  } else {
    null
  }
}
val firstEmployee = qaDepartment[0]
qaDepartment[0]?.plusAssign(1000)
operator fun set(index: Int, employee: Employee) {
  if (index < employees.size) {
    employees[index] = employee
  }
}
qaDepartment[1] = Thomas
operator fun contains(employee: Employee) =
  employees.contains(employee)
if (Thomas !in qaDepartment) {
  println("${Thomas.name} no longer works here")
}

Adding ranges

You can also get a list of employees in a given range using the .. operator. To implement such functionality, first define how to sort the employee list so that you always get the same result from this operator.

data class Employee(
  val company: Company,
  val name: String,
  var salary: Int
) : Comparable<Employee>
override operator fun compareTo(other: Employee): Int {
  return when (other) {
    this -> 0
    else -> name.compareTo(other.name)
  }
}
class Department(
  val name: String = "Department"
) : Iterable<Employee>
override fun iterator() = employees.iterator()
developmentDepartment.forEach {
 // do something
}
val allEmployees: List<Employee>
  get() = arrayListOf<Employee>().apply {
    departments.forEach { addAll(it.employees) }
    sort()
  }
operator fun rangeTo(other: Employee): List<Employee> {
  val currentIndex = company.allEmployees.indexOf(this)
  val otherIndex = company.allEmployees.indexOf(other)

  // start index cannot be larger or equal to the end index
  if (currentIndex >= otherIndex) {
    return emptyList()
  }

  // get all elements in a list from currentIndex to otherIndex
  return company.allEmployees.slice(currentIndex..otherIndex)
}
print((Alice..Mark).joinToString { it.name }) // prints "Alice, Bernadette, John, Julia, Mark"

Operator overloading and Java

Unlike Kotlin, Java doesn’t support user-defined operator overloading. However, the + operator is actually overloaded in standard Java; you not only use it to sum two numbers but also to concatenate strings:

String a = "a";
String b = "b";
System.out.print(a + b); // prints "ab"

Delegated properties as conventions

In Chapter 13: “Properties,” you were introduced to various types of delegated propeties. You can delegate the initialization of a property to another object by using conventions for the getValue() and setValue() functions in a delegate class:

class NameDelegate {
  operator fun getValue(
    thisRef: Any?,
    property: KProperty<*>
  ): String {
    // return existing value
  }
  operator fun setValue(
    thisRef: Any?,
    property: KProperty<*>,
    value: String
  ) {
    // set received value
  }
}
var name: String by NameDelegate()

Challenges

  1. developmentDepartment.hire(Julia + John + Peter)
    qaDepartment.hire(Sandra + Thomas + Alice)
    hrDepartment.hire(Bernadette + Mark)
    

Key points

  • To use overloaded operators, it’s necessary to follow the specific conventions for the operator.
  • Conventions manage multiple features in Kotlin, such as operator overloading, infix functions and delegated properties.
  • Operators should always behave predictably; don’t overload operators in a way that makes their behavior unclear for other developers who might use or read your code.

Where to go from here?

In this chapter, you learned how to add custom behaviors to different operators. Now, you’re ready to use them in a real project. Try to replace the routine and repetitive code in your own projects with overloaded operators to make your code more elegant and concise. But don’t forget about predictability and clarity!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now