1. Чому масиви порівнюються інакше
Коли ви вперше знайомитеся з масивами, легко сприймати їх як «список, тільки фіксованого розміру». І це частково правда. Але масиви мають свій характер: вони трохи ближчі до «заліза», їх часто використовують як компактний контейнер. Через це в Kotlin (і на JVM загалом) їх порівнюють не так, як List.
Уявімо, що ми розвиваємо наш консольний застосунок обліку витрат (той самий, де зберігаємо витрати як пари «категорія — сума», сортуємо, беремо топ‑N і будуємо звіти). Іноді зручно тримати, наприклад, суми за днями тижня в IntArray — рівно 7 чисел: Пн…Нд. І тут виникають цілком практичні запитання: «чи збігаються два тижневі знімки?», «чи не змінилися дані після перерахунку?», «чи не зламав я щось у логіці?».
І ось тут на вас чекає сюрприз: ви пишете «ну зараз порівняю через ==» — і отримуєте результат, який виглядає як знущання.
Чому == не порівнює масиви поелементно
Якщо коротко й по суті: == для масивів не робить того, чого ви очікуєте. У Kotlin == означає структурну рівність і викликає equals() (із безпечною обробкою null). Але з масивами на JVM історично склалося так, що їхній equals() поводиться як «порівняння контейнерів», а не як «порівняння вмісту». Kotlin прямо попереджає: для масивів не використовуйте ==/!=, якщо хочете порівнювати елементи.
На рівні відчуттів це схоже на ситуацію, коли ви порівнюєте дві однакові коробки з піцою, але замість «однакова начинка?» запитуєте «це та сама коробка з мого холодильника?». У більшості випадків відповідь буде «ні», навіть якщо всередині однаково смачно.
Мініприклад: пастка ==
fun main() {
val a = arrayOf(1, 2, 3)
val b = arrayOf(1, 2, 3)
println(a == b) // false (очікували true? ласкаво просимо)
println(a === b) // false (це точно різні масиви)
}
Чому так? Тому що a і b — це два різні масиви. == у цьому випадку не робить поелементного порівняння. По суті, він перевіряє не «рівні за вмістом».
Водночас для багатьох інших типів == справді означає «порівняти за змістом». Kotlin чітко розділяє структурну (==) і посилальну (===) рівність. Просто масиви — це окрема «особлива зона».
Карта порівнянь: ==, ===, contentEquals, contentDeepEquals
Щоб не тримати все в голові суцільною кашею, зручно мати маленьку табличку — як дорожній знак «обережно, масиви».
| Що пишемо | Що це означає | Підходить для масивів? | Коли використовувати |
|---|---|---|---|
|
структурна рівність (equals) | для масивів не для порівняння елементів | майже завжди для «звичайних» типів і колекцій (List, Map) |
|
посилальна рівність (той самий обʼєкт) | так, але це не про елементи | рідкісні перевірки «це той самий контейнер?» |
|
поелементне порівняння одновимірного масиву | так | коли масив 1D: IntArray, Array<String>, … |
|
«глибоке» порівняння для вкладених масивів | так | коли всередині масивів лежать інші масиви |
Головна думка лекції: якщо ви хочете порівняти масиви за даними — використовуйте contentEquals/contentDeepEquals.
2. contentEquals() для плоских масивів
Коли масив плоский (тобто його елементи — не масиви), майже завжди потрібен contentEquals().
Приклад 1: Array<Int>
fun main() {
val a = arrayOf(1, 2, 3)
val b = arrayOf(1, 2, 3)
println(a.contentEquals(b)) // true
}
Ось це вже схоже на нормальну розмову: «так, елементи однакові й стоять у тому самому порядку».
Приклад 2: IntArray
У наших задачах «облік витрат за днями» логічніше тримати саме IntArray (це примітивний масив на JVM, він ощадливіший). Kotlin дає такі типи, як IntArray, DoubleArray тощо.
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
}
Важливий нюанс: порядок має значення
Як і в списках, порівняння за вмістом враховує порядок. Це логічно: масив — це «послідовність». Якщо переплутали дні місцями — значення вже інше.
fun main() {
val a = intArrayOf(1, 2, 3)
val b = intArrayOf(3, 2, 1)
println(a.contentEquals(b)) // false
}
Як це виглядає в нашому застосунку
Припустімо, ви зробили функцію, яка будує тижневі суми витрат за днями (ми поки що не вибудовуємо архітектуру на класах — просто працюємо з простими типами й колекціями). І ви хочете перевірити: «перерахунок дав той самий результат, що й попередня версія алгоритму».
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("Чи збіглися тижневі підсумки? $same") // Чи збіглися тижневі підсумки? true
}
Це простий, але дуже життєвий «тест здорового глузду»: якщо ви рефакторите код і боїтеся зламати логіку, порівняння масивів за вмістом дає швидкий сигнал.
3. contentDeepEquals() для вкладених масивів
Щойно всередині масиву зʼявляються інші масиви, звичайного contentEquals() стає недостатньо. Він порівняє верхній рівень, а вкладені елементи порівнюватиме як окремі обʼєкти-контейнери. І знову спливе та сама проблема: «контейнери різні, хоча всередині однаково».
Саме для цього Kotlin і дає contentDeepEquals().
Приклад: «двовимірна таблиця» витрат
Уявімо, що ми захотіли зберігати витрати «тиждень × категорії» як таблицю: 7 рядків (дні), а в кожному рядку, наприклад, 3 числа (їжа, транспорт, інше). Це можна подати як вкладений масив.
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
}
Чому contentEquals дає false? Тому що він бачить елементи верхнього масиву, а елементи — це IntArray, тобто окремі масиви. Вони різні за посиланням. Тож contentEquals «згори» каже: «перший елемент не дорівнює першому елементу» — і на цьому все.
А contentDeepEquals «йде всередину» й порівнює вміст вкладених масивів рекурсивно, тому отримуємо true.
Невелика схема: чому потрібен deep
flowchart TD
A[Array верхнього рівня] --> B1[Елемент 0: IntArray]
A --> B2[Елемент 1: IntArray]
C[Інший Array верхнього рівня] --> D1[Елемент 0: IntArray]
C --> D2[Елемент 1: IntArray]
B1 -. contentEquals порівняє як обʼєкт .- D1
B2 -. contentEquals порівняє як обʼєкт .- D2
B1 --> E1[100, 20, 0]
D1 --> F1[100, 20, 0]
Ідея така: contentEquals на верхньому рівні не «розгортає» вкладені масиви за замовчуванням. Саме тому потрібен contentDeepEquals, який робить це свідомо.
4. Знімки та aliasing: пастка «до/після»
Навіть якщо ви використовуєте contentEquals, можна натрапити на іншу неприємність: ви порівнюєте масив «до» і «після», але насправді у вас не два масиви, а один і той самий — просто під двома іменами.
Це та сама історія, яку ми вже обговорювали для MutableList: присвоєння посилального значення створює аліас (друге імʼя). Для масивів це теж актуально.
Приклад: два імені, один масив
fun main() {
val week = intArrayOf(10, 20, 30)
val alias = week
alias[0] = 999
println(week.joinToString()) // 999, 20, 30
println(week === alias) // true
}
Тут порівнювати «до/після» безглуздо, бо «до» у вас просто не залишилося.
Рішення: робити копію, якщо вам потрібен знімок
Якщо ви хочете зберегти попередній стан масиву, зробіть копію (наприклад, copyOf() — ви з цим уже стикалися в темі масивів і фіксованого розміру).
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
}
І ось це вже справжнє «до/після»: один масив зберіг минулий стан, другий — змінився.
У контексті нашого застосунку це дуже корисно. Наприклад, ви перераховуєте тижневі підсумки й хочете звірити, чи змінився результат. Тоді «знімок» допомагає чесно зрозуміти, що сталося.
5. Мініутиліти для проєкту
У навчальному проєкті ми намагаємося писати код так, щоб main() не перетворювався на килим із дротів. Тому корисно загорнути порівняння масивів у невеликі функції. Це ще й робить код читабельнішим: замість «що за contentDeepEquals тут відбувається?» у вас буде «порівняти тижневі матриці».
Функція для плоского масиву
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
}
Суперпроста функція, але вона виграє в «словах»: ви одразу бачите намір.
Функція для «таблиці»
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
}
Зверніть увагу, як приємно читається: sameWeekMatrix одразу натякає, що структура вкладена, а порівняння буде «глибоким».
6. Діагностика: як виводити масиви
Коли ви порівнюєте масиви, часто виникає така думка: «Гаразд, вони не рівні. А де саме відрізняються?». І ось тут виведення масиву стає частиною налагодження.
Для плоских масивів можна використовувати joinToString(), а для вкладених — «глибокі» представлення на кшталт contentDeepToString().
Плоский масив
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
}
Вкладений масив
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]]
}
Таке виведення особливо корисне, коли ви будуєте звіти або таблиці, а потім порівнюєте результати після сортувань чи перерахунків.
7. Типові помилки під час порівняння масивів
Помилка №1: використовувати == «за звичкою» і вірити результату.
Списки (List) часто й справді порівнюються «за вмістом» через ==, тож рука автоматично пише так само й для масивів. Але масиви — виняток: Kotlin прямо попереджає не використовувати ==/!= для порівняння елементів масиву. Якщо ви хочете порівнювати елементи, ваш базовий вибір — contentEquals().
Помилка №2: використовувати contentEquals для вкладених масивів і дивуватися false.
Коли масив містить масиви, contentEquals порівнює елементи верхнього рівня як окремі обʼєкти. У результаті два однакові «за даними» двовимірні масиви виглядають різними. У таких структурах потрібен contentDeepEquals(), який спускається всередину й порівнює вкладений вміст.
Помилка №3: порівнювати «до/після», не зберігши «до».
Іноді ви думаєте, що у вас два масиви, а насправді — одна й та сама структура під двома іменами. Тоді будь-яка зміна «після» миттєво змінює і «до», а порівняння перетворюється на театр абсурду. Якщо вам потрібен знімок, робіть копію (наприклад, copyOf()), а вже потім порівнюйте.
Помилка №4: плутати завдання «один і той самий контейнер?» із завданням «однакові дані?».
=== може бути корисним, але це не заміна contentEquals. === відповідає на запитання «це той самий обʼєкт?» (наприклад, ви випадково передали масив кудись і хочете зрозуміти, чи не той це екземпляр). А «дані збігаються» — це contentEquals/contentDeepEquals.
Помилка №5: перевіряти лише факт false, але не вміти швидко побачити різницю.
Коли порівняння не пройшло, наступний крок — виведення. Для плоских масивів підходить joinToString(), для вкладених — contentDeepToString(). Це пришвидшує налагодження у рази: ви перестаєте гадати «а де саме не збіглося?» і починаєте бачити конкретні числа або рядки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ