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
Чтобы не держать всё в голове как кашу, удобно иметь маленькую табличку, как дорожный знак “осторожно, массивы”.
| Что пишем | Что это означает | Подходит для массивов? | Когда использовать |
|---|---|---|---|
|
структурное равенство (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: присваивание ссылочного значения создаёт 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(). Это ускоряет отладку в разы, потому что вы перестаёте гадать “а где именно не совпало?” и начинаете видеть конкретные числа/строки.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ