На этой странице показаны общие коллекции 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