На этой странице показаны общие коллекции Scala 3 и сопутствующие им методы. Scala поставляется с большим количеством типов коллекций, на изучение которых может уйти время, поэтому желательно начать с нескольких из них, а затем использовать остальные по мере необходимости. Точно так же у каждого типа коллекции есть десятки методов, облегчающих разработку, поэтому лучше начать изучение лишь с небольшого количества.
В этом разделе представлены наиболее распространенные типы и методы коллекций, которые вам понадобятся для начала работы.
В конце этого раздела представлены дополнительные ссылки, для более глубокого изучения коллекций.
Три основные категории коллекций
Для коллекций Scala можно выделить три основные категории:
- Последовательности (Sequences/Seq) представляют собой последовательный набор элементов и могут быть индексированными (как массив) или линейными (как связанный список)
- Мапы (Maps) содержат набор пар ключ/значение, например Java
Map, Python dictionary или RubyHash - Множества (Sets) — это неупорядоченный набор уникальных элементов
Все они являются базовыми типами и имеют подтипы подходящие под конкретные задачи, таких как параллелизм (concurrency), кэширование (caching) и потоковая передача (streaming). В дополнение к этим трем основным категориям существуют и другие полезные типы коллекций, включая диапазоны (ranges), стеки (stacks) и очереди (queues).
Иерархия коллекций
В качестве краткого обзора следующие три рисунка показывают иерархию классов и трейтов в коллекциях Scala.
На первом рисунке показаны типы коллекций в пакете scala.collection. Все это высокоуровневые абстрактные классы или трейты, которые обычно имеют неизменяемые и изменяемые реализации.
На этом рисунке показаны все коллекции в пакете scala.collection.immutable:
А на этом рисунке показаны все коллекции в пакете scala.collection.mutable:
В следующих разделах представлены некоторые из распространенных типов.
Общие коллекции
Основные коллекции, используемые чаще всего:
| Тип коллекции | Неизменяемая | Изменяемая | Описание |
|---|---|---|---|
List |
✓ | Линейная неизменяемая последовательность (связный список) | |
Vector |
✓ | Индексированная неизменяемая последовательность | |
LazyList |
✓ | Ленивый неизменяемый связанный список, элементы которого вычисляются только тогда, когда они необходимы; подходит для больших или бесконечных последовательностей. | |
ArrayBuffer |
✓ | Подходящий тип для изменяемой индексированной последовательности | |
ListBuffer |
✓ | Используется, когда вам нужен изменяемый список; обычно преобразуется в List |
|
Map |
✓ | ✓ | Итерируемая коллекция, состоящая из пар ключей и значений |
Set |
✓ | ✓ | Итерируемая коллекция без повторяющихся элементов |
Как показано, Map и Set бывают как изменяемыми, так и неизменяемыми.
Основы каждого типа демонстрируются в следующих разделах.
В Scala буфер (buffer), такой как
ArrayBufferилиListBuffer, представляет собой последовательность, которая может увеличиваться и уменьшаться.
Примечание о неизменяемых коллекциях
В последующих разделах всякий раз, когда используется слово immutable, можно с уверенностью сказать, что тип предназначен для использования в стиле функционального программирования (ФП). С помощью таких типов коллекция не меняется, а при вызове функциональных методов возвращается новый результат - новая коллекция.
Выбор последовательности
При выборе последовательности (последовательной коллекции элементов) нужно руководствоваться двумя основными вопросами:
- должна ли последовательность индексироваться (как массив), обеспечивая быстрый доступ к любому элементу, или она должна быть реализована как линейный связанный список?
- необходима изменяемая или неизменяемая коллекция?
Рекомендуемые универсальные последовательности:
| Тип\Категория | Неизменяемая | Изменяемая |
|---|---|---|
| индексируемая | Vector |
ArrayBuffer |
| линейная (связанный список) | List |
ListBuffer |
Например, если нужна неизменяемая индексированная коллекция, в общем случае следует использовать Vector.
И наоборот, если нужна изменяемая индексированная коллекция, используйте ArrayBuffer.
ListиVectorчасто используются при написании кода в функциональном стиле.ArrayBufferобычно используется при написании кода в императивном стиле.ListBufferиспользуется тогда, когда стили смешиваются, например, при создании списка.
Следующие несколько разделов кратко демонстрируют типы List, Vector и ArrayBuffer.
List
List представляет собой линейную неизменяемую последовательность. Каждый раз, когда в список добавляются или удаляются элементы, по сути создается новый список из существующего.
Создание списка
List можно создать различными способами:
val ints = List(1, 2, 3)
val names = List("Joel", "Chris", "Ed")
// другой путь создания списка List
val namesAgain = "Joel" :: "Chris" :: "Ed" :: Nil
При желании тип списка можно объявить, хотя обычно в этом нет необходимости:
val ints: List[Int] = List(1, 2, 3)
val names: List[String] = List("Joel", "Chris", "Ed")
Одно исключение — когда в коллекции смешанные типы; в этом случае тип желательно указывать явно:
val things: List[Any] = List(1, "two", 3.0)
val things: List[String | Int | Double] = List(1, "two", 3.0) // с типами объединения
val thingsAny: List[Any] = List(1, "two", 3.0) // с Any
Добавление элементов в список
Поскольку List неизменяем, в него нельзя добавлять новые элементы.
Вместо этого создается новый список с добавленными к существующему списку элементами.
Например, учитывая этот List:
val a = List(1, 2, 3)
Для добавления (prepend) к началу списка одного элемента используется метод ::, для добавления нескольких — :::, как показано здесь:
val b = 0 :: a // List(0, 1, 2, 3)
val c = List(-1, 0) ::: a // List(-1, 0, 1, 2, 3)
Также можно добавить (append) элементы в конец List, но, поскольку List является односвязным,
следует добавлять к нему элементы только в начало;
добавление элементов в конец списка — относительно медленная операция,
особенно при работе с большими последовательностями.
Совет: если необходимо добавлять к неизменяемой последовательности элементы в начало и конец, используйте
Vector.
Поскольку List является связанным списком,
крайне нежелательно пытаться получить доступ к элементам больших списков по значению их индекса.
Например, если есть List с миллионом элементов, доступ к такому элементу, как myList(999_999),
займет относительно много времени, потому что этот запрос должен пройти почти через все элементы.
Если есть большая коллекция и необходимо получать доступ к элементам по их индексу, то
вместо List используйте Vector или ArrayBuffer.
Как запомнить названия методов
В методах Scala символ : представляет сторону, на которой находится последовательность,
поэтому, когда используется метод +:, список нужно указывать справа:
0 +: a
Аналогично, если используется :+, список должен быть слева:
a :+ 4
Хорошей особенностью таких символических имен у методов является то, что они стандартизированы.
Те же имена методов используются с другими неизменяемыми последовательностями, такими как Seq и Vector.
Также можно использовать несимволические имена методов для добавления элементов в начало (a.prepended(4))
или конец (a.appended(4)).
Как пройтись по списку
Представим, что есть List имён:
val names = List("Joel", "Chris", "Ed")
Напечатать каждое имя можно следующим способом:
for (name <- names) println(name)
for name <- names do println(name)
Вот как это выглядит в REPL:
scala> for (name <- names) println(name)
Joel
Chris
Ed
scala> for name <- names do println(name)
Joel
Chris
Ed
Преимуществом использования выражений вида for с коллекциями в том, что Scala стандартизирован,
и один и тот же подход работает со всеми последовательностями,
включая Array, ArrayBuffer, List, Seq, Vector, Map, Set и т.д.
Немного истории
Список Scala подобен списку из языка программирования Lisp, который был впервые представлен в 1958 году. Действительно, в дополнение к привычному способу создания списка:
val ints = List(1, 2, 3)
точно такой же список можно создать следующим образом:
val list = 1 :: 2 :: 3 :: Nil
REPL показывает, как это работает:
scala> val list = 1 :: 2 :: 3 :: Nil
list: List[Int] = List(1, 2, 3)
Это работает, потому что List — односвязный список, оканчивающийся элементом Nil,
а :: — это метод List, работающий как оператор “cons” в Lisp.
Отступление: LazyList
Коллекции Scala также включают LazyList, который представляет собой ленивый неизменяемый связанный список. Он называется «ленивым» — или нестрогим — потому что вычисляет свои элементы только тогда, когда они необходимы.
Вы можете увидеть отложенное вычисление LazyList в REPL:
val x = LazyList.range(1, Int.MaxValue)
x.take(1) // LazyList(<not computed>)
x.take(5) // LazyList(<not computed>)
x.map(_ + 1) // LazyList(<not computed>)
Во всех этих примерах ничего не происходит.
Действительно, ничего не произойдет, пока вы не заставите это произойти, например, вызвав метод foreach:
scala> x.take(1).foreach(println)
1
Дополнительные сведения об использовании, преимуществах и недостатках строгих и нестрогих (ленивых) коллекций см. в обсуждениях “строгих” и “нестрогих” на странице Архитектура коллекции в Scala 2.13.
Vector
Vector - это индексируемая неизменяемая последовательность.
“Индексируемая” часть описания означает, что она обеспечивает произвольный доступ
и обновление за практически постоянное время,
поэтому можно быстро получить доступ к элементам Vector по значению их индекса,
например, получить доступ к listOfPeople(123_456_789).
В общем, за исключением той разницы, что (а) Vector индексируется, а List - нет,
и (б) List имеет метод ::, эти два типа работают одинаково,
поэтому мы быстро пробежимся по следующим примерам.
Вот несколько способов создания Vector:
val nums = Vector(1, 2, 3, 4, 5)
val strings = Vector("one", "two")
case class Person(name: String)
val people = Vector(
Person("Bert"),
Person("Ernie"),
Person("Grover")
)
Поскольку Vector неизменяем, в него нельзя добавить новые элементы.
Вместо этого создается новая последовательность, с добавленными к существующему Vector в начало или в конец элементами.
Например, так элементы добавляются в конец:
val a = Vector(1,2,3) // Vector(1, 2, 3)
val b = a :+ 4 // Vector(1, 2, 3, 4)
val c = a ++ Vector(4, 5) // Vector(1, 2, 3, 4, 5)
А так - в начало Vector-а:
val a = Vector(1,2,3) // Vector(1, 2, 3)
val b = 0 +: a // Vector(0, 1, 2, 3)
val c = Vector(-1, 0) ++: a // Vector(-1, 0, 1, 2, 3)
В дополнение к быстрому произвольному доступу и обновлениям, Vector обеспечивает быстрое добавление в начало и конец.
Подробную информацию о производительности
Vectorи других коллекций см. в характеристиках производительности коллекций.
Наконец, Vector в выражениях вида for используется точно так же, как List, ArrayBuffer или любая другая последовательность:
scala> val names = Vector("Joel", "Chris", "Ed")
val names: Vector[String] = Vector(Joel, Chris, Ed)
scala> for (name <- names) println(name)
Joel
Chris
Ed
scala> val names = Vector("Joel", "Chris", "Ed")
val names: Vector[String] = Vector(Joel, Chris, Ed)
scala> for name <- names do println(name)
Joel
Chris
Ed
ArrayBuffer
ArrayBuffer используется тогда, когда нужна изменяемая индексированная последовательность общего назначения.
Поскольку ArrayBuffer индексирован, произвольный доступ к элементам выполняется быстро.
Создание ArrayBuffer
Чтобы использовать ArrayBuffer, его нужно вначале импортировать:
import scala.collection.mutable.ArrayBuffer
Если необходимо начать с пустого ArrayBuffer, просто укажите его тип:
var strings = ArrayBuffer[String]()
var ints = ArrayBuffer[Int]()
var people = ArrayBuffer[Person]()
Если известен примерный размер ArrayBuffer, его можно задать:
// готов вместить 100 000 чисел
val buf = new ArrayBuffer[Int](100_000)
Чтобы создать новый ArrayBuffer с начальными элементами,
достаточно просто указать начальные элементы, как для List или Vector:
val nums = ArrayBuffer(1, 2, 3)
val people = ArrayBuffer(
Person("Bert"),
Person("Ernie"),
Person("Grover")
)
Добавление элементов в ArrayBuffer
Новые элементы добавляются в ArrayBuffer с помощью методов += и ++=.
Также можно использовать текстовый аналог: append, appendAll, insert, insertAll, prepend и prependAll.
Вот несколько примеров с += и ++=:
val nums = ArrayBuffer(1, 2, 3) // ArrayBuffer(1, 2, 3)
nums += 4 // ArrayBuffer(1, 2, 3, 4)
nums ++= List(5, 6) // ArrayBuffer(1, 2, 3, 4, 5, 6)
Удаление элементов из ArrayBuffer
ArrayBuffer является изменяемым,
поэтому у него есть такие методы, как -=, --=, clear, remove и другие.
Примеры с -= и --=:
val a = ArrayBuffer.range('a', 'h') // ArrayBuffer(a, b, c, d, e, f, g)
a -= 'a' // ArrayBuffer(b, c, d, e, f, g)
a --= Seq('b', 'c') // ArrayBuffer(d, e, f, g)
a --= Set('d', 'e') // ArrayBuffer(f, g)
Обновление элементов в ArrayBuffer
Элементы в ArrayBuffer можно обновлять, либо переназначать:
val a = ArrayBuffer.range(1,5) // ArrayBuffer(1, 2, 3, 4)
a(2) = 50 // ArrayBuffer(1, 2, 50, 4)
a.update(0, 10) // ArrayBuffer(10, 2, 50, 4)
Maps
Map — это итерируемая коллекция, состоящая из пар ключей и значений.
В Scala есть как изменяемые, так и неизменяемые типы Map.
В этом разделе показано, как использовать неизменяемый Map.
Создание неизменяемой Map
Неизменяемая Map создается следующим образом:
val states = Map(
"AK" -> "Alaska",
"AL" -> "Alabama",
"AZ" -> "Arizona"
)
Перемещаться по элементам Map используя выражение for можно следующим образом:
for ((k, v) <- states) println(s"key: $k, value: $v")
for (k, v) <- states do println(s"key: $k, value: $v")
REPL показывает, как это работает:
scala> for ((k, v) <- states) println(s"key: $k, value: $v")
key: AK, value: Alaska
key: AL, value: Alabama
key: AZ, value: Arizona
scala> for (k, v) <- states do println(s"key: $k, value: $v")
key: AK, value: Alaska
key: AL, value: Alabama
key: AZ, value: Arizona
Доступ к элементам Map
Доступ к элементам Map осуществляется через указание в скобках значения ключа:
val ak = states("AK") // ak: String = Alaska
val al = states("AL") // al: String = Alabama
На практике также используются такие методы, как keys, keySet, keysIterator, for выражения
и функции высшего порядка, такие как map, для работы с ключами и значениями Map.
Добавление элемента в Map
При добавлении элементов в неизменяемую мапу с помощью + и ++, создается новая мапа:
val a = Map(1 -> "one") // a: Map(1 -> one)
val b = a + (2 -> "two") // b: Map(1 -> one, 2 -> two)
val c = b ++ Seq(
3 -> "three",
4 -> "four"
)
// c: Map(1 -> one, 2 -> two, 3 -> three, 4 -> four)
Удаление элементов из Map
Элементы удаляются с помощью методов - или --.
В случае неизменяемой Map создается новый экземпляр, который нужно присвоить новой переменной:
val a = Map(
1 -> "one",
2 -> "two",
3 -> "three",
4 -> "four"
)
val b = a - 4 // b: Map(1 -> one, 2 -> two, 3 -> three)
val c = a - 4 - 3 // c: Map(1 -> one, 2 -> two)
Обновление элементов в Map
Чтобы обновить элементы на неизменяемой Map, используется метод update (или оператор +):
val a = Map(
1 -> "one",
2 -> "two",
3 -> "three"
)
val b = a.updated(3, "THREE!") // b: Map(1 -> one, 2 -> two, 3 -> THREE!)
val c = a + (2 -> "TWO...") // c: Map(1 -> one, 2 -> TWO..., 3 -> three)
Перебор элементов в Map
Элементы в Map можно перебрать с помощью выражения for, как и для остальных коллекций:
val states = Map(
"AK" -> "Alaska",
"AL" -> "Alabama",
"AZ" -> "Arizona"
)
for ((k, v) <- states) println(s"key: $k, value: $v")
val states = Map(
"AK" -> "Alaska",
"AL" -> "Alabama",
"AZ" -> "Arizona"
)
for (k, v) <- states do println(s"key: $k, value: $v")
Существует много способов работы с ключами и значениями на Map.
Общие методы Map включают foreach, map, keys и values.
В Scala есть много других специализированных типов Map,
включая CollisionProofHashMap, HashMap, LinkedHashMap, ListMap, SortedMap, TreeMap, WeakHashMap и другие.
Работа с множествами
Множество (Set) - итерируемая коллекция без повторяющихся элементов.
В Scala есть как изменяемые, так и неизменяемые типы Set.
В этом разделе демонстрируется неизменяемое множество.
Создание множества
Создание нового пустого множества:
val nums = Set[Int]()
val letters = Set[Char]()
Создание множества с исходными данными:
val nums = Set(1, 2, 3, 3, 3) // Set(1, 2, 3)
val letters = Set('a', 'b', 'c', 'c') // Set('a', 'b', 'c')
Добавление элементов в множество
В неизменяемое множество новые элементы добавляются с помощью + и ++, результат присваивается новой переменной:
val a = Set(1, 2) // Set(1, 2)
val b = a + 3 // Set(1, 2, 3)
val c = b ++ Seq(4, 1, 5, 5) // HashSet(5, 1, 2, 3, 4)
Стоит отметить, что повторяющиеся элементы не добавляются в множество, а также, что порядок элементов произвольный.
Удаление элементов из множества
Элементы из множества удаляются с помощью методов - и --, результат также должен присваиваться новой переменной:
val a = Set(1, 2, 3, 4, 5) // HashSet(5, 1, 2, 3, 4)
val b = a - 5 // HashSet(1, 2, 3, 4)
val c = b -- Seq(3, 4) // HashSet(1, 2)
Диапазон (Range)
Range часто используется для заполнения структур данных и для for выражений.
Эти REPL примеры демонстрируют, как создавать диапазоны:
1 to 5 // Range(1, 2, 3, 4, 5)
1 until 5 // Range(1, 2, 3, 4)
1 to 10 by 2 // Range(1, 3, 5, 7, 9)
'a' to 'c' // NumericRange(a, b, c)
Range можно использовать для заполнения коллекций:
val x = (1 to 5).toList // List(1, 2, 3, 4, 5)
val x = (1 to 5).toBuffer // ArrayBuffer(1, 2, 3, 4, 5)
Они также используются в for выражениях:
scala> for (i <- 1 to 3) println(i)
1
2
3
scala> for i <- 1 to 3 do println(i)
1
2
3
Во многих коллекциях есть метод range:
Vector.range(1, 5) // Vector(1, 2, 3, 4)
List.range(1, 10, 2) // List(1, 3, 5, 7, 9)
Set.range(1, 10) // HashSet(5, 1, 6, 9, 2, 7, 3, 8, 4)
Диапазоны также полезны для создания тестовых коллекций:
val evens = (0 to 10 by 2).toList // List(0, 2, 4, 6, 8, 10)
val odds = (1 to 10 by 2).toList // List(1, 3, 5, 7, 9)
val doubles = (1 to 5).map(_ * 2.0) // Vector(2.0, 4.0, 6.0, 8.0, 10.0)
// Создание Map
val map = (1 to 3).map(e => (e,s"$e")).toMap
// map: Map[Int, String] = Map(1 -> "1", 2 -> "2", 3 -> "3")
Больше деталей
Если вам нужна дополнительная информация о специализированных коллекциях, см. следующие ресурсы:
Contributors to this page:
Contents
- Введение
- Возможности Scala
- Почему Scala 3?
- Почувствуй Scala
- Пример 'Hello, World!'
- REPL
- Переменные и типы данных
- Структуры управления
- Моделирование данных
- Методы
- Функции первого класса
- Одноэлементные объекты
- Коллекции
- Контекстные абстракции
- Верхнеуровневые определения
- Обзор
- Первый взгляд на типы
- Интерполяция строк
- Структуры управления
- Моделирование предметной области
- Инструменты
- Моделирование ООП
- Моделирование ФП
- Методы
- Особенности методов
- Main методы в Scala 3
- Обзор
- Функции
- Анонимные функции
- Параметры функции
- Eta расширение
- Функции высшего порядка
- Собственный map
- Создание метода, возвращающего функцию
- Обзор
- Пакеты и импорт
- Коллекции в Scala
- Типы коллекций
- Методы в коллекциях
- Обзор
- Функциональное программирование
- Что такое функциональное программирование?
- Неизменяемые значения
- Чистые функции
- Функции — это значения
- Функциональная обработка ошибок
- Обзор
- Типы и система типов
- Определение типов
- Параметризованные типы
- Пересечение типов
- Объединение типов
- Алгебраические типы данных
- Вариантность
- Непрозрачные типы
- Структурные типы
- Зависимые типы функций
- Другие типы
- Контекстные абстракции
- Методы расширения
- Параметры контекста
- Контекстные границы
- Given импорты
- Классы типов
- Многостороннее равенство
- Неявное преобразование типов
- Обзор
- Параллелизм
- Scala утилиты
- Сборка и тестирование проектов Scala с помощью Sbt
- Рабочие листы
- Взаимодействие с Java