102.10K
Категория: ПрограммированиеПрограммирование

Java Lambda, Streams API

1.

Java Lambda, Streams API

2.

Среди новшеств, которые были привнесены в язык Java с выходом
JDK 8, особняком стоят лямбда-выражения. Лямбда представляет
набор инструкций, которые можно выделить в отдельную
переменную и затем многократно вызвать в различных местах
программы.
Основу лямбда-выражения составляет лямбда-оператор, который
представляет стрелку ->. Этот оператор разделяет лямбдавыражение на две части: левая часть содержит список параметров
выражения, а правая собственно представляет тело лямбдавыражения, где выполняются все действия.
Лямбда-выражение не выполняется само по себе, а образует
реализацию метода, определенного в функциональном
интерфейсе. При этом важно, что функциональный интерфейс
должен содержать только один единственный метод без
реализации.

3.

Выражение n -> n + 1, это просто аналог выражения
Integer func(Integer n) {
return n+1;
},
а выражение () -> «a1» аналог выражения
String func() {
return «a1»;
}

4.

Stream
Stream — это объект для универсальной работы с данными. Мы указываем, какие операции хотим провести, при этом не заботясь о деталях
реализации. Например, взять элементы из списка сотрудников, выбрать тех, кто младше 40 лет, отсортировать по фамилии и
поместить в новый список. Или чуть сложнее, прочитать все json-файлы, находящиеся в папке books, десериализировать в список
объектов книг, обработать элементы всех этих списков, а затем сгруппировать книги по автору.
Данные могут быть получены из источников, коими являются коллекции или методы, поставляющие данные. Например, список файлов, массив
строк, метод range() для числовых промежутков и т.д. То есть, стрим использует существующие коллекции для получения новых элементов,
это ни в коем случае не новая структура данных.
К данным затем применяются операторы. Например, взять лишь некоторые элементы (filter), преобразовать каждый элемент (map), посчитать
сумму элементов или объединить всё в один объект (reduce).
Операторы можно разделить на две группы:
- Промежуточные (intermediate) — обрабатывают поступающие элементы и возвращают стрим. Промежуточных операторов в
цепочке обработки элементов может быть много.
- Терминальные (terminal) — обрабатывают элементы и завершают работу стрима, так что терминальный оператор в цепочке
может быть только один.

5.

Способ создания стрима
Шаблон создания
Пример
1. Классический: Создание
стрима из коллекции
collection.stream()
Collection<String> collection =
Arrays.asList("a1", "a2", "a3");
Stream<String>
streamFromCollection =
collection.stream();
2. Создание стрима из значений
Stream.of(значение1,…
значениеN)
Stream<String> streamFromValues
= Stream.of("a1", "a2", "a3");
3. Создание стрима из массива
Arrays.stream(массив)
String[] array = {"a1","a2","a3"};
Stream<String> streamFromArrays
= Arrays.stream(array);
4. Создание стрима из файла
(каждая строка в файле будет
отдельным элементом в стриме)
Files.lines(путь_к_файлу)
Stream<String> streamFromFiles =
Files.lines(Paths.get("file.txt"))
5. Создание стрима из строки
«строка».chars()
IntStream streamFromString =
"123".chars()

6.

6. С помощью Stream.builder
Stream.builder().add(...)....build(
)
Stream.builder().add("a1").add("a
2").add("a3").build()
7. Создание параллельного
стрима
collection.parallelStream()
Stream<String> stream =
collection.parallelStream();
8. Создание бесконечных
стрима с помощью
Stream.iterate
Stream.iterate(начальное_услов Stream<Integer>
ие, выражение_генерации)
streamFromIterate =
Stream.iterate(1, n -> n + 1)
9. Создание бесконечных
стрима с помощью
Stream.generate
Stream.generate(выражение_ге
нерации)
Stream<String>
streamFromGenerate =
Stream.generate(() -> "a1")

7.

Конвейерные
Метод
stream
Описание
Пример
filter
Отфильтровывает записи, возвращает только записи,
соответствующие условию
collection.stream().filter(«a1»::e
quals).count()
skip
Позволяет пропустить N первых элементов
collection.stream().skip(collectio
n.size() —
1).findFirst().orElse(«1»)
distinct
Возвращает стрим без дубликатов (для метода equals)
collection.stream().distinct().coll
ect(Collectors.toList())
map
Преобразует каждый элемент стрима
collection.stream().map((s) -> s
+
"_1").collect(Collectors.toList())
peek
Возвращает тот же стрим, но применяет функцию к
каждому элементу стрима
collection.stream().map(String::t
oUpperCase).peek((e) ->
System.out.print("," + e)).
collect(Collectors.toList())

8.

limit
Позволяет ограничить выборку
определенным количеством
первых элементов
collection.stream().limit(2).collect(
Collectors.toList())
sorted
Позволяет сортировать
значения либо в натуральном
порядке, либо задавая
Comparator
collection.stream().sorted().collect
(Collectors.toList())
mapToInt,
mapToDouble,
mapToLong
Аналог map, но возвращает
числовой стрим (то есть стрим
из числовых примитивов)
collection.stream().mapToInt((s) > Integer.parseInt(s)).toArray()
flatMap,
flatMapToInt,
flatMapToDouble,
flatMapToLong
Похоже на map, но может
создавать из одного элемента
несколько

9.

Терминальны
е
Метод
stream
Описание
Пример
findFirst
Возвращает первый элемент из стрима (возвращает collection.stream().findFirst().orE
Optional)
lse(«1»)
findAny
Возвращает любой подходящий элемент из стрима
(возвращает Optional)
collection.stream().findAny().orEl
se(«1»)
collect
Представление результатов в виде коллекций и
других структур данных
collection.stream().filter((s) ->
s.contains(«1»)).collect(Collector
s.toList())
count
Возвращает количество элементов в стриме
collection.stream().filter(«a1»::eq
uals).count()
anyMatch
Возвращает true, если условие выполняется хотя
бы для одного элемента
collection.stream().anyMatch(«a
1»::equals)
noneMatch
Возвращает true, если условие не выполняется ни
для одного элемента
collection.stream().noneMatch(«
a8»::equals)

10.

allMatch
Возвращает true, если условие выполняется
для всех элементов
collection.stream().allMatch((s) ->
s.contains(«1»))
min
Возвращает минимальный элемент, в
качестве условия использует компаратор
collection.stream().min(String::compareTo).get
()
max
Возвращает максимальный элемент, в
качестве условия использует компаратор
collection.stream().max(String::compareTo).ge
t()
forEach
Применяет функцию к каждому объекту
стрима, порядок при параллельном
выполнении не гарантируется
set.stream().forEach((p) -> p.append("_1"));
forEachOrdered
Применяет функцию к каждому объекту
стрима, сохранение порядка элементов
гарантирует
list.stream().forEachOrdered((p) ->
p.append("_new"));
toArray
Возвращает массив значений стрима
collection.stream().map(String::toUpperCase).t
oArray(String[]::new);
reduce
Позволяет выполнять агрегатные функции на
всей коллекцией и возвращать один
результат
collection.stream().reduce((s1, s2) -> s1 +
s2).orElse(0)

11.

В чем разница между методами map и flatMap
И map и flatMap могут быть применены к стриму Stream<T> и оба возвращают стрим Stream<R>. Разница заключается в том, что операция map создает одно
выходное значение для каждого входного значения, тогда как операция flatMap создает произвольное число(ноль или больше) значений для каждого
входного значения.
Операция map в качестве аргумента принимает Function(например, лямбду), которая вызывается для каждого значения входного стрима(который <T>),
преобразует это значение в другое значение, и посылает получившееся значение в выходной стрим(который <R>).
Т.е. map для каждого объекта в стриме возвращает по 1 объекту, потом преобразует все объекты в итоговый стрим.
Операция flatMap принимает функцию (которая преобразует каждое значение входного стрима в стрим), применяет ее к каждому элементу, и на выходе
возвращает стрим с одним, несколькими или ни c одним из элементов для каждого элемента входящего стрима.
Т.е., flatMap возвращает по стриму для каждого объекта в первоначальном стриме, а затем результирующие потоки объединяются в исходный стрим.
Пример для map: есть одномерный массив array с числами. Нужно получить массив из первоначального массива, в котором к каждому числу
прибавлена 1.
Решение:
array = Arrays.stream(array) //преобразовываем массив в стрим .map(i -> i+1) //преобразовываем каждый элемент стрима .toArray(); //преобразовываем стрим в массив
map к каждому значению стрима прибавляет 1, потом все новые значения преобразует в итоговый стрим.
Пример для flatMap: есть двухмерный array с числами, надо получить одномерный массив с числами.
Решение:
secondArray = Arrays.stream(array) .flatMapToInt(i -> Arrays.stream(i)) //преобразовываем Stream<int[]> в Stream .toArray(); // преобразовываем Stream в int[]
В этом примере создается стрим состоящий из каждого элемента первоначального массива - т.е. стрим из массивов. Потом с помощью i -> Arrays.stream(i)
преобразовываем каждый элемент(который является массивом) стрима в стрим с числами. После этого flatMap собирает все получившееся стримы в
один итоговый стрим.

12.

Параллельные стримы
Стримы бывают последовательными (sequential) и параллельными (parallel). Последовательные выполняются только в текущем потоке, а вот параллельные используют
общий пул ForkJoinPool.commonPool(). При этом элементы разбиваются (если это возможно) на несколько групп и обрабатываются в каждом потоке отдельно. Затем на
нужном этапе группы объединяются в одну для предоставления конечного результата.
Чтобы получить параллельный стрим, нужно либо вызвать метод parallelStream() вместо stream(), либо превратить обычный стрим в параллельный, вызвав
промежуточный оператор parallel.
[копировать] [скачать]
1.
2.
3.
4.
5.
6.
7.
8.
9.
list.parallelStream()
.filter(x -> x > 10)
.map(x -> x * 2)
.collect(Collectors.toList());
IntStream.range(0, 10)
.parallel()
.map(x -> x * 10)
.sum();
Работа с потоконебезопасными коллекциями, разбиение элементов на части, создание потоков, объединение частей воедино, всё это кроется в реализации Stream API. От
нас лишь требуется вызвать нужный метод и проследить, чтобы функции в операторах не зависели от каких-либо внешних факторов, иначе есть риск получить неверный
результат или ошибку.
Вот так делать нельзя:
[копировать] [скачать]
1.
2.
3.
4.
5.
final List<Integer> ints = new ArrayList<>();
IntStream.range(0, 1000000)
.parallel()
.forEach(i -> ints.add(i));
System.out.println(ints.size());
Это код Шрёдингера. Он может нормально выполниться и показать 1000000, может выполниться и показать 869877, а может и упасть с ошибкой Exception in thread
"main" java.lang.ArrayIndexOutOfBoundsException: 332 at java.util.ArrayList.add(ArrayList.java:459) .
Поэтому разработчики настоятельно просят воздержаться от побочных эффектов в лямбдах, то тут, то там говоря в документации о невмешательстве (non-interference).

13.

Стримы для примитивов
Кроме объектных стримов Stream<T>, существуют специальные стримы для примитивных типов:
- IntStream для int,
- LongStream для long,
- DoubleStream для double.
Для boolean, byte, short и char специальных стримов не придумали, но вместо них можно использовать
IntStream, а затем приводить к нужному типу. Для float тоже придётся воспользоваться DoubleStream.
Примитивные стримы полезны, так как не нужно тратить время на боксинг/анбоксинг, к тому же у них есть ряд
специальных операторов, упрощающих жизнь. Их мы рассмотрим очень скоро.

14.

Советы и best practices
1. Если задачу не получается красиво решить стримами, не решайте её стримами.
2. Если задачу не получается красиво решить стримами, не решайте её стримами!
3. Если задача уже красиво решена не стримами, всё работает и всех всё устраивает, не перерешивайте её стримами!
4. В большинстве случаев нет смысла сохранять стрим в переменную. Используйте цепочку вызовов методов (method chaining).
[копировать] [скачать]
1.
2.
3.
4.
5.
6.
7.
8.
// Нечитабельно
Stream<Integer> stream = list.stream();
stream = stream.filter(x -> x > 2);
stream.forEach(System.out::println);
// Так лучше
list.stream()
.filter(x -> x > 2)
.forEach(System.out::println);
5. Старайтесь сперва отфильтровать стрим от ненужных элементов или ограничить его, а потом выполнять преобразования.
[копировать] [скачать]
1. // Лишние затраты
2. list.stream()
3.
.sorted()
4.
.filter(x -> x > 0)
5.
.forEach(System.out::println);
6. // Так лучше
7. list.stream()
8.
.filter(x -> x > 0)
9.
.sorted()
10.
.forEach(System.out::println);

15.

6. Не используйте параллельные стримы везде, где только можно. Затраты на разбиение элементов, обработку в другом потоке и последующее их слияние
порой больше, чем выполнение в одном потоке.
7. При использовании параллельных стримов, убедитесь, что нигде нет блокирующих операций или чего-то, что может помешать обработке элементов.
1.
2.
3.
list.parallelStream()
.filter(s -> isFileExists(hash(s)))
...
8. Если где-то в модели вы возвращаете копию списка или другой коллекции, то подумайте о замене на стримы. Например:
[копировать] [скачать]
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
// Было
class Model {
private final List<String> data;
public List<String> getData() {
return new ArrayList<>(data);
}
}
// Стало
class Model {
private final List<String> data;
public Stream<String> dataStream() {
return data.stream();
}
}
Теперь есть возможность получить не только список model.dataStream().collect(toList());, но и множество, любую другую коллекцию,
отфильтровать что-то, отсортировать и так далее. Оригинальный List<String> data так и останется нетронутым.

16.

Не забудьте выключить
компьютер
English     Русский Правила