Stream API настолько большая тема, что приходится разбивать её на несколько статей. В первой части были примеры решения классических проблем при работе с коллекциями, а во второй я покажу использование редуцирующих функций.
Map/Reduce
Map/Reduce это в общем-то модный buzzword. На самом деле Map/Reduce это очень простой шаблон проектирования, описывающий работу с наборами данных в два шага: на первом шаге выполняются (параллельные) операции над набором, на втором шаге результаты первого шага объединяются.
1
2
3
4
5
6
7
8
|
public Double averageAge() {
Integer currentYear = Calendar.getInstance().get(Calendar.YEAR);
return data
.stream()
.mapToInt(u -> currentYear - u.getYob())
.average()
.getAsDouble();
}
|
Интерфейс Stream определяет методы mapTo*(), которые возвращают особые реализации Stream, имеющие методы average(), sum(), min(), max(), которые выполняют соответствующие арифметические действия над элементами набора данных.
В случае, когда нужно обработать поток, к которому операции mapTo*() неприменимы, например из-за типа данных, можно использовать функцию reduce, которая принимает два параметра: начальное значение, оно же значение по умолчанию, и лямбда-выражение с двумя аргументами: первый хранит результат предыдущего вычисления, второй — текущее значение. Пример — вычисление списка уникальных символов в логинах пользователей:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
static String removeDuplicates(String s) {
StringBuilder noDupes = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
String si = s.substring(i, i + 1);
if (noDupes.indexOf(si) == -1) {
noDupes.append(si);
}
}
return noDupes.toString();
}
public String getCommonSymbols() {
return data
.stream()
.map(User::getLogin)
.reduce("", (p,c) -> removeDuplicates(p+c));
}
|
Коллекторы
Метод collect() дополняет метод reduce(). Он так же является редуцирующим методом, но, в отличие от reduce(), в ходе работы он обновляет значение результата, а не заменяет его.
Обычно метод collect() используется с уже существующими коллекторами из класса java.util.stream.Collectors, который предоставляет множество стандартных операций.
С один типом коллекторов мы сталкивались раньше, в примере Hello,Streams: объединение потока строк в строку:
1
2
3
4
5
6
|
public String logins() {
return data
.stream()
.map(User::getLogin)
.collect(Collectors.joining(",", "User names: '", "'"));
}
|
Коллектор joining(delimiter, prefix, suffix) объединяет строки в одну, разделяя их разделителем, результирующая строка будет начинаться с префикса и заканчиваться суффиксом. Существует две упрощённых версии этого коллектора: одна принимает только разделитель, другая не принимает ничего, склеивая строки одна с другой.
Второй и, наверное, самый частой используемый тип коллекторов, это коллекторы в коллекции: Collectors.toCollection(), Collectors.toList(), Collectors.toSet() Первый принимает аргументом лямбда-выражение, возвращающее Collection, остальные два создают контейнеры для данных сами.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public Collection<String> loginsCollections() {
return data
.stream()
.map(User::getLogin)
.collect(Collectors.toCollection(ArrayList::new));
}
public Set<String> loginsSet() {
return data
.stream()
.map(User::getLogin)
.collect(Collectors.toSet());
}
|
Очень похожи на них коллекторы работающие с Map. Проблема с Map в том, что один элемент в ней состоит из двух значений, в то время как в Stream каждый элемент есть одно значение. Поэтому методам toMap() необходимо передавать два лямбда-выражения: одно для создания значения в Map из текущего элемента Stream, другое для создания ключа из него же.
1
2
3
4
5
|
public Map<Integer, String> yobLoginsMap() {
return data
.stream()
.collect(Collectors.toMap(User::getYob, User::getLogin));
}
|
Особенностью toMap() является нетерпимость к повторениям значений ключей. Если при сборке потока в Map бует обнаружено дублирующееся значение ключа, будет выброшено IllegalStateException. А если гарантировать это условие не получается, необходимо использовать расширенный вариант toMap(), в который передаётся дополнительная функция. Эта функция будет вызвана для всех значений с одинаковым ключом и должна их обработать и вернуть единственно верное значение для ключа. Например для примера с возрастами логинов можно объединять логины с одним возрастом:
1
2
3
4
5
6
7
8
|
public Map<Integer, String> yobLoginsMultiKeys() {
Collection<User> local = new ArrayList<>(data);
local.add(new User("OLD", 2, 1970));
local.add(new User("YOUNG", 1, 1990));
return local
.stream()
.collect(Collectors.toMap(User::getYob, User::getLogin, (s, a) -> s + ", " + a));
}
|
1
2
3
4
5
|
@Test
public void testYobMultiLogin() {
assertThat(testedObject.yobLoginsMultiKeys().get(1970), is("LOGIN, OLD"));
assertThat(testedObject.yobLoginsMultiKeys().get(1990), is("EXAMPLE, YOUNG"));
}
|
Третий вариант toMap() принимает ещё один параметр: функцию, возвращающий пустой экземпляр Map, позволяя тем самым указывать конкретный тип контейнера.
К трём методам toMap() есть три дополняющих методов toConcurrentMap(), принимающих теже самые аргументы, но работающие с потокобезопасной ConcurrentMap и реализующие потокобезопасный Collector, что позволяет их использовать с параллельной обработкой потоков, про которую я расскажу в следующей части.
Код примера доступен на github.