1. Why arrays are compared differently
When you first run into arrays, you want to think of them as “a list, just with a fixed size.” And that’s partly true. But arrays have a personality: they’re a bit closer to the “metal,” they’re often used as a compact container, and because of that in Kotlin (and on the JVM in general) they aren’t compared the same way as List.
Let’s imagine we’re evolving our console expense-tracking app (the one where we store expenses as “category–amount” pairs, sort them, take top-N, and build reports). Sometimes it’s convenient to keep, for example, amounts by day of the week in an IntArray—exactly 7 numbers: Mon…Sun. And then a practical question comes up: “Do two weekly snapshots match?”, “Did the data change after recalculation?”, “Did I break something in the logic?”.
And this is where the surprise hits: you write “okay, I’ll just compare with ==” — and you get a result that feels like it’s mocking you.
Why == doesn’t compare arrays element-by-element
Briefly and to the point: == for arrays doesn’t do what you expect. In Kotlin, == means structural equality and calls equals() (with safe handling of null). But for arrays on the JVM, historically equals() behaves like “compare containers,” not “compare contents.” And Kotlin explicitly warns you: for arrays, don’t use ==/!= if you want to compare elements.
Intuitively, it’s like comparing two identical pizza boxes, but instead of asking “is the topping the same?” you ask “is this the exact same box from my fridge?”. In most cases the answer will be “no,” even if the inside is equally delicious.
Mini example: the == trap
fun main() {
val a = arrayOf(1, 2, 3)
val b = arrayOf(1, 2, 3)
println(a == b) // false (expected true? welcome)
println(a === b) // false (these are definitely different arrays)
}
Why is that? Because a and b are two different arrays. == in this case does not do an element-by-element comparison, and essentially does not say “equal by content.”
At the same time, for many other types == really does compare “by meaning,” and Kotlin clearly separates structural (==) and referential (===) equality. Arrays are just a special zone.
Comparison map: ==, ===, contentEquals, contentDeepEquals
So you don’t have to keep everything in your head as a mess, it’s handy to have a small table—a road sign that says “careful, arrays.”
| What you write | What it means | Works for arrays? | When to use |
|---|---|---|---|
|
structural equality (equals) | for arrays: not for comparing elements | almost always for “regular” types and collections (List, Map) |
|
referential equality (the exact same object) | yes, but it’s not about elements | rare checks like “is this the same container?” |
|
element-by-element comparison of a flat array | yes | when the array is 1D: IntArray, Array<String>, … |
|
“deep” comparison for nested arrays | yes | when arrays contain other arrays |
The main takeaway of this lecture: if you want to compare arrays by their data, use contentEquals/contentDeepEquals.
2. contentEquals() for flat arrays
When an array is flat (meaning its elements are not arrays), you almost always want contentEquals().
Example 1: Array<Int>
fun main() {
val a = arrayOf(1, 2, 3)
val b = arrayOf(1, 2, 3)
println(a.contentEquals(b)) // true
}
Now this looks like a human conversation: “yes, the elements are the same and in the same order.”
Example 2: IntArray
For our “expense tracking by day” tasks, it’s more logical to use IntArray (it’s a primitive array on the JVM, so it’s more efficient). Kotlin provides types like IntArray, DoubleArray, etc.
fun main() {
val week1 = intArrayOf(100, 0, 250, 0, 90, 500, 10)
val week2 = intArrayOf(100, 0, 250, 0, 90, 500, 10)
println(week1.contentEquals(week2)) // true
}
Important nuance: order matters
Just like with lists, content comparison takes order into account. That’s logical: an array is a sequence. If you swap days around, that’s already a different value.
fun main() {
val a = intArrayOf(1, 2, 3)
val b = intArrayOf(3, 2, 1)
println(a.contentEquals(b)) // false
}
What this looks like in our app
Suppose you wrote a function that builds weekly expense totals by day (we’re not building class-based architecture yet—we’re just working with simple types and collections). And you want to check: “Did the recalculation produce the same result as the previous algorithm version?”
fun main() {
val oldCalc = intArrayOf(120, 0, 50, 80, 0, 0, 10)
val newCalc = intArrayOf(120, 0, 50, 80, 0, 0, 10)
val same = oldCalc.contentEquals(newCalc)
println("Did the weekly totals match? $same") // Did the weekly totals match? true
}
This is a simple but very real “sanity test”: if you’re refactoring code and afraid of breaking logic, comparing arrays by content gives you a quick signal.
3. contentDeepEquals() for nested arrays
As soon as an array contains other arrays, a regular contentEquals() is no longer enough. It will compare the top level, but nested elements will be compared as separate container objects. And the same problem resurfaces: “different containers, even though the inside is identical.”
That’s exactly why Kotlin provides contentDeepEquals().
Example: a “2D table” of expenses
Let’s imagine we wanted to store expenses as a “week × categories” table: 7 rows (days), and in each row, say, 3 numbers (food, transport, other). This can be represented as a nested array.
fun main() {
val a = arrayOf(
intArrayOf(100, 20, 0),
intArrayOf(0, 0, 10)
)
val b = arrayOf(
intArrayOf(100, 20, 0),
intArrayOf(0, 0, 10)
)
println(a.contentEquals(b)) // false
println(a.contentDeepEquals(b)) // true
}
Why does contentEquals return false? Because it sees the elements of the top-level array, and those elements are IntArray—i.e., separate arrays. They differ by reference. So contentEquals at the top says: “the first element is not equal to the first element,” and that’s it.
But contentDeepEquals “goes inside” and compares the contents of nested arrays recursively, so we get true.
A small diagram: why you need deep
flowchart TD
A[Top-level Array] --> B1[Element 0: IntArray]
A --> B2[Element 1: IntArray]
C[Another top-level Array] --> D1[Element 0: IntArray]
C --> D2[Element 1: IntArray]
B1 -. contentEquals compares as an object .- D1
B2 -. contentEquals compares as an object .- D2
B1 --> E1[100, 20, 0]
D1 --> F1[100, 20, 0]
The idea: contentEquals at the top level does not “unwrap” nested arrays by default, so you need contentDeepEquals, which does it explicitly.
4. Snapshots and aliasing: the “before/after” trap
Even if you use contentEquals, you can run into another nasty issue: you compare an array “before” and “after,” but in reality you don’t have two arrays—you have the same array under two different names.
It’s the same story we discussed for MutableList: assigning a reference value creates an alias (a second name). That’s also relevant for arrays.
Example: two names, one array
fun main() {
val week = intArrayOf(10, 20, 30)
val alias = week
alias[0] = 999
println(week.joinToString()) // 999, 20, 30
println(week === alias) // true
}
Comparing “before/after” is pointless here, because you no longer have a “before.”
Solution: make a copy if you need a snapshot
If you want to preserve the old state of an array, make a copy (for example, copyOf()—you’ve already seen it in the arrays and fixed-size topic).
fun main() {
val week = intArrayOf(10, 20, 30)
val snapshot = week.copyOf()
week[0] = 999
println(snapshot.joinToString()) // 10, 20, 30
println(snapshot.contentEquals(week)) // false
}
And now this is a real “before/after”: one array kept the past state, the other changed.
In the context of our app, this is genuinely useful: for example, you recalculate weekly totals and want to compare whether the result changed. A “snapshot” helps you honestly see what happened.
5. Mini utilities for the project
In the learning project, we try to write code so that main() doesn’t turn into a spaghetti mess. So it’s useful to wrap array comparisons into small functions. This also makes the code more readable: instead of “why is contentDeepEquals happening here?” you’ll have “compare week matrices.”
Function for a flat array
fun sameWeeklyTotals(a: IntArray, b: IntArray): Boolean {
return a.contentEquals(b)
}
fun main() {
val x = intArrayOf(1, 2, 3)
val y = intArrayOf(1, 2, 3)
println(sameWeeklyTotals(x, y)) // true
}
A super-simple function, but it wins in “words”: you immediately read the intent.
Function for a “table”
fun sameWeekMatrix(a: Array<IntArray>, b: Array<IntArray>): Boolean {
return a.contentDeepEquals(b)
}
fun main() {
val a = arrayOf(intArrayOf(1, 2), intArrayOf(3, 4))
val b = arrayOf(intArrayOf(1, 2), intArrayOf(3, 4))
println(sameWeekMatrix(a, b)) // true
}
Notice how nicely it reads: sameWeekMatrix immediately hints that the structure is nested and the comparison will be “deep.”
6. Diagnostics: how to print arrays
When you compare arrays, the next thought is often: “Okay, they’re not equal. But where exactly do they differ?” And this is where printing arrays becomes part of debugging.
For flat arrays you can use joinToString(), and for nested ones—“deep” representations like contentDeepToString().
Flat array
fun main() {
val a = intArrayOf(1, 2, 3)
val b = intArrayOf(1, 999, 3)
println("a = ${a.joinToString()}") // a = 1, 2, 3
println("b = ${b.joinToString()}") // b = 1, 999, 3
}
Nested array
fun main() {
val a = arrayOf(intArrayOf(1, 2), intArrayOf(3, 4))
val b = arrayOf(intArrayOf(1, 2), intArrayOf(3, 999))
println(a.contentDeepToString()) // [[1, 2], [3, 4]]
println(b.contentDeepToString()) // [[1, 2], [3, 999]]
}
These prints are especially useful when you build reports/tables and then compare results after sorting or recalculation.
7. Common mistakes when comparing arrays
Mistake #1: using == “out of habit” and trusting the result.
Lists (List) are often truly compared “by content” via ==, so your hand automatically writes the same for arrays. But arrays are the exception: Kotlin explicitly warns not to use ==/!= to compare array elements. If you want to compare elements, your default choice is contentEquals().
Mistake #2: using contentEquals for nested arrays and being surprised by false.
When an array contains arrays, contentEquals compares top-level elements as separate objects. As a result, two 2D arrays that are identical “by data” look different. For such structures you need contentDeepEquals(), which goes inside and compares nested contents.
Mistake #3: comparing “before/after” without preserving “before.”
Sometimes you think you have two arrays, but in reality you have one and the same structure under two names. Then any “after” change instantly changes “before” as well, and the comparison becomes absurd theater. If you need a snapshot, make a copy (for example, copyOf()) and only then compare.
Mistake #4: confusing “is it the same container?” with “is the data the same?”
=== can be useful, but it’s not a replacement for contentEquals. === answers “is it the same object?” (for example, you accidentally passed an array somewhere and want to see whether it’s that exact instance). But “the data matches” is contentEquals/contentDeepEquals.
Mistake #5: checking only that it’s false, but not being able to quickly see the difference.
When comparison fails, the next step is printing. For flat arrays, joinToString() works well; for nested ones, use contentDeepToString(). This speeds up debugging drastically, because you stop guessing “where exactly did it not match?” and start seeing specific numbers/strings.