1. Introduction
Sorting is like tidying up your desk before looking for the right piece of paper. Sure, you can do without it: “I remember where it is,” but a week later the desk turns into an archaeological layer of receipts, stickers, and cables. In programs it’s the same: data comes in some order, but the user often needs a different one — ascending, descending, “largest on top”, “alphabetical order”.
The nicest thing about Kotlin is that sorting looks like “take a list → get a new sorted list”. And that’s the key idea of today’s lecture: we don’t break the original data (i.e., we don’t mutate the source); we build the result separately. It sounds pedantic, but in practice it saves you hours of debugging and a couple of nerve cells.
So we have something to lean on, we’ll keep evolving our small console project (let’s call it ExpensePocket): a mini expense tracker where we store spending amounts and sometimes want to see them “in order” rather than “as they were added”.
2. Natural order and Comparable
When you ask Kotlin to “just sort a list”, Kotlin has to understand how to compare elements. For numbers and strings that’s obvious (well, almost), but for some “complex objects” (which we’ll get to much later) it isn’t.
In the Kotlin standard library, the sorts sorted() and sortedDescending() work for elements that have a natural order. Formally, that means: an element can compare itself with another element of the same type (this is tied to the Comparable interface). Kotlin documentation explicitly says that sorted() and sortedDescending() sort by natural order and return a new list without changing the original collection.
For a beginner, it’s convenient to think of it like this: “if the type looks like something you can order, Kotlin already knows how”.
A small table to help you orient:
| Element type | What “natural order” looks like | Example result |
|---|---|---|
|
by numeric value | |
|
by character code | |
|
lexicographically (roughly “dictionary order”) | |
|
usually |
|
There’s a nuance with strings: “dictionary order” in programming is usually not about any specific natural language dictionary, but about comparing characters by their codes, so "10" and "2" are compared as strings, not as numbers. That’s not a bug — it’s just a different meaning of the data.
3. Basic sorts: sorted() and sortedDescending()
sorted() and ascending order
sorted() is a function that takes your collection and returns a new list where elements are arranged in ascending order (by natural order). The most important idea: it doesn’t “shake up” the original list; it creates the result separately. Kotlin documentation states this plainly: sorting operations for read-only collections return the result as a new collection.
Let’s start with a very simple example — numbers:
fun main() {
val xs = listOf(3, 1, 2)
val sortedXs = xs.sorted()
println(xs) // [3, 1, 2]
println(sortedXs) // [1, 2, 3]
}
Two things are easy to notice here. First, xs didn’t change. Second, if you forget to store the result in a variable, it will look like sorting “doesn’t work” — because it has no right to change your original list (and that’s actually good).
Now strings:
fun main() {
val names = listOf("Bob", "Ann", "Charlie")
val sortedNames = names.sorted()
println(sortedNames) // [Ann, Bob, Charlie]
}
Pause: “nothing happened”
Beginners often write this:
fun main() {
val xs = listOf(3, 1, 2)
xs.sorted()
println(xs) // [3, 1, 2]
}
This isn’t a compilation error, and Kotlin doesn’t have to complain. You simply asked, “give me a sorted copy”, Kotlin did (to nowhere), and then you printed the original.
This kind of code is like ordering coffee and leaving the cafe without picking it up. The coffee exists… but it’s no longer yours.
sortedDescending() — descending order
sortedDescending() does almost the same thing as sorted(), except the order is reversed: “larger” first, then “smaller”. And again: a new list is returned; the original isn’t changed. Kotlin documentation places sorted() and sortedDescending() side by side as basic natural-order sorts.
Example with numbers:
fun main() {
val scores = listOf(10, 70, 50, 20)
val sorted = scores.sortedDescending()
println(scores) // [10, 70, 50, 20]
println(sorted) // [70, 50, 20, 10]
}
Example with strings:
fun main() {
val words = listOf("one", "two", "three", "four")
println(words.sortedDescending()) // [two, three, one, four]
}
If the result seems unexpected, that’s normal: strings are sorted lexicographically, and sometimes the order may “not match your intuition about word length”. We’ll learn how to use length as a criterion later; today we keep the focus on natural order.
4. The principle “don’t mutate the source”
The phrase “don’t mutate the source” sounds like advice from a clean-code book that’s easy to skim. But in practice it’s insurance against very unpleasant effects, especially when the same list is used in different parts of the program.
Kotlin clearly separates operations that “return a new result” from operations that “change the collection in place”. Even Kotlin documentation about collection operations highlights an important idea: there are pairs of functions where one sorts in place (sort()), and the other returns a new list (sorted()). Today we intentionally practice the sorted() variant, because it’s easier to understand and more often leads to more predictable code.
Imagine that in our ExpensePocket we have an expenses list “in the order of insertion” — that’s useful because you can see the history: what was added and when. But sometimes we want to see “the same numbers, but ascending” or “descending”. If sorting changed the original, we’d lose the insertion history. With sorted() we get both: the history remains, and the report is built separately.
Here’s a visual diagram of what’s happening:
flowchart LR
A["Original expenses list (insertion order)"] --> B["sorted() / sortedDescending()"]
B --> C["New list (sorted)"]
A --> D["Original list stays as it was"]
See the “two branches”? That’s the main idea: sorting is a data-processing step, not memory rewriting.
5. Sorting in the ExpensePocket project
Now let’s do sorting not “in a vacuum”, but as part of a small application. Let’s say we store a list of expense amounts (MutableList<Int>), and we have commands: add an expense and print the list. We’ll implement two output modes: “as added” and “sorted descending”.
Expense storage
Right now we don’t need Pair or an “expense record” — that will come later. Today we’re practicing basic sorting, so let the data be as simple as possible.
import kotlin.system.exitProcess
fun main() {
val expenses = mutableListOf<Int>()
while (true) {
print("Command (add/list/list-desc/exit): ")
when (readln().trim()) {
"add" -> addExpense(expenses)
"list" -> printExpenses(expenses)
"list-desc" -> printExpensesDesc(expenses)
"exit" -> exitProcess(0)
}
}
}
Note: expenses is a MutableList, which means we can add elements to it. But for the report we will sort via sorted()/sortedDescending() so we don’t destroy the original order.
Adding an expense
Let’s make a small function to input a number. We already know how to parse an Int and handle errors (at least via toIntOrNull()), so it’ll be simple.
fun addExpense(expenses: MutableList<Int>) {
print("Expense amount: ")
val value = readln().trim().toIntOrNull()
if (value == null) {
println("That is not a number; the expense was not added.")
return
}
expenses.add(value)
println("Added: $value")
}
Printing “as added”
This is our “historical” mode: show the list as-is.
fun printExpenses(expenses: List<Int>) {
println("Expenses (as added): $expenses")
}
Printing “descending”
This is where our topic shows up. We build a sorted copy and print it. We do not touch the original list.
fun printExpensesDesc(expenses: List<Int>) {
val sorted = expenses.sortedDescending()
println("Expenses (descending): $sorted")
}
The function turned out short, but there’s a lot of meaning in it: sorting is not a command “redo the data”, but a command “build me a view of the data sorted”.
To make sure we don’t break the source, you can temporarily add debug output:
fun printExpensesDesc(expenses: List<Int>) {
val sorted = expenses.sortedDescending()
println("Original: $expenses")
println("Sorted: $sorted")
}
If you add expenses 10, 5, 20, you’ll see something like this:
Original: [10, 5, 20]
Sorted: [20, 10, 5]
And that is exactly the behavior we want in a sensible program.
6. Why it sometimes seems like sorting changes the list
Sometimes you get a strange feeling: “I didn’t sort anything ‘inside’, so why did the data become different somewhere else in the program?” This usually happens not because of sorted(), but because somewhere you accidentally started using the same mutable list under two names (aliasing). This is related to what we discussed earlier in the “value vs reference” topic.
With sorted() there are fewer surprises, because it returns a new list, not a “view of the same list”. Kotlin documentation explicitly emphasizes that sorted() creates a new collection (unlike operations that mutate mutable collections in place).
But you can create a surprise yourself if you do this:
fun main() {
val a = mutableListOf(3, 1, 2)
val b = a
b.add(10)
println(a) // [3, 1, 2, 10]
}
This has nothing to do with sorting — it’s about “two variables, one list”. That’s why the principle “don’t mutate the source” is especially good as your program gets bigger: you’re less dependent on who accidentally changed shared data and where.
Lists and equality: order is part of the contents
When you sort, you change the order of elements. Even if the set of values is “the same”, order is considered part of the contents of a list. This matters because later you may compare lists via == and be surprised.
Look:
fun main() {
val a = listOf(1, 2, 3)
val b = listOf(3, 2, 1)
println(a == b) // false
}
To a human, that might be “the same numbers”. To a list, it’s “a different sequence”. And that’s logical: a list is an ordered collection.
This idea will be useful when you start checking whether “something changed or not” and comparing operation results.
7. Typical mistakes when working with sorted() and sortedDescending()
Error No. 1: calling sorted() but not using the result.
This is the most common case: you write xs.sorted() and expect xs to become sorted. But sorted() returns a new list and does not change the original — that’s the basic contract of sorts for read-only collections. The fix is simple: either store it in val sorted = xs.sorted(), or print it immediately: println(xs.sorted()).
Error No. 2: thinking sorting “must” work for any element type.
sorted() and sortedDescending() are designed for elements that have a natural order, meaning they are comparable. If you try to sort something Kotlin cannot compare “by itself”, you’ll run into type constraints. At this stage of the course, that’s a normal signal: for now we sort numbers and strings, and “how to sort more complex things” is a separate topic.
Error No. 3: mixing up sorting and reversing.
Sometimes it feels like sortedDescending() is “sort and then reverse”, and people start trying weird combinations like “I sorted, then sorted again”. The meaning is simpler: sorted() is ascending, sortedDescending() is descending. If you just need to display elements in reverse insertion order, that’s a different task (and a different operation), and it’s not about sorting by value.
Error No. 4: using sorting where you need “largest/smallest” logic and doing extra work.
A very common inefficiency for beginners looks like this: “I want the maximum element” → “I’ll sort the entire list” → “I’ll take the first”. It works, but the program does more than necessary. At this stage we won’t optimize it (clarity is more important), but it’s useful to notice the fact: sorting is a heavy tool, and you should apply it deliberately.
Error No. 5: losing the original “insertion order” in the source list because somewhere you still modified it in place.
Even if today you use sorted() correctly, some other operation may appear nearby that mutates a MutableList. Kotlin explicitly distinguishes between “create a new result” and “change the collection state”, and it’s important to keep that distinction in mind. A good beginner habit is to build reports via sorted()/sortedDescending(), and touch the original data only where you explicitly add/remove elements.