Scala 3 — Book

Типы коллекций

Language

На этой странице показаны общие коллекции Scala 3 и сопутствующие им методы. Scala поставляется с большим количеством типов коллекций, на изучение которых может уйти время, поэтому желательно начать с нескольких из них, а затем использовать остальные по мере необходимости. Точно так же у каждого типа коллекции есть десятки методов, облегчающих разработку, поэтому лучше начать изучение лишь с небольшого количества.

В этом разделе представлены наиболее распространенные типы и методы коллекций, которые вам понадобятся для начала работы.

В конце этого раздела представлены дополнительные ссылки, для более глубокого изучения коллекций.

Три основные категории коллекций

Для коллекций Scala можно выделить три основные категории:

  • Последовательности (Sequences/Seq) представляют собой последовательный набор элементов и могут быть индексированными (как массив) или линейными (как связанный список)
  • Мапы (Maps) содержат набор пар ключ/значение, например Java Map, Python dictionary или Ruby Hash
  • Множества (Sets) — это неупорядоченный набор уникальных элементов

Все они являются базовыми типами и имеют подтипы подходящие под конкретные задачи, таких как параллелизм (concurrency), кэширование (caching) и потоковая передача (streaming). В дополнение к этим трем основным категориям существуют и другие полезные типы коллекций, включая диапазоны (ranges), стеки (stacks) и очереди (queues).

Иерархия коллекций

В качестве краткого обзора следующие три рисунка показывают иерархию классов и трейтов в коллекциях Scala.

На первом рисунке показаны типы коллекций в пакете scala.collection. Все это высокоуровневые абстрактные классы или трейты, которые обычно имеют неизменяемые и изменяемые реализации.

General collection hierarchy

На этом рисунке показаны все коллекции в пакете scala.collection.immutable:

Immutable collection hierarchy

А на этом рисунке показаны все коллекции в пакете scala.collection.mutable:

Mutable collection hierarchy

В следующих разделах представлены некоторые из распространенных типов.

Общие коллекции

Основные коллекции, используемые чаще всего:

Тип коллекции Неизменяемая Изменяемая Описание
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: