Списки, множества и словари

Коллекции Scala по умолчанию неизменяемы — и это меняет то, как вы думаете о данных.

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

Три рабочие лошадки коллекций: List (упорядоченная последовательность), Set (уникальные элементы) и Map (пары ключ-значение). По умолчанию все они неизменяемые: любая операция возвращает новую коллекцию, не трогая исходную.

val nums = List(1, 2, 3, 4)
val unique = Set(1, 2, 2, 3)        // Set(1, 2, 3) — дубликаты ушли
val ages = Map("Аня" -> 25, "Иван" -> 30)

println(nums.head)        // 1 — первый элемент
println(nums(2))          // 3 — по индексу
println(ages("Аня"))      // 25 — по ключу

Синтаксис "Аня" -> 25 — это пара (ключ, значение). Стрелка -> создаёт кортеж из двух элементов.

«Изменение» создаёт новую коллекцию

val nums = List(1, 2, 3)
val more = nums :+ 4       // добавить в конец -> List(1,2,3,4)
val pre = 0 :: nums       // добавить в начало -> List(0,1,2,3)
println(nums)             // List(1,2,3) — оригинал цел!
println(more)             // List(1,2,3,4)

Оператор :: («cons») добавляет элемент в начало списка — это очень дешёвая операция для List.

Проверки и размер

val s = Set(1, 2, 3)
println(s.contains(2))   // true
println(s.size)          // 3
println(List().isEmpty)  // true

Та же идея на Python ▶

# В Python list изменяем, но идею снимка передаёт tuple/frozenset
nums = [1, 2, 3, 4]
unique = set([1, 2, 2, 3])         # {1, 2, 3}
ages = {"Аня": 25, "Иван": 30}

print(nums[0], nums[2])            # 1 3
print(ages["Аня"])                 # 25

# Неизменяемое "добавление" — новый список
more = nums + [5]                  # новый список
print(nums, more)                  # оригинал цел
print(2 in unique, len(unique))    # True 3
List(1,2,3)   :+ 4   ->   List(1,2,3,4)   (новый)
      |
   оригинал НЕ меняется ->  всё ещё List(1,2,3)

Как работает под капотом (JVM)

Неизменяемый List в Scala — это односвязный список: каждый элемент хранит значение и ссылку на хвост. Поэтому добавление в начало (::) мгновенно — создаётся одна новая «голова», а хвост переиспользуется (это называется структурное разделение, structural sharing). Старая коллекция остаётся валидной, потому что её узлы не меняются. Map и Set реализованы как сбалансированные деревья или хеш-структуры, тоже с разделением. Благодаря неизменяемости коллекции безопасны для многопоточности без блокировок.

Частые ошибки

  • Ждать, что операция изменит коллекцию. Она возвращает новую; присвойте результат.
  • Добавлять в конец List в цикле. Это медленно (O(n)); добавляйте в начало или используйте Vector.
  • Обращаться к list(i) по индексу часто. У List это медленно; для индексного доступа берите Vector или массив.

Best practices

  • По умолчанию используйте неизменяемые коллекции — они безопаснее.
  • Для частого индексного доступа выбирайте Vector, для начала-добавления — List.
  • Помните, что результат операций нужно присвоить — оригинал не меняется.

Почему неизменяемость по умолчанию — правильный выбор

Решение сделать коллекции неизменяемыми по умолчанию определяет весь стиль работы с данными в Scala. Неизменяемую коллекцию можно без опаски передать в любую функцию: она не сможет её испортить, в худшем случае вернёт новую. Можно хранить её как ключ или делиться ею между потоками без блокировок. Целый класс коварных багов — кто-то неожиданно изменил список, который вы держали, — просто исчезает.

Вопрос производительности часто пугает новичков: «разве создавать новую коллекцию на каждое изменение не дорого?» Ответ — структурное разделение. Неизменяемые структуры переиспользуют общие части между версиями, поэтому «новая» коллекция обычно делит большую часть данных со старой. Добавление в начало списка не копирует список, а создаёт одну новую ссылку. Понимание этого механизма снимает страх и помогает выбирать правильную коллекцию под задачу.

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

Итоги. List, Set, Map — неизменяемые по умолчанию; операции создают новые коллекции с переиспользованием структуры. Дальше — преобразование коллекций через map и filter.

Проверьте себя
1. Что произойдёт с исходным списком после nums :+ 4?
AОн получит новый элемент 4
BОн не изменится; вернётся новый список с добавленным 4
CОн станет пустым
DБудет ошибка
2. Почему добавление в начало списка через :: очень быстрое?
AСписок сортируется
BСоздаётся новая голова, а хвост переиспользуется без копирования
CСписок хранится в базе данных
DЭлементы удаляются