На этой странице показаны общие коллекции 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") Больше деталей
Если вам нужна дополнительная информация о специализированных коллекциях, см. следующие ресурсы: