JavaRush /Курсы /Kotlin SELF /Сравнение массивов: почему == не подходит и когда нужен c...

Сравнение массивов: почему == не подходит и когда нужен contentDeepEquals

Kotlin SELF
23 уровень , 4 лекция
Открыта

1. Почему массивы сравниваются иначе

Когда вы впервые встречаете массивы, хочется думать о них как о «списке, только фиксированного размера». И это частично правда. Но у массивов есть характер: они чуть ближе к «железу», их часто используют как компактный контейнер, и из‑за этого в Kotlin (и на JVM в целом) они сравниваются не так, как List.

Представим, что мы развиваем наше консольное приложение учёта расходов (то самое, где мы храним расходы как пары “категория–сумма”, сортируем, берём top‑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: присваивание ссылочного значения создаёт alias (второе имя). Для массивов это тоже актуально.

Пример: два имени, один массив

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
Задача
Kotlin SELF, 23 уровень, 4 лекция
Недоступна
Три проверки массива
Три проверки массива
1
Задача
Kotlin SELF, 23 уровень, 4 лекция
Недоступна
Итоги недели
Итоги недели
1
Задача
Kotlin SELF, 23 уровень, 4 лекция
Недоступна
Две таблицы сравнение
Две таблицы сравнение
1
Опрос
Сортировки, 23 уровень, 4 лекция
Недоступен
Сортировки
Сортировки
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ