JavaRush /Курси /Kotlin SELF /Порівняння масивів: чому == не підходить і коли потрібен ...

Порівняння масивів: чому == не підходить і коли потрібен contentDeepEquals

Kotlin SELF
Рівень 23 , Лекція 4
Відкрита

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

Щоб не тримати все в голові суцільною кашею, зручно мати маленьку табличку — як дорожній знак «обережно, масиви».

Що пишемо Що це означає Підходить для масивів? Коли використовувати
a == b
структурна рівність (equals) для масивів не для порівняння елементів майже завжди для «звичайних» типів і колекцій (List, Map)
a === b
посилальна рівність (той самий обʼєкт) так, але це не про елементи рідкісні перевірки «це той самий контейнер?»
a.contentEquals(b)
поелементне порівняння одновимірного масиву так коли масив 1D: IntArray, Array<String>, …
a.contentDeepEquals(b)
«глибоке» порівняння для вкладених масивів так коли всередині масивів лежать інші масиви

Головна думка лекції: якщо ви хочете порівняти масиви за даними — використовуйте 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(). Це пришвидшує налагодження у рази: ви перестаєте гадати «а де саме не збіглося?» і починаєте бачити конкретні числа або рядки.

1
Опитування
Сортування, рівень 23, лекція 4
Недоступний
Сортування
Сортування
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ