JavaRush/Courses/Kotlin SELF/operator in Kotlin: plus, compareTo, plusAssign

operator in Kotlin: plus, compareTo, plusAssign

Open

1. Operator syntax

When you first see that in Kotlin you can write a + b for your own classes, it can feel like the language let you “rewrite math”. In reality it is much more down-to-earth (and that’s a good thing): an operator is just a short form for calling a function with a fixed name. The compiler performs the translation, and then everything works like ordinary methods/functions.

The most important convention today: the expression a + b is translated by the compiler into a call to a.plus(b). That is, an operator is syntactic sugar, not a separate “operator universe”.

Let’s summarize what we need today in a small table (don’t try to memorize it — it’s more important to understand the idea):

What you write What the compiler translates it into
a + b
a.plus(b)
a += b
either a.plusAssign(b), or a = a + b (depending on whether plusAssign exists and whether assignment is possible)
a < b
a.compareTo(b) < 0
a > b
a.compareTo(b) > 0

And that leads to a practical conclusion: for your class to “understand” the + operator, you write a normal function plus, but you mark it with the operator modifier so the compiler allows the operator syntax.

2. plus and +=: surprise-free design

With plus there is one subtle but very real problem: the + symbol looks harmless, so the reader expects the operation to be fast, predictable, and without side effects. If inside plus you print to the console, change global state, or delete files (you never know), it won’t be the reader’s fault — it will be yours. + must behave “like addition”; otherwise the code turns into a riddle.

Let’s keep developing our educational console app (let’s call it ExpenseTracker), where we store expenses and build reports. The most common pain point in such programs is money. Storing “money” in Double is like keeping dumplings in your pocket: technically possible, but somehow everything later gets sticky and rounded the wrong way. So let’s introduce a Money type that stores the amount in cents.

Mini model Money and the + operator

@JvmInline
value class Money(val cents: Long)

operator fun Money.plus(other: Money): Money =
    Money(this.cents + other.cents)

fun main() {
    val a = Money(150)
    val b = Money(200)

    println((a + b).cents) // 350
}

Two ideas matter here. First: Money is a “value”, and + creates a new value, rather than mutating the old one. Second: + for money reads naturally, without decoding. If someone needs to open the implementation to understand what + means, the operator choice is a poor one.

There’s a common trap: “what if plus adds into the current object”. That breaks expectations. In most languages and libraries, a + b is understood as producing a result, not as modifying a. It’s better to make mutation explicit (a method like add, append, increase) or to use plusAssign (+=), where mutation is at least expected by the form.

If you still mutate through plus, your code will start behaving like a cat: sometimes affectionate, sometimes bites, and it’s unclear why.

How += works and how plusAssign helps

The += operator looks like “addition too”, but it has a different meaning: it is augmented assignment. In Kotlin the compiler tries to call plusAssign, and if it can’t, it rewrites it into “a = a + b”. This is not our guess; it’s a translation rule built into operator conventions.

To avoid confusion, it helps to keep this mini flowchart in mind:

flowchart TD
    A["Code: a += b"] --> B{"Is there a.plusAssign(b)?"}
    B -- "Yes" --> C["Calling a.plusAssign(b) (should return Unit)"]
    B -- "No" --> D["Trying a = a + b (that is, a = a.plus(b))"]

This matters not only “in theory”, but because the behavior of += can depend on whether the left side is val or var, and whether the object is mutable.

Suppose we don’t have plusAssign, but we do have plus. Then += can simply become a convenient way to write “reassignment”:

@JvmInline
value class Money(val cents: Long)

operator fun Money.plus(other: Money): Money =
    Money(this.cents + other.cents)

fun main() {
    var total = Money(0)
    total += Money(99)

    println(total.cents) // 99
}

Why does this work? Because total is var, so it can be assigned a new value, and total += Money(99) turns into total = total + Money(99).

If plusAssign doesn’t exist and reassignment isn’t possible, the compiler can’t “figure out” what you meant:

@JvmInline
value class Money(val cents: Long)

operator fun Money.plus(other: Money): Money =
    Money(this.cents + other.cents)

fun main() {
    val total = Money(0)
    // total += Money(99) // compilation error: val cannot be reassigned
}

This is where the “fun part” begins: a beginner says, “wait, but I have val list = mutableListOf(...), and list += x works!” Yes, it works — because MutableCollection has mutating operations, and for mutable collections += can mean mutating the contents rather than reassigning the variable. The standard library explicitly describes that plusAssign (+=) exists for collections, and for mutable collections it adds elements “in place”.

Key idea: val forbids changing the reference (the variable), but it does not forbid changing the contents of a mutable object. Therefore:

fun main() {
    val items = mutableListOf("coffee")
    items += "pizza"

    println(items) // [coffee, pizza]
}

This isn’t a violation of the laws of physics; it’s simply different levels of mutability: the variable is immutable, the object is mutable.

Your own plusAssign: when it makes sense

When you declare operator fun plusAssign(...), you’re effectively promising the reader: “yes, this will mutate the current object”. This is useful for accumulator classes, counters, buffers, reports — anywhere “accumulating” is natural.

The compiler imposes several strict expectations on plusAssign. One of them is that the return type must be Unit, otherwise it will be an error. And that’s logical too: a += b is an assignment, not an expression; you don’t expect a result from it.

Imagine that in ExpenseTracker we have a “category budget” — an object that lives a long time and changes over time.

@JvmInline
value class Money(val cents: Long)

class Budget(var limit: Money) {
    operator fun plusAssign(delta: Money) {
        limit = limit + delta
    }
}

operator fun Money.plus(other: Money): Money =
    Money(this.cents + other.cents)

fun main() {
    val b = Budget(Money(10_00))
    b += Money(2_50)

    println(b.limit.cents) // 1250
}

Here the mutation is expected: the budget changes. We made plusAssign “boring”: it just updated a field. No prints, no surprises.

Kotlin has a rule about ambiguity: if you have both plusAssign and plus, and the left-hand side can be assigned to, the compiler may complain about ambiguity in some cases. In practice, this means: don’t try to give two different meanings to + and += within the same type if it can confuse both the compiler and people.

A good general rule: + is “produce a new result”, += is “mutate the current accumulator”. If that rule doesn’t hold, consider whether a normal method would be better.

3. Comparisons via compareTo

Comparisons are one of the most important reasons to overload operators in business code. When you write if (total > limit), the code reads like human language. And Kotlin supports this: the <, >, <=, >= operators are translated into compareTo calls and comparison with zero.

But it’s easy to make a mistake here that will show up rarely and be maximally unpleasant: implement compareTo via subtraction.

A correct compareTo for Money

@JvmInline
value class Money(val cents: Long) : Comparable<Money> {
    override operator fun compareTo(other: Money): Int =
        this.cents.compareTo(other.cents)
}

fun main() {
    println(Money(10) < Money(20))   // true
    println(Money(10) > Money(20))   // false
}

We delegated the comparison to Long.compareTo, and that is safe. The meaning of compareTo is simple: positive means “greater”, negative means “less”, zero means “equal”. Kotlin interprets the result exactly this way when it rewrites a < b into a.compareTo(b) < 0.

Why you shouldn’t do “return (this.cents - other.cents).toInt()”

Because subtraction can overflow. For small numbers everything will be fine, but for large numbers you’ll get the wrong sign. The bug will be rare, and that’s exactly why production bug reports love it: they sound like “sometimes the budget is negative, but only on Tuesdays”.

The safe approach is .compareTo(...).

Comparing by multiple fields and common sense

In ExpenseTracker we might have expenses with a date, category, and amount. Let’s imagine a simple expense model (we won’t dive deep into dates; we’ll use a string for the example because the topic of the lecture is operators):

data class Expense(
    val title: String,
    val category: String,
    val amount: Money
)

If we want a “natural order” of expenses by amount, we can create a separate wrapper type or implement Comparable on another entity. But the main thing is that the comparison should be obvious. If you make expenses compare “by title length, then by amount, then by category”, the user of your code will suffer.

In Kotlin, the standard library explicitly explains that the natural order is defined by Comparable and compareTo — that is, it’s a contract used by sorting. Therefore, a “natural order” should actually be natural.

4. Operators in application logic

Now we’ll add operators to pieces of logic that really occur in a console expense-tracking app: accumulating the sum, comparing to the limit, and printing a warning. The important point is that operators here are not for looks but for clarity: total + amount and total > limit read better than total.add(amount) or total.isGreaterThan(limit) (though variants like those are sometimes needed).

Calculating the total amount of expenses

@JvmInline
value class Money(val cents: Long) : Comparable<Money> {
    override operator fun compareTo(other: Money): Int =
        cents.compareTo(other.cents)
}

operator fun Money.plus(other: Money): Money =
    Money(this.cents + other.cents)

fun main() {
    val expenses = listOf(Money(199), Money(350), Money(451))
    var total = Money(0)

    for (x in expenses) total += x
    println(total.cents) // 1000
}

Note: we used total += x, but Money does not have plusAssign. And that’s fine: here += works as “reassignment via +”, because total is var.

Checking the limit and warning

@JvmInline
value class Money(val cents: Long) : Comparable<Money> {
    override operator fun compareTo(other: Money): Int =
        cents.compareTo(other.cents)
}

operator fun Money.plus(other: Money): Money =
    Money(this.cents + other.cents)

fun main() {
    val limit = Money(500)
    val spent = Money(650)

    if (spent > limit) {
        println("Careful: you exceeded the limit!") // Careful: you exceeded the limit!
    }
}

The > operator here turns into spent.compareTo(limit) > 0, but you can read the code without knowing that detail — and that’s the main advantage of operators.

5. Operator readability rules

Operators are a very powerful tool, and that’s exactly why they’re easy to ruin. In good code, an operator is a short, honest notation for an obvious action. In bad code, it’s a puzzle that requires “just in case” comments. Comments are useful, of course, but it’s better when you don’t need them to decode +.

A good sanity check is this: imagine your code is read by someone who knows Kotlin but doesn’t know your project. If they see a + b, they expect “added”. If they see a += b, they expect “added to an accumulator / mutated state”. If they see a < b, they expect “compared by natural order”. If your code does something else, it’s better to choose a named function.

One more rule from practice: an operator should be predictably fast. Not necessarily O(1), but definitely not “go to the network and download the exchange rate” (yes, I’ve seen that — and yes, it was painful).

And finally, the most important anti-hero move: don’t use an operator where the meaning is not broadly known. Money + Money is fine. User + User is already questionable. Logger + "hello" is suspicious. When in doubt, a named method almost always wins in maintainability.

6. Common mistakes when working with operator, plus, compareTo, plusAssign

Mistake #1: making plus have side effects (printing, modifying someone else’s collections, “just in case” logging).
The + symbol looks like a pure operation that produces a result, and the reader expects the expression can be safely moved around, reordered, or extracted into a variable. If plus suddenly prints to the console or changes state, you’ll get code that “sometimes makes noise” and “sometimes breaks logic” simply because of refactoring.

Mistake #2: implementing compareTo via subtraction.
On small numbers, “this.x - other.x” almost always works, so the bug won’t show up for a long time. Then overflow happens, the sign becomes wrong, and sorting starts behaving as if elements swapped places in line. The safe option is to call .compareTo(...) on the compared field.

Mistake #3: mixing up the meanings of + and += and trying to make them “completely different operations”.
+ usually means creating a new value, and += means mutating an accumulator or reassigning a variable via +. If a + b does one thing, while a += b does something else (unrelated in meaning), the code becomes unpredictable. It’s especially unpleasant if you have both plus and plusAssign and the compiler starts complaining about ambiguity — that’s a good signal to stop and simplify the design.

Mistake #4: writing plusAssign with a return value (not Unit).
Sometimes you want to “return a new object” and continue a chain. But Kotlin’s += contract expects plusAssign to return Unit; otherwise it’s a compilation error. If you need to “return a result”, you need plus or a regular function with a descriptive name.

Mistake #5: overloading operators for “beauty” and saving keystrokes.
Operators save characters but can cost hours of reading and maintenance. If after introducing an operator the team starts asking “what does + do for this type?”, the operator choice was wrong. In that case, a regular method (for example, addExpense, increaseLimit, mergeWith) often makes the code 5–10 characters longer, but costs 5–10 fewer nerve cells.

Mistake #6: forgetting that += can mean different things for different types (reassignment or object mutation).
For immutable values, += often turns into a = a + b, and that requires var. For mutable collections, += can change contents even with val. If you don’t keep that in mind, you can accidentally write code that looks the same but behaves differently — and then spend a long time explaining to yourself why.

1
Task
Kotlin SELF,  38 level,  2 lecture
Locked
Player Points
Player Points
1
Task
Kotlin SELF,  38 level,  2 lecture
Locked
Comparing amounts
Comparing amounts
1
Task
Kotlin SELF,  38 level,  2 lecture
Locked
Daily step counter
Daily step counter
1
Task
Kotlin SELF,  38 level,  2 lecture
Locked
Shift temperatures
Shift temperatures
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet