JavaRush /Курсы /Kotlin SELF /Set и MutableSet — уникальность и проверки принадлежности...

Set и MutableSet — уникальность и проверки принадлежности

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

1. Зачем нужен Set, если уже есть List

Если List — это «очередь людей с номерками», то Set — это «список гостей на входе», где важно только одно: человек либо есть в списке, либо его нет. В программировании такая модель встречается постоянно: «разрешённые команды», «уникальные теги», «уже обработанные значения», «не добавлять повторно». Set помогает выражать эту идею напрямую, а не через костыли вроде «проверю список перед добавлением».

С точки зрения Kotlin, Set<T> хранит уникальные элементы: одно и то же значение не может существовать внутри множества «в двух экземплярах». Обычно ещё говорят, что порядок в множестве не является частью смысла. Документация формулирует это так: Set хранит уникальные элементы, а их порядок «обычно не определён».

Представьте, что вы делаете маленькое консольное приложение, которое хранит записи расходов (мы начали это в прошлой лекции на списках). В какой-то момент вам почти наверняка захочется:

  • хранить список расходов (там повторы допустимы: кофе можно купить 200 раз);
  • но при этом держать уникальный набор категорий, которые уже встречались (чтобы показывать подсказки или валидировать ввод).

Вот это и есть классическое место для Set.

2. Set и MutableSet: чтение и изменения

Когда вы впервые видите Set и MutableSet, очень хочется подумать: «О, Set — это как val, а MutableSet — как var». И вот здесь Kotlin коварно улыбается.

Set<T> — это интерфейс для чтения: размер, проверка наличия, обход.
MutableSet<T> — это интерфейс для изменения: добавление, удаление.

То есть разница не в том, можно ли изменить переменную, а в том, какие операции доступны через тип. Этот стиль такой же, как у List/MutableList.

И ещё один важный момент: mutable-множество почти всегда стоит хранить в val. Тогда вы не сможете случайно «перепривязать» переменную к другому множеству, но сможете честно менять содержимое через add/remove. Эта идея уже звучала в обзоре коллекций Kotlin: val защищает ссылку, но не запрещает write-операции у mutable-коллекции.

Создание множеств: setOf, mutableSetOf, пустые множества

Когда вы только начинаете, удобнее всего создавать множества фабричными функциями стандартной библиотеки. Они читаются как «создай мне список/множество прямо сейчас», без лишнего шума. А ещё это отлично ложится на привычный стиль из listOf(...) и mutableListOf(...), который мы уже использовали для списков.

setOf(...) и «схлопывание» дублей

fun main() {
    val tags = setOf("kotlin", "kotlin", "jvm")

    println(tags)        // [kotlin, jvm]
    println(tags.size)   // 2
}

Здесь важно не то, в каком порядке напечатается множество, а то, что повтор "kotlin" не создаёт второй элемент. Set — это про уникальность, а не про «сколько раз встретилось».

Пустое множество: иногда нужен явный тип

Как и со списками, пустая коллекция часто требует подсказки компилятору: «а элементы какого типа ты тут ожидаешь?».

fun main() {
    val usedCategories = mutableSetOf<String>()

    usedCategories.add("food")
    println(usedCategories) // [food]
}

Если вы попробуете написать просто mutableSetOf() без <String>, Kotlin может честно ответить: «Я не знаю, что тут должно лежать». И он будет прав.

3. Базовые операции множества

Уникальность как контракт: add() и возвращаемое Boolean

В списке add() почти всегда означает «элемент точно добавился (в конец)». В множестве всё интереснее: add(x) означает «попробуй добавить, но если такой элемент уже есть — ничего не меняй». Поэтому операция возвращает Boolean: получилось ли реально добавить новый элемент.

Это очень полезно, когда вы хотите сделать «один раз сообщить» или «не дублировать действие».

fun main() {
    val seen = mutableSetOf<Int>()

    println(seen.add(10)) // true
    println(seen.add(10)) // false
    println(seen)         // [10]
}

Вторая попытка не добавила «вторую десятку» — она просто не нужна по смыслу множества.

Проверка принадлежности: contains() и оператор in

Самая «натуральная» операция для Set — это вопрос: «А элемент тут есть?». В Kotlin это можно делать двумя одинаково корректными способами: через contains(x) и через x in set. Второй вариант обычно читается приятнее, особенно в условиях.

И да, это одна из причин, почему множество так любят для проверок «разрешено/запрещено»: код становится почти разговорным.

fun main() {
    val allowedCommands = setOf("add", "list", "remove")

    val cmd = "list"
    println(cmd in allowedCommands) // true
}

На этом месте очень удобно держать в голове образ: Set — это «набор допустимых значений». Он не про порядок, не про индексы, и уж точно не про «удали элемент номер 3».

Удаление: remove() и что оно возвращает

Удаление в MutableSet похоже на remove(value) у списка, но по смыслу естественнее: в множестве нет индексов, поэтому удаление всегда «по значению». Как и add, оно возвращает Boolean: удалось ли что-то удалить.

fun main() {
    val usedCategories = mutableSetOf("food", "transport")

    println(usedCategories.remove("food")) // true
    println(usedCategories.remove("food")) // false
    println(usedCategories)                // [transport]
}

Второй раз удалять нечего — "food" уже отсутствует.

4. Порядок, равенство и «убрать дубли»

Можно ли полагаться на порядок в Set

Интуитивно хочется: «Ну оно же печатается как "[a, b, c]", значит порядок есть». И Kotlin действительно часто делает так, что порядок выглядит стабильным. Но здесь важно отделить «как сегодня реализовано» от «какой смысл у структуры данных».

Формально Set — это множество: уникальные элементы, и порядок «вообще не главное».
При этом в стандартной библиотеке Kotlin реализация MutableSet по умолчанию — LinkedHashSet, и она сохраняет порядок вставки, поэтому first() и last() могут быть предсказуемыми именно в этой реализации.

Практическое правило для начинающего звучит так: относитесь к Set как к контейнеру, где порядок не является контрактом. Если ваша логика разваливается от перестановки элементов — скорее всего, вам нужна другая структура (обычно список).

«Убрать дубли»: toSet() как быстрый фильтр уникальности

Один из самых частых прикладных сценариев: у вас уже есть список (или массив) с повторами, а вам нужен набор уникальных значений. Например, пользователь вводил категории как попало, и вы хотите получить «список всех уникальных категорий».

Для этого есть toSet(). Она создаёт новую коллекцию и «схлопывает» дубли. Важно, что функции копирования/конвертации вроде toSet() делают снимок на момент вызова: изменения исходной коллекции потом не «магически» отражаются в результате.

fun main() {
    val words = listOf("a", "b", "a", "c")
    val uniqueWords = words.toSet()

    println(uniqueWords)      // [a, b, c]
    println(uniqueWords.size) // 3
}

То же самое работает и для массивов: массив можно превратить в Set, и вы увидите, что повторы исчезли, а при превращении в List — останутся.

Равенство множеств: важнее элементы, а не порядок

У списков равенство зависит от порядка: [1, 2] не равно [2, 1]. У множеств логика другая: если элементы одинаковые, множества равны, даже если вы создавали их «в разном порядке». Это соответствует математическому смыслу множества и полезно в реальной жизни, когда вы сравниваете наборы разрешений/тегов/фичей.

В документации Kotlin это показано на примере: множества равны, если в них одни и те же элементы.

fun main() {
    val a = setOf(1, 2, 3, 4)
    val b = setOf(4, 3, 2, 1)

    println(a == b) // true
}

5. Пример: учёт расходов и уникальные категории

Сейчас мы продолжим наш мини-проект дня: «консольный учёт расходов», без классов и без сложной архитектуры — просто чтобы набить руку на коллекциях. В прошлой лекции мы хранили записи расходов в MutableList<Triple<String, String, Int>>, где поля — это категория, описание и сумма.

Теперь добавим ещё одну структуру: MutableSet<String> для уникальных категорий, которые уже встречались. Это даст нам две полезные вещи: быстрый ответ на вопрос «категория уже была?» и возможность в любой момент получить «список категорий без дублей», не пробегая вручную по всему списку.

Мини-схема данных

Нарисуем очень простую схему, чтобы голова не пыталась держать всё в памяти (память — штука дорогая, особенно в понедельник утром):

flowchart LR
    Records["records: MutableList⟨Triple⟨Category, Title, Amount⟩⟩"]
    Categories["knownCategories: MutableSet⟨Category⟩"]

    Records -->|"добавили запись"| Categories

Идея: каждая новая запись может «подкинуть» новую категорию в множество. Если категория уже была — множество просто не изменится.

Код: добавление записи и обновление множества категорий

Здесь мы сознательно пишем маленькие функции и короткие примеры, чтобы код читался как учебник, а не как «взрослый прод» (он ещё успеет вас догнать).

fun addExpense(
    records: MutableList<Triple<String, String, Int>>,
    knownCategories: MutableSet<String>,
    category: String,
    title: String,
    amount: Int
) {
    records.add(Triple(category, title, amount))
    knownCategories.add(category)
}

Обратите внимание: knownCategories.add(category) ничего не ломает, даже если категория уже была. В этом и кайф множества: оно защищает нас от дублей по контракту.

Проверка «новая категория или нет» через add()

Теперь используем тот факт, что add возвращает Boolean, чтобы вывести пользователю (или самим себе в отладке) полезное сообщение: новая категория или нет.

fun addCategoryIfNew(knownCategories: MutableSet<String>, category: String) {
    val added = knownCategories.add(category)

    if (added) {
        println("Новая категория: $category") // например: Новая категория: food
    } else {
        println("Категория уже была: $category") // например: Категория уже была: food
    }
}

Список так «из коробки» не умеет: в списке вам бы пришлось отдельно делать contains, а потом add, и легко ошибиться (или просто забыть).

Полный мини-пример: записи и категории

Соберём маленький main, который демонстрирует идею целиком. Он не претендует на полноценный интерфейс — это учебная «песочница», где мы руками проверяем, что структуры данных ведут себя так, как обещают.

fun main() {
    val records = mutableListOf<Triple<String, String, Int>>()
    val knownCategories = mutableSetOf<String>()

    addExpense(records, knownCategories, "food", "coffee", 300)
    addExpense(records, knownCategories, "transport", "taxi", 1200)
    addExpense(records, knownCategories, "food", "lunch", 550)

    println(records.size)              // 3
    println(knownCategories)           // [food, transport] (порядок не считаем контрактом)
    println("food" in knownCategories) // true
}

Здесь очень хорошо видно: записей три (две из них в категории "food"), а категорий всего две — потому что множество хранит уникальные значения.

6. Set и null: да, можно — но аккуратно

Иногда данные приходят «грязные»: категория не распарсилась, пользователь ввёл пустую строку, а вы пока не навели порядок. Раз уж мы уже проходили null-safety, полезно знать: Set может хранить null, но как уникальный элемент — максимум один null в множестве. Документация прямо говорит, что null в Set тоже уникален: второй null не появится.

Это не призыв хранить null везде, где можно (мы не коллекционируем проблемы), но полезное знание для отладки и переходных состояний.

fun main() {
    val categories: MutableSet<String?> = mutableSetOf()

    categories.add(null)
    categories.add(null)

    println(categories.size) // 1
    println(categories)      // [null]
}

7. Типичные ошибки при работе с Set и MutableSet

Ошибка №1: ожидать, что множество хранит повторы «как список».
Новички часто добавляют одно и то же значение в MutableSet и удивляются: «Почему размер не растёт?». Потому что Set по смыслу хранит уникальные элементы. Если вам нужно хранить «каждый ввод пользователя», даже если он повторяется, это задача списка. А если вам нужно хранить «какие значения вообще встречались» — это задача множества.

Ошибка №2: писать логику, зависящую от порядка элементов в Set.
Иногда всё выглядит так, будто порядок есть, и рука тянется сделать «возьму первый элемент и буду считать его главным». В Kotlin действительно часто используется реализация, сохраняющая порядок вставки, но смысл Set — не в этом. Как только вы начнёте опираться на порядок, код станет хрупким: поменяется реализация, поменяется источник данных, и поведение неожиданно поплывёт.

Ошибка №3: пытаться обращаться к Set по индексу.
После List очень хочется написать что-то вроде mySet[0]. Но у множества нет индексов как части модели. Это сигнал, что вы выбрали не тот контейнер под задачу. Если вам нужна позиция — берите List. Если вам нужна уникальность и проверки «есть/нет» — берите Set.

Ошибка №4: забывать, что add/remove возвращают Boolean, и терять полезную информацию.
Многие просто вызывают add(x) и не смотрят на результат, хотя это почти бесплатная подсказка о том, произошло ли изменение. Когда вы делаете валидацию ввода или ведёте список «уже виденных значений», этот Boolean помогает писать код проще и понятнее: «если добавилось впервые — сообщи/сделай действие, иначе — пропусти».

Ошибка №5: создавать пустое mutableSetOf() без типа и получать ошибку компиляции.
Пустая коллекция не даёт компилятору подсказок, поэтому иногда нужно явно указать тип: mutableSetOf<String>(). Это нормально и не делает вас «хуже программистом». Скорее наоборот: вы явно фиксируете контракт, а Kotlin перестаёт гадать, что вы имели в виду.

1
Задача
Kotlin SELF, 19 уровень, 2 лекция
Недоступна
Проверка команды
Проверка команды
1
Задача
Kotlin SELF, 19 уровень, 2 лекция
Недоступна
Слова без повторов
Слова без повторов
1
Задача
Kotlin SELF, 19 уровень, 2 лекция
Недоступна
Регистрация гостей
Регистрация гостей
1
Задача
Kotlin SELF, 19 уровень, 2 лекция
Недоступна
Консоль подписок
Консоль подписок
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ