Важным преимуществом коллекций Scala является то, что они поставляются с десятками методов “из коробки”,
которые доступны как для неизменяемых, так и для изменяемых типов коллекций.
Больше нет необходимости писать пользовательские циклы for
каждый раз, когда нужно работать с коллекцией.
При переходе от одного проекта к другому, можно обнаружить, что используются одни и те же методы.
В коллекциях доступны десятки методов, поэтому здесь показаны не все из них. Показаны только некоторые из наиболее часто используемых методов, в том числе:
map
filter
foreach
head
tail
take
,takeWhile
drop
,dropWhile
reduce
Следующие методы работают со всеми типами последовательностей, включая List
, Vector
, ArrayBuffer
и т.д.
Примеры рассмотрены на List
-е, если не указано иное.
Важно напомнить, что ни один из методов в
List
не изменяет список. Все они работают в функциональном стиле, то есть возвращают новую коллекцию с измененными результатами.
Примеры распространенных методов
Для общего представления в примерах ниже показаны некоторые из наиболее часто используемых методов коллекций. Вот несколько методов, которые не используют лямбда-выражения:
val a = List(10, 20, 30, 40, 10) // List(10, 20, 30, 40, 10)
a.distinct // List(10, 20, 30, 40)
a.drop(2) // List(30, 40, 10)
a.dropRight(2) // List(10, 20, 30)
a.head // 10
a.headOption // Some(10)
a.init // List(10, 20, 30, 40)
a.intersect(List(19,20,21)) // List(20)
a.last // 10
a.lastOption // Some(10)
a.slice(2,4) // List(30, 40)
a.tail // List(20, 30, 40, 10)
a.take(3) // List(10, 20, 30)
a.takeRight(2) // List(40, 10)
Функции высшего порядка и лямбда-выражения
Далее будут показаны некоторые часто используемые функции высшего порядка (HOF), которые принимают лямбды (анонимные функции). Для начала приведем несколько вариантов лямбда-синтаксиса, начиная с самой длинной формы, поэтапно переходящей к наиболее сжатой:
// все эти функции одинаковые и возвращают
// одно и тоже: List(10, 20, 10)
a.filter((i: Int) => i < 25) // 1. наиболее расширенная форма
a.filter((i) => i < 25) // 2. `Int` необязателен
a.filter(i => i < 25) // 3. скобки можно опустить
a.filter(_ < 25) // 4. `i` необязателен
В этих примерах:
- Первый пример показывает самую длинную форму. Такое многословие требуется редко, только в самых сложных случаях.
- Компилятор знает, что
a
содержитInt
, поэтому нет необходимости повторять это в функции. - Если в функции только один параметр, например
i
, то скобки не нужны. - В случае одного параметра, если он появляется в анонимной функции только раз, его можно заменить на
_
.
В главе Анонимные функции представлена более подробная информация и примеры правил, связанных с сокращением лямбда-выражений.
Примеры других HOF, использующих краткий лямбда-синтаксис:
a.dropWhile(_ < 25) // List(30, 40, 10)
a.filter(_ > 100) // List()
a.filterNot(_ < 25) // List(30, 40)
a.find(_ > 20) // Some(30)
a.takeWhile(_ < 30) // List(10, 20)
Важно отметить, что HOF также принимают в качестве параметров методы и функции, а не только лямбда-выражения.
Вот несколько примеров, в которых используется метод с именем double
.
Снова показаны несколько вариантов лямбда-выражений:
def double(i: Int) = i * 2
// these all return `List(20, 40, 60, 80, 20)`
a.map(i => double(i))
a.map(double(_))
a.map(double)
В последнем примере, когда анонимная функция состоит из одного вызова функции, принимающей один аргумент,
нет необходимости указывать имя аргумента, поэтому даже _
не требуется.
Наконец, HOF можно комбинировать:
// выдает `List(100, 200)`
a.filter(_ < 40)
.takeWhile(_ < 30)
.map(_ * 10)
Пример данных
В следующих разделах используются такие списки:
val oneToTen = (1 to 10).toList
val names = List("adam", "brandy", "chris", "david")
map
Метод map
проходит через каждый элемент в списке, применяя переданную функцию к элементу, по одному за раз;
затем возвращается новый список с измененными элементами.
Вот пример применения метода map
к списку oneToTen
:
scala> val doubles = oneToTen.map(_ * 2)
doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
Также можно писать анонимные функции, используя более длинную форму, например:
scala> val doubles = oneToTen.map(i => i * 2)
doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
Однако в этом документе будет всегда использоваться первая, более короткая форма.
Вот еще несколько примеров применения метода map
к oneToTen
и names
:
scala> val capNames = names.map(_.capitalize)
capNames: List[String] = List(Adam, Brandy, Chris, David)
scala> val nameLengthsMap = names.map(s => (s, s.length)).toMap
nameLengthsMap: Map[String, Int] = Map(adam -> 4, brandy -> 6, chris -> 5, david -> 5)
scala> val isLessThanFive = oneToTen.map(_ < 5)
isLessThanFive: List[Boolean] = List(true, true, true, true, false, false, false, false, false, false)
Как показано в последних двух примерах, совершенно законно (и распространено) использование map
для возврата коллекции,
которая имеет тип, отличный от исходного типа.
filter
Метод filter
создает новый список, содержащий только те элементы, которые удовлетворяют предоставленному предикату.
Предикат или условие — это функция, которая возвращает Boolean
(true
или false
).
Вот несколько примеров:
scala> val lessThanFive = oneToTen.filter(_ < 5)
lessThanFive: List[Int] = List(1, 2, 3, 4)
scala> val evens = oneToTen.filter(_ % 2 == 0)
evens: List[Int] = List(2, 4, 6, 8, 10)
scala> val shortNames = names.filter(_.length <= 4)
shortNames: List[String] = List(adam)
Отличительной особенностью функциональных методов коллекций является то,
что их можно объединять вместе для решения задач.
Например, в этом примере показано, как связать filter
и map
:
oneToTen.filter(_ < 4).map(_ * 10)
REPL показывает результат:
scala> oneToTen.filter(_ < 4).map(_ * 10)
val res1: List[Int] = List(10, 20, 30)
foreach
Метод foreach
используется для перебора всех элементов коллекции.
Стоит обратить внимание, что foreach
используется для побочных эффектов, таких как печать информации.
Вот пример с names
:
scala> names.foreach(println)
adam
brandy
chris
david
head
Метод head
взят из Lisp и других более ранних языков функционального программирования.
Он используется для доступа к первому элементу (головному (head) элементу) списка:
oneToTen.head // 1
names.head // adam
String
можно рассматривать как последовательность символов, т.е. строка также является коллекцией,
а значит содержит соответствующие методы.
Вот как head
работает со строками:
"foo".head // 'f'
"bar".head // 'b'
head
— отличный метод для работы, но в качестве предостережения следует помнить, что
он также может генерировать исключение при вызове для пустой коллекции:
val emptyList = List[Int]() // emptyList: List[Int] = List()
emptyList.head // java.util.NoSuchElementException: head of empty list
Чтобы не натыкаться на исключение вместо head
желательно использовать headOption
,
особенно при разработке в функциональном стиле:
emptyList.headOption // None
headOption
не генерирует исключение, а возвращает тип Option
со значением None
.
Более подробно о функциональном стиле программирования будет рассказано в соответствующей главе.
tail
Метод tail
также взят из Lisp и используется для вывода всех элементов в списке после head
.
oneToTen.head // 1
oneToTen.tail // List(2, 3, 4, 5, 6, 7, 8, 9, 10)
names.head // adam
names.tail // List(brandy, chris, david)
Так же, как и head
, tail
можно использовать со строками:
"foo".tail // "oo"
"bar".tail // "ar"
tail
выбрасывает исключение java.lang.UnsupportedOperationException, если список пуст,
поэтому, как и в случае с head
и headOption
, существует также метод tailOption
,
который предпочтительнее в функциональном программировании.
Список матчится, поэтому можно использовать такие выражения:
val x :: xs = names
Помещение этого кода в REPL показывает, что x
назначается заглавному элементу списка, а xs
назначается “хвосту”:
scala> val x :: xs = names
val x: String = adam
val xs: List[String] = List(brandy, chris, david)
Подобное сопоставление с образцом полезно во многих случаях, например, при написании метода sum
с использованием рекурсии:
def sum(list: List[Int]): Int = list match {
case Nil => 0
case x :: xs => x + sum(xs)
}
def sum(list: List[Int]): Int = list match
case Nil => 0
case x :: xs => x + sum(xs)
take
, takeRight
, takeWhile
Методы take
, takeRight
и takeWhile
предоставляют удобный способ “брать” (taking) элементы из списка для создания нового.
Примеры take
и takeRight
:
oneToTen.take(1) // List(1)
oneToTen.take(2) // List(1, 2)
oneToTen.takeRight(1) // List(10)
oneToTen.takeRight(2) // List(9, 10)
Обратите внимание, как эти методы работают с «пограничными» случаями, когда запрашивается больше элементов, чем есть в последовательности, или запрашивается ноль элементов:
oneToTen.take(Int.MaxValue) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.takeRight(Int.MaxValue) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.take(0) // List()
oneToTen.takeRight(0) // List()
А это takeWhile
, который работает с функцией-предикатом:
oneToTen.takeWhile(_ < 5) // List(1, 2, 3, 4)
names.takeWhile(_.length < 5) // List(adam)
drop
, dropRight
, dropWhile
drop
, dropRight
и dropWhile
удаляют элементы из списка
и, по сути, противоположны своим аналогам “take”.
Вот некоторые примеры:
oneToTen.drop(1) // List(2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.drop(5) // List(6, 7, 8, 9, 10)
oneToTen.dropRight(8) // List(1, 2)
oneToTen.dropRight(7) // List(1, 2, 3)
Пограничные случаи:
oneToTen.drop(Int.MaxValue) // List()
oneToTen.dropRight(Int.MaxValue) // List()
oneToTen.drop(0) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.dropRight(0) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
А это dropWhile
, который работает с функцией-предикатом:
oneToTen.dropWhile(_ < 5) // List(5, 6, 7, 8, 9, 10)
names.dropWhile(_ != "chris") // List(chris, david)
reduce
Метод reduce
позволяет свертывать коллекцию до одного агрегируемого значения.
Он принимает функцию (или анонимную функцию) и последовательно применяет эту функцию к элементам в списке.
Лучший способ объяснить reduce
— создать небольшой вспомогательный метод.
Например, метод add
, который складывает вместе два целых числа,
а также предоставляет хороший вывод отладочной информации:
def add(x: Int, y: Int): Int = {
val theSum = x + y
println(s"received $x and $y, their sum is $theSum")
theSum
}
def add(x: Int, y: Int): Int =
val theSum = x + y
println(s"received $x and $y, their sum is $theSum")
theSum
Рассмотрим список:
val a = List(1,2,3,4)
вот что происходит, когда в reduce
передается метод add
:
scala> a.reduce(add)
received 1 and 2, their sum is 3
received 3 and 3, their sum is 6
received 6 and 4, their sum is 10
res0: Int = 10
Как видно из результата, функция reduce
использует add
для сокращения списка a
до единственного значения,
в данном случае — суммы всех чисел в списке.
reduce
можно использовать с анонимными функциями:
scala> a.reduce(_ + _)
res0: Int = 10
Аналогично можно использовать другие функции, например, умножение:
scala> a.reduce(_ * _)
res1: Int = 24
Важная концепция, которую следует знать о
reduce
, заключается в том, что, как следует из ее названия (reduce - сокращать), она используется для сокращения коллекции до одного значения.
Дальнейшее изучение коллекций
В коллекциях Scala есть десятки дополнительных методов, которые избавляют от необходимости писать еще один цикл for
.
Более подробную информацию о коллекциях Scala см.
в разделе Изменяемые и неизменяемые коллекции
и Архитектура коллекций Scala.
В качестве последнего примечания, при использовании Java-кода в проекте Scala, коллекции Java можно преобразовать в коллекции Scala. После этого, их можно использовать в выражениях
for
, а также воспользоваться преимуществами методов функциональных коллекций Scala. Более подробную информацию можно найти в разделе Взаимодействие с Java.
Contributors to this page:
Contents
- Введение
- Возможности Scala
- Почему Scala 3?
- Почувствуй Scala
- Пример 'Hello, World!'
- REPL
- Переменные и типы данных
- Структуры управления
- Моделирование данных
- Методы
- Функции первого класса
- Одноэлементные объекты
- Коллекции
- Контекстные абстракции
- Верхнеуровневые определения
- Обзор
- Первый взгляд на типы
- Интерполяция строк
- Структуры управления
- Моделирование предметной области
- Инструменты
- Моделирование ООП
- Моделирование ФП
- Методы
- Особенности методов
- Main методы в Scala 3
- Обзор
- Функции
- Анонимные функции
- Параметры функции
- Eta расширение
- Функции высшего порядка
- Собственный map
- Создание метода, возвращающего функцию
- Обзор
- Пакеты и импорт
- Коллекции в Scala
- Типы коллекций
- Методы в коллекциях
- Обзор
- Функциональное программирование
- Что такое функциональное программирование?
- Неизменяемые значения
- Чистые функции
- Функции — это значения
- Функциональная обработка ошибок
- Обзор
- Типы и система типов
- Определение типов
- Параметризованные типы
- Пересечение типов
- Объединение типов
- Алгебраические типы данных
- Вариантность
- Непрозрачные типы
- Структурные типы
- Зависимые типы функций
- Другие типы
- Контекстные абстракции
- Методы расширения
- Параметры контекста
- Контекстные границы
- Given импорты
- Классы типов
- Многостороннее равенство
- Неявное преобразование типов
- Обзор
- Параллелизм
- Scala утилиты
- Сборка и тестирование проектов Scala с помощью Sbt
- Рабочие листы
- Взаимодействие с Java