Методы в коллекциях
Важным преимуществом коллекций 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)
// a: List[Int] = List(10, 20, 30, 40, 10)
a.distinct
// res0: List[Int] = List(10, 20, 30, 40)
a.drop(2)
// res1: List[Int] = List(30, 40, 10)
a.dropRight(2)
// res2: List[Int] = List(10, 20, 30)
a.head
// res3: Int = 10
a.headOption
// res4: Option[Int] = Some(value = 10)
a.init
// res5: List[Int] = List(10, 20, 30, 40)
a.intersect(List(19, 20, 21))
// res6: List[Int] = List(20)
a.last
// res7: Int = 10
a.lastOption
// res8: Option[Int] = Some(value = 10)
a.slice(2, 4)
// res9: List[Int] = List(30, 40)
a.tail
// res10: List[Int] = List(20, 30, 40, 10)
a.take(3)
// res11: List[Int] = List(10, 20, 30)
a.takeRight(2)
// res12: List[Int] = List(40, 10)
Функции высшего порядка и лямбда-выражения
Далее будут показаны некоторые часто используемые функции высшего порядка (HOF), которые принимают лямбды (анонимные функции). Для начала приведем несколько вариантов лямбда-синтаксиса, начиная с самой длинной формы, поэтапно переходящей к наиболее сжатой:
a.filter((i: Int) => i < 25)
// res13: List[Int] = List(10, 20, 10)
a.filter((i) => i < 25)
// res14: List[Int] = List(10, 20, 10)
a.filter(i => i < 25)
// res15: List[Int] = List(10, 20, 10)
a.filter(_ < 25)
// res16: List[Int] = List(10, 20, 10)
В этих примерах:
- Первый пример показывает самую длинную форму. Такое многословие требуется редко, только в самых сложных случаях.
- Компилятор знает, что
a
содержитInt
, поэтому нет необходимости повторять это в функции. - Если в функции только один параметр, например
i
, то скобки не нужны. - В случае одного параметра, если он появляется в анонимной функции только раз, его можно заменить на
_
.
В главе Анонимные функции представлена более подробная информация и примеры правил, связанных с сокращением лямбда-выражений.
Примеры других HOF, использующих краткий лямбда-синтаксис:
a.dropWhile(_ < 25)
// res17: List[Int] = List(30, 40, 10)
a.filter(_ > 35)
// res18: List[Int] = List(40)
a.filterNot(_ < 25)
// res19: List[Int] = List(30, 40)
a.find(_ > 20)
// res20: Option[Int] = Some(value = 30)
a.takeWhile(_ < 30)
// res21: List[Int] = List(10, 20)
Важно отметить, что HOF также принимают в качестве параметров методы и функции, а не только лямбда-выражения.
Вот несколько примеров, в которых используется метод с именем double
.
Снова показаны несколько вариантов лямбда-выражений:
def double(i: Int) = i * 2
a.map(i => double(i))
// res22: List[Int] = List(20, 40, 60, 80, 20)
a.map(double(_))
// res23: List[Int] = List(20, 40, 60, 80, 20)
a.map(double)
// res24: List[Int] = List(20, 40, 60, 80, 20)
В последнем примере, когда анонимная функция состоит из одного вызова функции, принимающей один аргумент,
нет необходимости указывать имя аргумента, поэтому даже _
не требуется.
Наконец, HOF можно комбинировать:
a.filter(_ < 40)
.takeWhile(_ < 30)
.map(_ * 10)
// res25: List[Int] = List(100, 200)
P.S. Пример призван показать только то, как принято последовательно вызывать функции на неизменяемых коллекциях. Его недостаток в том, что обход коллекции происходит целых три раза.
Пример данных
В следующих разделах используются такие списки:
val oneToTen = (1 to 10).toList
// oneToTen: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val names = List("adam", "brandy", "chris", "david")
// names: List[String] = List("adam", "brandy", "chris", "david")
map
Метод map
проходит через каждый элемент в списке, применяя переданную функцию к элементу,
по одному за раз; затем возвращается новый список с измененными элементами.
Вот пример применения метода map
к списку oneToTen
:
val doubles = oneToTen.map(_ * 2)
Также можно писать анонимные функции, используя более длинную форму, например:
val doubles = oneToTen.map(i => i * 2)
Однако в этом документе будет всегда использоваться первая, более короткая форма.
Вот еще несколько примеров применения метода map
к oneToTen
и names
:
val capNames = names.map(_.capitalize)
// capNames: List[String] = List("Adam", "Brandy", "Chris", "David")
val nameLengthsMap = names.map(s => (s, s.length)).toMap
// nameLengthsMap: Map[String, Int] = Map(
// "adam" -> 4,
// "brandy" -> 6,
// "chris" -> 5,
// "david" -> 5
// )
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
).
Вот несколько примеров:
val lessThanFive = oneToTen.filter(_ < 5)
// lessThanFive: List[Int] = List(1, 2, 3, 4)
val evens = oneToTen.filter(_ % 2 == 0)
// evens: List[Int] = List(2, 4, 6, 8, 10)
val shortNames = names.filter(_.length <= 4)
// shortNames: List[String] = List("adam")
Отличительной особенностью функциональных методов коллекций является то,
что их можно объединять вместе для решения задач.
Например, в этом примере показано, как связать filter
и map
:
oneToTen.filter(_ < 4).map(_ * 10)
// res27: List[Int] = List(10, 20, 30)
Еслиfilter
используется передmap
,flatMap
илиforeach
, то для лучшей производительности вместо него должен использоватьсяwithFilter
, например,oneToTen.withFilter(_ < 4).map(_ * 10)
foreach
Метод foreach
используется для перебора всех элементов коллекции.
Стоит обратить внимание, что foreach
используется для побочных эффектов, таких как печать информации.
Вот пример с names
:
names.foreach(println)
// adam
// brandy
// chris
// david
head
Метод head
взят из Lisp и других более ранних языков функционального программирования.
Он используется для доступа к первому элементу (головному (head) элементу) списка:
oneToTen.head
// res29: Int = 1
names.head
// res30: String = "adam"
String
можно рассматривать как последовательность символов,
т.е. строка также является коллекцией, а значит содержит соответствующие методы.
Вот как head
работает со строками:
"foo".head
// res31: Char = 'f'
"bar".head
// res32: Char = 'b'
На пустой коллекции head
выдает исключение:
val emptyList = List[Int]()
emptyList.head
// java.util.NoSuchElementException: head of empty list
// at scala.collection.immutable.Nil$.head(List.scala:662)
Чтобы не натыкаться на исключение вместо head
желательно использовать headOption
,
особенно при разработке в функциональном стиле:
emptyList.headOption
// res33: Option[Int] = None
headOption
не генерирует исключение, а возвращает тип Option
со значением None
.
Более подробно о функциональном стиле программирования будет рассказано
в соответствующей главе.
tail
Метод tail
также взят из Lisp и используется для вывода всех элементов в списке после head
.
oneToTen.head
// res34: Int = 1
oneToTen.tail
// res35: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)
names.head
// res36: String = "adam"
names.tail
// res37: List[String] = List("brandy", "chris", "david")
Так же, как и head
, tail
можно использовать со строками:
"foo".tail
// res38: String = "oo"
"bar".tail
// res39: String = "ar"
tail
выбрасывает исключение java.lang.UnsupportedOperationException
, если список пуст,
поэтому, как и в случае с head
и headOption
, существует также метод tailOption
,
который предпочтительнее в функциональном программировании.
Список матчится, поэтому можно использовать такие выражения:
val x :: xs = names
// x: String = "adam"
// xs: List[String] = List("brandy", "chris", "david")
x
- это head
списка, а xs
- tail
.
Подобный pattern matching полезен во многих случаях, например, при написании метода суммирования с использованием рекурсии:
def sum(list: List[Int]): Int = list match
case Nil => 0
case x :: xs => x + sum(xs)
take, takeRight, takeWhile
Методы take
, takeRight
и takeWhile
предоставляют удобный способ "брать" (take) элементы из списка
для создания нового. Примеры take
и takeRight
:
oneToTen.take(1)
// res40: List[Int] = List(1)
oneToTen.take(2)
// res41: List[Int] = List(1, 2)
oneToTen.takeRight(1)
// res42: List[Int] = List(10)
oneToTen.takeRight(2)
// res43: List[Int] = List(9, 10)
Обратите внимание, как эти методы работают с «пограничными» случаями, когда запрашивается больше элементов, чем есть в последовательности, или запрашивается ноль элементов:
oneToTen.take(Int.MaxValue)
// res44: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.takeRight(Int.MaxValue)
// res45: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.take(0)
// res46: List[Int] = List()
oneToTen.takeRight(0)
// res47: List[Int] = List()
А это takeWhile
, который работает с функцией-предикатом:
oneToTen.takeWhile(_ < 5)
// res48: List[Int] = List(1, 2, 3, 4)
names.takeWhile(_.length < 5)
// res49: List[String] = List("adam")
drop, dropRight, dropWhile
drop
, dropRight
и dropWhile
удаляют элементы из списка и, по сути, противоположны своим аналогам "take".
Вот некоторые примеры:
oneToTen.drop(1)
// res50: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.drop(5)
// res51: List[Int] = List(6, 7, 8, 9, 10)
oneToTen.dropRight(8)
// res52: List[Int] = List(1, 2)
oneToTen.dropRight(7)
// res53: List[Int] = List(1, 2, 3)
Пограничные случаи:
oneToTen.drop(Int.MaxValue)
// res54: List[Int] = List()
oneToTen.dropRight(Int.MaxValue)
// res55: List[Int] = List()
oneToTen.drop(0)
// res56: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.dropRight(0)
// res57: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
А это dropWhile
, который работает с функцией-предикатом:
oneToTen.dropWhile(_ < 5)
// res58: List[Int] = List(5, 6, 7, 8, 9, 10)
names.dropWhile(_ != "chris")
// res59: List[String] = 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
Рассмотрим список:
val a = List(1,2,3,4)
вот что происходит, когда в reduce
передается метод add
:
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
// res60: Int = 10
Как видно из результата, функция reduce
использует add
для сокращения списка a
до единственного значения,
в данном случае — суммы всех чисел в списке.
reduce
можно использовать с анонимными функциями:
a.reduce(_ + _)
// res61: Int = 10
Аналогично можно использовать другие функции, например, умножение:
a.reduce(_ * _)
// res62: Int = 24
Дальнейшее изучение коллекций
В коллекциях Scala есть десятки дополнительных методов, которые избавляют от необходимости писать еще один цикл for
.
Более подробную информацию о коллекциях Scala см. в разделе
Изменяемые и неизменяемые коллекции
и Архитектура коллекций Scala.
А также в API.
В качестве последнего примечания, при использовании Java-кода в проекте Scala,
коллекции Java можно преобразовать в коллекции Scala.
После этого, их можно использовать в выражениях for
,
а также воспользоваться преимуществами методов функциональных коллекций Scala.
Более подробную информацию можно найти в разделе Взаимодействие с Java.
Ссылки:
- Scala3 book
- Scala3 book, Collections Methods
- Изменяемые и неизменяемые коллекции
- Архитектура коллекций Scala
- collections API
- SKB – Scala flatMap
- SKB – Scala foldLeft
- SKB – Scala List Filter Method
- SKB – Scala List Flatten
- SKB – Scala List of Option flatten
- SKB – Scala List parallel
- SKB – Scala List zip
- SKB – Scala Map for List