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: