Java Stream API
Java Stream API
Несмотря на то, что Java 8 вышла уже достаточно давно, далеко не все программисты используют её новые возможности, кого-то останавливает то, что рабочие проекты слишком сложно перевести с Java 7 или даже Java 6, кого-то использование в своих проектах GWT, кто-то делает проекты под Android и не хочет или не может использовать сторонние библиотеки для реализации лямбд и Stream Api. Однако знание лямбд и Stream Api для программиста Java зачастую требуют на собеседованиях, ну и просто будет полезно при переходе на проект где используется Java 8. Я хотел бы предложить вам краткую шпаргалку по Stream Api с практическими примерами реализации различных задач с новым функциональным подходом. Знания лямбд и функционального программирования не потребуется (я постарался дать примеры так, чтобы все было понятно), уровень от самого базового знания Java и выше.
Также, так как это шпаргалка, статья может использоваться, чтобы быстро вспомнить как работает та или иная особенность Java Stream Api. Краткое перечисление возможностей основных функций дано в начале статьи.
Для тех кто совсем не знает что такое Stream Api
Stream API это новый способ работать со структурами данных в функциональном стиле. Чаще всего с помощью stream в Java 8 работают с коллекциями, но на самом деле этот механизм может использоваться для самых различных данных.
Stream Api позволяет писать обработку структур данных в стиле SQL, то если раньше задача получить сумму всех нечетных чисел из коллекции решалась следующим кодом:
Integer sumOddOld = 0;
for(Integer i: collection) {
if(i % 2 != 0) {
sumOddOld += i;
}
}
То с помощью Stream Api можно решить такую задачу в функциональном стиле:
Integer sumOdd = collection.stream().filter(o -> o % 2 != 0).reduce((s1, s2) -> s1 + s2).orElse(0);
Более того, Stream Api позволяет решать задачу параллельно лишь изменив stream() на parallelStream() без всякого лишнего кода, т.е.
Integer sumOdd = collection.parallelStream().filter(o -> o % 2 != 0).reduce((s1, s2) -> s1 + s2).orElse(0);
Уже делает код параллельным, без всяких семафоров, синхронизаций, рисков взаимных блокировок и т.п.
Общее оглавление 'Шпаргалок'
Давайте начнем с начала, а именно с создания объектов stream в Java 8.
I. Способы создания стримов
Перечислим несколько способов создать стрим
Способ создания стрима | Шаблон создания | Пример |
---|---|---|
1. Классический: Создание стрима из коллекции | collection.stream() | <br>Collection<String> collection = Arrays.asList("a1", "a2", "a3");<br> Stream<String> streamFromCollection = collection.stream(); <br> |
2. Создание стрима из значений | Stream.of(значение1,… значениеN) | <br>Stream<String> streamFromValues = Stream.of("a1", "a2", "a3"); <br> |
3. Создание стрима из массива | Arrays.stream(массив) | <br>String[] array = {"a1","a2","a3"}; <br> Stream<String> streamFromArrays = Arrays.stream(array); <br> |
4. Создание стрима из файла (каждая строка в файле будет отдельным элементом в стриме) | Files.lines(путь_к_файлу) | <br>Stream<String> streamFromFiles = Files.lines(Paths.get("file.txt")) <br> |
5. Создание стрима из строки | «строка».chars() | <br>IntStream streamFromString = "123".chars() <br> |
6. С помощью Stream.builder | Stream.builder().add(...)....build() | <br>Stream.builder().add("a1").add("a2").add("a3").build() <br> |
7. Создание параллельного стрима | collection.parallelStream() | <br>Stream<String> stream = collection.parallelStream(); <br> |
8. Создание бесконечных стрима с помощью Stream.iterate | Stream.iterate(начальное_условие, выражение_генерации) | <br>Stream<Integer> streamFromIterate = Stream.iterate(1, n -> n + 1) <br> |
9. Создание бесконечных стрима с помощью Stream.generate | Stream.generate(выражение_генерации) | <br>Stream<String> streamFromGenerate = Stream.generate(() -> "a1") <br> |
В принципе, кроме последних двух способов создания стрима, все не отличается от обычных способов создания коллекций. Последние два способа служат для генерации бесконечных стримов, в iterate задается начальное условие и выражение получение следующего значения из предыдущего, то есть Stream.iterate(1, n -> n + 1) будет выдавать значения 1, 2, 3, 4,… N. Stream.generate служит для генерации константных и случайных значений, он просто выдает значения соответствующие выражению, в данном примере, он будет выдавать бесконечное количество значений «a1».
Для тех кто не знает лямбды
Выражение n -> n + 1, это просто аналог выражения Integer func(Integer n) { return n+1;}, а выражение () -> «a1» аналог выражения String func() { return «a1»;} обернутых в анонимный класс.
Более подробные примеры
Так же этот пример можно найти на github'e
System.out.println("Test buildStream start");
Stream<String> streamFromValues = Stream.of("a1", "a2", "a3");
System.out.println("streamFromValues = " + streamFromValues.collect(Collectors.toList()));
String[] array = {"a1","a2","a3"};
Stream<String> streamFromArrays = Arrays.stream(array);
System.out.println("streamFromArrays = " + streamFromArrays.collect(Collectors.toList()));
Stream<String> streamFromArrays1 = Stream.of(array);
System.out.println("streamFromArrays1 = " + streamFromArrays1.collect(Collectors.toList()));
File file = new File("1.tmp");
file.deleteOnExit();
PrintWriter out = new PrintWriter(file);
out.println("a1");
out.println("a2");
out.println("a3");
out.close();
Stream<String> streamFromFiles = Files.lines(Paths.get(file.getAbsolutePath()));
System.out.println("streamFromFiles = " + streamFromFiles.collect(Collectors.toList()));
Collection<String> collection = Arrays.asList("a1", "a2", "a3");
Stream<String> streamFromCollection = collection.stream();
System.out.println("streamFromCollection = " + streamFromCollection.collect(Collectors.toList()));
IntStream streamFromString = "123".chars();
System.out.print("streamFromString = ");
streamFromString.forEach((e)->System.out.print(e + " , "));
System.out.println();
Stream.Builder<String> builder = Stream.builder();
Stream<String> streamFromBuilder = builder.add("a1").add("a2").add("a3").build();
System.out.println("streamFromBuilder = " + streamFromBuilder.collect((Collectors.toList())));
Stream<Integer> streamFromIterate = Stream.iterate(1, n -> n + 2);
System.out.println("streamFromIterate = " + streamFromIterate.limit(3).collect(Collectors.toList()));
Stream<String> streamFromGenerate = Stream.generate(() -> "a1");
System.out.println("streamFromGenerate = " + streamFromGenerate.limit(3).collect(Collectors.toList()));
Stream<String> streamEmpty = Stream.empty();
System.out.println("streamEmpty = " + streamEmpty.collect(Collectors.toList()));
Stream<String> parallelStream = collection.parallelStream();
System.out.println("parallelStream = " + parallelStream.collect(Collectors.toList()));
II. Методы работы со стримами
Java Stream API предлагает два вида методов: 1. Конвейерные — возвращают другой stream, то есть работают как builder, 2. Терминальные — возвращают другой объект, такой как коллекция, примитивы, объекты, Optional и т.д.
О том чем отличаются конвейерные и терминальные методы
2.1 Краткое описание конвейерных методов работы со стримами
Метод stream | Описание | Пример |
---|---|---|
filter | Отфильтровывает записи, возвращает только записи, соответствующие условию | collection.stream().filter(«a1»::equals).count() |
skip | Позволяет пропустить N первых элементов | collection.stream().skip(collection.size() — 1).findFirst().orElse(«1») |
distinct | Возвращает стрим без дубликатов (для метода equals) | collection.stream().distinct().collect(Collectors.toList()) |
map | Преобразует каждый элемент стрима | collection.stream().map((s) -> s + "_1").collect(Collectors.toList()) |
peek | Возвращает тот же стрим, но применяет функцию к каждому элементу стрима | collection.stream().map(String::toUpperCase).peek((e) -> System.out.print("," + e)). collect(Collectors.toList()) |
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, но может создавать из одного элемента несколько | collection.stream().flatMap((p) -> Arrays.asList(p.split(",")).stream()).toArray(String[]::new) |
2.2 Краткое описание терминальных методов работы со стримами
Метод stream | Описание | Пример |
---|---|---|
findFirst | Возвращает первый элемент из стрима (возвращает Optional) | collection.stream().findFirst().orElse(«1») |
findAny | Возвращает любой подходящий элемент из стрима (возвращает Optional) | collection.stream().findAny().orElse(«1») |
collect | Представление результатов в виде коллекций и других структур данных | collection.stream().filter((s) -> s.contains(«1»)).collect(Collectors.toList()) |
count | Возвращает количество элементов в стриме | collection.stream().filter(«a1»::equals).count() |
anyMatch | Возвращает true, если условие выполняется хотя бы для одного элемента | collection.stream().anyMatch(«a1»::equals) |
noneMatch | Возвращает true, если условие не выполняется ни для одного элемента | collection.stream().noneMatch(«a8»::equals) |
allMatch | Возвращает true, если условие выполняется для всех элементов | collection.stream().allMatch((s) -> s.contains(«1»)) |
min | Возвращает минимальный элемент, в качестве условия использует компаратор | collection.stream().min(String::compareTo).get() |
max | Возвращает максимальный элемент, в качестве условия использует компаратор | collection.stream().max(String::compareTo).get() |
forEach | Применяет функцию к каждому объекту стрима, порядок при параллельном выполнении не гарантируется | set.stream().forEach((p) -> p.append("_1")); |
forEachOrdered | Применяет функцию к каждому объекту стрима, сохранение порядка элементов гарантирует | list.stream().forEachOrdered((p) -> p.append("_new")); |
toArray | Возвращает массив значений стрима | collection.stream().map(String::toUpperCase).toArray(String[]::new); |
reduce | Позволяет выполнять агрегатные функции на всей коллекцией и возвращать один результат | collection.stream().reduce((s1, s2) -> s1 + s2).orElse(0) |
Обратите внимание методы findFirst, findAny, anyMatch это short-circuiting методы, то есть обход стримов организуется таким образом чтобы найти подходящий элемент максимально быстро, а не обходить весь изначальный стрим.
2.3 Краткое описание дополнительных методов у числовых стримов
Метод stream | Описание | Пример |
---|---|---|
sum | Возвращает сумму всех чисел | collection.stream().mapToInt((s) -> Integer.parseInt(s)).sum() |
average | Возвращает среднее арифметическое всех чисел | collection.stream().mapToInt((s) -> Integer.parseInt(s)).average() |
mapToObj | Преобразует числовой стрим обратно в объектный | intStream.mapToObj((id) -> new Key(id)).toArray() |
2.4 Несколько других полезных методов стримов
Метод stream | Описание |
---|---|
isParallel | Узнать является ли стрим параллельным |
parallel | Вернуть параллельный стрим, если стрим уже параллельный, то может вернуть самого себя |
sequential | Вернуть последовательный стрим, если стрим уже последовательный, то может вернуть самого себя |
С помощью, методов parallel и sequential можно определять какие операции могут быть параллельными, а какие только последовательными. Так же из любого последовательного стрима можно сделать параллельный и наоборот, то есть:
collection.stream().
peek(...).
parallel().
map(...).
sequential().
reduce(...)
Внимание: крайне не рекомендуется использовать параллельные стримы для сколько-нибудь долгих операций (получение данных из базы, сетевых соединений), так как все параллельные стримы работают c одним пулом fork/join и такие долгие операции могут остановить работу всех параллельных стримов в JVM из-за того отсутствия доступных потоков в пуле, т.е. параллельные стримы стоит использовать лишь для коротких операций, где счет идет на миллисекунды, но не для тех где счет может идти на секунды и минуты.
III. Примеры работы с методами стримов
Рассмотрим работу с методами на различных задачах, обычно требующихся при работе с коллекциями.
3.1 Примеры использования filter, findFirst, findAny, skip, limit и count
Условие: дана коллекция строк Arrays.asList(«a1», «a2», «a3», «a1»), давайте посмотрим как её можно обрабатывать используя методы filter, findFirst, findAny, skip и count:
Задача | Код примера | Результат |
---|---|---|
Вернуть количество вхождений объекта «a1» | collection.stream().filter(«a1»::equals).count() | 2 |
Вернуть первый элемент коллекции или 0, если коллекция пуста | collection.stream().findFirst().orElse(«0») | a1 |
Вернуть последний элемент коллекции или «empty», если коллекция пуста | collection.stream().skip(collection.size() — 1).findAny().orElse(«empty») | a1 |
Найти элемент в коллекции равный «a3» или кинуть ошибку | collection.stream().filter(«a3»::equals).findFirst().get() | a3 |
Вернуть третий элемент коллекции по порядку | collection.stream().skip(2).findFirst().get() | a3 |
Вернуть два элемента начиная со второго | collection.stream().skip(1).limit(2).toArray() | [a2, a3] |
Выбрать все элементы по шаблону | collection.stream().filter((s) -> s.contains(«1»)).collect(Collectors.toList()) | [a1, a1] |
Обратите внимание, что методы findFirst и findAny возвращают новый тип Optional, появившийся в Java 8, для того чтобы избежать NullPointerException. Метод filter удобно использовать для выборки лишь определенного множества значений, а метод skip позволяет пропускать определенное количество элементов.
Если вы не знаете лямбды
Выражение «a3»::equals это аналог boolean func(s) { return «a3».equals(s);}, а (s) -> s.contains(«1») это аналог boolean func(s) { return s.contains(«1»);} обернутых в анонимный класс.
Условие: дана коллекция класс People (с полями name — имя, age — возраст, sex — пол), вида Arrays.asList( new People(«Вася», 16, Sex.MAN), new People(«Петя», 23, Sex.MAN), new People(«Елена», 42, Sex.WOMEN), new People(«Иван Иванович», 69, Sex.MAN)). Давайте посмотрим примеры как работать с таким классом:
Задача | Код примера | Результат |
---|---|---|
Выбрать мужчин-военнообязанных (от 18 до 27 лет) | peoples.stream().filter((p)-> p.getAge() >= 18 && p.getAge() < 27 && p.getSex() == Sex.MAN).collect(Collectors.toList()) |
[{name='Петя', age=23, sex=MAN}] |
Найти средний возраст среди мужчин | peoples.stream().filter((p) -> p.getSex() == Sex.MAN). mapToInt(People::getAge).average().getAsDouble() |
36.0 |
Найти кол-во потенциально работоспособных людей в выборке (т.е. от 18 лет и учитывая что женщины выходят в 55 лет, а мужчина в 60) | peoples.stream().filter((p) -> p.getAge() >= 18).filter( (p) -> (p.getSex() == Sex.WOMEN && p.getAge() < 55) | (p.getSex() == Sex.MAN && p.getAge() < 60)).count() |
2 |
Детальные примеры
Также этот пример можно найти на github'e: первый класс и второй класс
private static void testFilterAndCount() {
System.out.println();
System.out.println("Test filter and count start");
Collection<String> collection = Arrays.asList("a1", "a2", "a3", "a1");
Collection<People> peoples = Arrays.asList(
new People("Вася", 16, Sex.MAN),
new People("Петя", 23, Sex.MAN),
new People("Елена", 42, Sex.WOMEN),
new People("Иван Иванович", 69, Sex.MAN)
);
long count = collection.stream().filter("a1"::equals).count();
System.out.println("count = " + count);
List<String> select = collection.stream().filter((s) -> s.contains("1")).collect(Collectors.toList());
System.out.println("select = " + select);
List<People> militaryService = peoples.stream().filter((p)-> p.getAge() >= 18 && p.getAge() < 27
&& p.getSex() == Sex.MAN).collect(Collectors.toList());
System.out.println("militaryService = " + militaryService);
double manAverageAge = peoples.stream().filter((p) -> p.getSex() == Sex.MAN).
mapToInt(People::getAge).average().getAsDouble();
System.out.println("manAverageAge = " + manAverageAge);
long peopleHowCanWork = peoples.stream().filter((p) -> p.getAge() >= 18).filter(
(p) -> (p.getSex() == Sex.WOMEN && p.getAge() < 55) || (p.getSex() == Sex.MAN && p.getAge() < 60)).count();
System.out.println("peopleHowCanWork = " + peopleHowCanWork);
}
private static void testFindFirstSkipCount() {
Collection<String> collection = Arrays.asList("a1", "a2", "a3", "a1");
System.out.println("Test findFirst and skip start");
String first = collection.stream().findFirst().orElse("1");
System.out.println("first = " + first);
String last = collection.stream().skip(collection.size() - 1).findAny().orElse("1");
System.out.println("last = " + last );
String find = collection.stream().filter("a3"::equals).findFirst().get();
System.out.println("find = " + find);
String third = collection.stream().skip(2).findFirst().get();
System.out.println("third = " + third);
System.out.println();
System.out.println("Test collect start");
List<String> select = collection.stream().filter((s) -> s.contains("1")).collect(Collectors.toList());
System.out.println("select = " + select);
}
private static void testLimit() {
System.out.println();
System.out.println("Test limit start");
Collection<String> collection = Arrays.asList("a1", "a2", "a3", "a1");
List<String> limit = collection.stream().limit(2).collect(Collectors.toList());
System.out.println("limit = " + limit);
List<String> fromTo = collection.stream().skip(1).limit(2).collect(Collectors.toList());
System.out.println("fromTo = " + fromTo);
String last = collection.stream().skip(collection.size() - 1).findAny().orElse("1");
System.out.println("last = " + last );
}
private enum Sex {
MAN,
WOMEN
}
private static class People {
private final String name;
private final Integer age;
private final Sex sex;
public People(String name, Integer age, Sex sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
public Sex getSex() {
return sex;
}
@Override
public String toString() {
return "{" +
"name='" + name + '\'' +
", age=" + age +
", sex=" + sex +
'}';
}
}
3.2 Примеры использования distinct
Метод distinct возвращает stream без дубликатов, при этом для упорядоченного стрима (например, коллекция на основе list) порядок стабилен, для неупорядоченного — порядок не гарантируется. Рассмотрим результаты работы над коллекцией Collection ordered = Arrays.asList(«a1», «a2», «a2», «a3», «a1», «a2», «a2») и Collection nonOrdered = new HashSet<>(ordered).
Задача | Код примера | Результат |
---|---|---|
Получение коллекции без дубликатов из неупорядоченного стрима | nonOrdered.stream().distinct().collect(Collectors.toList()) | [a1, a2, a3] — порядок не гарантируется |
Получение коллекции без дубликатов из упорядоченного стрима | ordered.stream().distinct().collect(Collectors.toList()); | [a1, a2, a3] — порядок гарантируется |
Обратите внимание: 1. Если вы используете distinct с классом, у которого переопределен equals, обязательно так же корректно переопределить hashCode в соответствие с контрактом equals/hashCode (самое главное чтобы hashCode для всех equals объектов, возвращал одинаковое значение), иначе distinct может не удалить дубликаты (аналогично, как при использовании HashSet/HashMap), 2. Если вы используете параллельные стримы и вам не важен порядок элементов после удаления дубликатов — намного лучше для производительности сделать сначала стрим неупорядоченным с помощь unordered(), а уже потом применять distinct(), так как подержание стабильности сортировки при параллельном стриме довольно затратно по ресурсам и distinct() на упорядоченным стриме будет выполнятся значительно дольше чем при неупорядоченном,
Детальные примеры
Так же этот пример можно найти на github'e
private static void testDistinct() {
System.out.println();
System.out.println("Test distinct start");
Collection<String> ordered = Arrays.asList("a1", "a2", "a2", "a3", "a1", "a2", "a2");
Collection<String> nonOrdered = new HashSet<>(ordered);
List<String> distinct = nonOrdered.stream().distinct().collect(Collectors.toList());
System.out.println("distinct = " + distinct);
List<String> distinctOrdered = ordered.stream().distinct().collect(Collectors.toList());
System.out.println("distinctOrdered = " + distinctOrdered);
}
3.3 Примеры использования Match функций (anyMatch, allMatch, noneMatch)
Условие: дана коллекция строк Arrays.asList(«a1», «a2», «a3», «a1»), давайте посмотрим, как её можно обрабатывать используя Match функции
Задача | Код примера | Результат |
---|---|---|
Найти существуют ли хоть один «a1» элемент в коллекции | collection.stream().anyMatch(«a1»::equals) | true |
Найти существуют ли хоть один «a8» элемент в коллекции | collection.stream().anyMatch(«a8»::equals) | false |
Найти есть ли символ «1» у всех элементов коллекции | collection.stream().allMatch((s) -> s.contains(«1»)) | false |
Проверить что не существуют ни одного «a7» элемента в коллекции | collection.stream().noneMatch(«a7»::equals) | true |
Детальные примеры
Также этот пример можно найти на github'e
private static void testMatch() {
System.out.println();
System.out.println("Test anyMatch, allMatch, noneMatch start");
Collection<String> collection = Arrays.asList("a1", "a2", "a3", "a1");
boolean isAnyOneTrue = collection.stream().anyMatch("a1"::equals);
System.out.println("anyOneTrue " + isAnyOneTrue);
boolean isAnyOneFalse = collection.stream().anyMatch("a8"::equals);
System.out.println("anyOneFlase " + isAnyOneFalse);
boolean isAll = collection.stream().allMatch((s) -> s.contains("1"));
System.out.println("isAll " + isAll);
boolean isNotEquals = collection.stream().noneMatch("a7"::equals);
System.out.println("isNotEquals " + isNotEquals);
}
3.4 Примеры использования Map функций (map, mapToInt, FlatMap, FlatMapToInt)
Условие: даны две коллекции collection1 = Arrays.asList(«a1», «a2», «a3», «a1») и collection2 = Arrays.asList(«1,2,0», «4,5»), давайте посмотрим как её можно обрабатывать используя различные map функции
Задача | Код примера | Результат |
---|---|---|
Добавить "_1" к каждому элементу первой коллекции | collection1.stream().map((s) -> s + "_1").collect(Collectors.toList()) | [a1_1, a2_1, a3_1, a1_1] |
В первой коллекции убрать первый символ и вернуть массив чисел (int[]) | collection1.stream().mapToInt((s) -> Integer.parseInt(s.substring(1))).toArray() | [1, 2, 3, 1] |
Из второй коллекции получить все числа, перечисленные через запятую из всех элементов | collection2.stream().flatMap((p) -> Arrays.asList(p.split(",")).stream()).toArray(String[]::new) | [1, 2, 0, 4, 5] |
Из второй коллекции получить сумму всех чисел, перечисленных через запятую | collection2.stream().flatMapToInt((p) -> Arrays.asList(p.split(",")).stream().mapToInt(Integer::parseInt)).sum() | 12 |
Обратите внимание: все map функции могут вернуть объект другого типа (класса), то есть map может работать со стримом строк, а на выходе дать Stream из значений Integer или получать класс людей People, а возвращать класс Office, где эти люди работают и т.п., flatMap (flatMapToInt и т.п.) на выходе должны возвращать стрим с одним, несколькими или ни одним элементов для каждого элемента входящего стрима (см. последние два примера).
Детальные примеры
Так же этот пример можно найти на github'e
private static void testMap() {
System.out.println();
System.out.println("Test map start");
Collection<String> collection = Arrays.asList("a1", "a2", "a3", "a1");
List<String> transform = collection.stream().map((s) -> s + "_1").collect(Collectors.toList());
System.out.println("transform = " + transform);
List<Integer> number = collection.stream().map((s) -> Integer.parseInt(s.substring(1))).collect(Collectors.toList());
System.out.println("number = " + number);
}
private static void testMapToInt() {
System.out.println();
System.out.println("Test mapToInt start");
Collection<String> collection = Arrays.asList("a1", "a2", "a3", "a1");
int[] number = collection.stream().mapToInt((s) -> Integer.parseInt(s.substring(1))).toArray();
System.out.println("number = " + Arrays.toString(number));
}
private static void testFlatMap() {
System.out.println();
System.out.println("Test flat map start");
Collection<String> collection = Arrays.asList("1,2,0", "4,5");
String[] number = collection.stream().flatMap((p) -> Arrays.asList(p.split(",")).stream()).toArray(String[]::new);
System.out.println("number = " + Arrays.toString(number));
}
private static void testFlatMapToInt() {
System.out.println();
System.out.println("Test flat map start");
Collection<String> collection = Arrays.asList("1,2,0", "4,5");
int sum = collection.stream().flatMapToInt((p) -> Arrays.asList(p.split(",")).stream().mapToInt(Integer::parseInt)).sum();
System.out.println("sum = " + sum);
}
3.5 Примеры использования Sorted функции
Условие: даны две коллекции коллекция строк Arrays.asList(«a1», «a4», «a3», «a2», «a1», «a4») и коллекция людей класса People (с полями name — имя, age — возраст, sex — пол), вида Arrays.asList( new People(«Вася», 16, Sex.MAN), new People(«Петя», 23, Sex.MAN), new People(«Елена», 42, Sex.WOMEN), new People(«Иван Иванович», 69, Sex.MAN)). Давайте посмотрим примеры как их можно сортировать:
Задача | Код примера | Результат |
---|---|---|
Отсортировать коллекцию строк по алфавиту | collection.stream().sorted().collect(Collectors.toList()) | [a1, a1, a2, a3, a4, a4] |
Отсортировать коллекцию строк по алфавиту в обратном порядке | collection.stream().sorted((o1, o2) -> -o1.compareTo(o2)).collect(Collectors.toList()) | [a4, a4, a3, a2, a1, a1] |
Отсортировать коллекцию строк по алфавиту и убрать дубликаты | collection.stream().sorted().distinct().collect(Collectors.toList()) | [a1, a2, a3, a4] |
Отсортировать коллекцию строк по алфавиту в обратном порядке и убрать дубликаты | collection.stream().sorted((o1, o2) -> -o1.compareTo(o2)).distinct().collect(Collectors.toList()) | [a4, a3, a2, a1] |
Отсортировать коллекцию людей по имени в обратном алфавитном порядке | peoples.stream().sorted((o1,o2) -> -o1.getName().compareTo(o2.getName())).collect(Collectors.toList()) | [{'Петя'}, {'Иван Иванович'}, {'Елена'}, {'Вася'}] |
Отсортировать коллекцию людей сначала по полу, а потом по возрасту | peoples.stream().sorted((o1, o2) -> o1.getSex() != o2.getSex()? o1.getSex(). compareTo(o2.getSex()): o1.getAge().compareTo(o2.getAge())).collect(Collectors.toList()) |
[{'Вася'}, {'Петя'}, {'Иван Иванович'}, {'Елена'}] |
Детальные примеры
Так же этот пример можно найти на github'e
private static void testSorted() {
System.out.println();
System.out.println("Test sorted start");
Collection<String> collection = Arrays.asList("a1", "a4", "a3", "a2", "a1", "a4");
List<String> sorted = collection.stream().sorted().collect(Collectors.toList());
System.out.println("sorted = " + sorted);
List<String> sortedDistinct = collection.stream().sorted().distinct().collect(Collectors.toList());
System.out.println("sortedDistinct = " + sortedDistinct);
List<String> sortedReverse = collection.stream().sorted((o1, o2) -> -o1.compareTo(o2)).collect(Collectors.toList());
System.out.println("sortedReverse = " + sortedReverse);
List<String> distinctReverse = collection.stream().sorted((o1, o2) -> -o1.compareTo(o2)).distinct().collect(Collectors.toList());
System.out.println("distinctReverse = " + distinctReverse);
Collection<People> peoples = Arrays.asList(
new People("Вася", 16, Sex.MAN),
new People("Петя", 23, Sex.MAN),
new People("Елена", 42, Sex.WOMEN),
new People("Иван Иванович", 69, Sex.MAN)
);
Collection<People> byName = peoples.stream().sorted((o1,o2) -> -o1.getName().compareTo(o2.getName())).collect(Collectors.toList());
System.out.println("byName = " + byName);
Collection<People> bySexAndAge = peoples.stream().sorted((o1, o2) -> o1.getSex() != o2.getSex() ? o1.getSex().
compareTo(o2.getSex()) : o1.getAge().compareTo(o2.getAge())).collect(Collectors.toList());
System.out.println("bySexAndAge = " + bySexAndAge);
}
private enum Sex {
MAN,
WOMEN
}
private static class People {
private final String name;
private final Integer age;
private final Sex sex;
public People(String name, Integer age, Sex sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
public Sex getSex() {
return sex;
}
@Override
public String toString() {
return "{" +
"name='" + name + '\'' +
", age=" + age +
", sex=" + sex +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof People)) return false;
People people = (People) o;
return Objects.equals(name, people.name) &&
Objects.equals(age, people.age) &&
Objects.equals(sex, people.sex);
}
@Override
public int hashCode() {
return Objects.hash(name, age, sex);
}
}
3.6 Примеры использования Max и Min функций
Условие: дана коллекция строк Arrays.asList(«a1», «a2», «a3», «a1»), и коллекция класса Peoples из прошлых примеров про Sorted и Filter функции.
Задача | Код примера | Результат |
---|---|---|
Найти максимальное значение среди коллекции строк | collection.stream().max(String::compareTo).get() | a3 |
Найти минимальное значение среди коллекции строк | collection.stream().min(String::compareTo).get() | a1 |
Найдем человека с максимальным возрастом | peoples.stream().max((p1, p2) -> p1.getAge().compareTo(p2.getAge())).get() | {name='Иван Иванович', age=69, sex=MAN} |
Найдем человека с минимальным возрастом | peoples.stream().min((p1, p2) -> p1.getAge().compareTo(p2.getAge())).get() | {name='Вася', age=16, sex=MAN} |
Детальные примеры
Так же этот пример можно найти на github'e
private static void testMinMax() {
System.out.println();
System.out.println("Test min and max start");
Collection<String> collection = Arrays.asList("a1", "a2", "a3", "a1");
String max = collection.stream().max(String::compareTo).get();
System.out.println("max " + max);
String min = collection.stream().min(String::compareTo).get();
System.out.println("min " + min);
Collection<People> peoples = Arrays.asList(
new People("Вася", 16, Sex.MAN),
new People("Петя", 23, Sex.MAN),
new People("Елена", 42, Sex.WOMEN),
new People("Иван Иванович", 69, Sex.MAN)
);
People older = peoples.stream().max((p1, p2) -> p1.getAge().compareTo(p2.getAge())).get();
System.out.println("older " + older);
People younger = peoples.stream().min((p1, p2) -> p1.getAge().compareTo(p2.getAge())).get();
System.out.println("younger " + younger);
}
private enum Sex {
MAN,
WOMEN
}
private static class People {
private final String name;
private final Integer age;
private final Sex sex;
public People(String name, Integer age, Sex sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
public Sex getSex() {
return sex;
}
@Override
public String toString() {
return "{" +
"name='" + name + '\'' +
", age=" + age +
", sex=" + sex +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof People)) return false;
People people = (People) o;
return Objects.equals(name, people.name) &&
Objects.equals(age, people.age) &&
Objects.equals(sex, people.sex);
}
@Override
public int hashCode() {
return Objects.hash(name, age, sex);
}
}
3.7 Примеры использования ForEach и Peek функций
Обе ForEach и Peek по сути делают одно и тоже, меняют свойства объектов в стриме, единственная разница между ними в том что ForEach терминальная и она заканчивает работу со стримом, в то время как Peek конвейерная и работа со стримом продолжается. Например, есть коллекция:
Collection<StringBuilder> list = Arrays.asList(new StringBuilder("a1"), new StringBuilder("a2"), new StringBuilder("a3"));
И нужно добавить к каждому элементу "_new", то для ForEach код будет
list.stream().forEachOrdered((p) -> p.append("_new"));
а для peek код будет
List<StringBuilder> newList = list.stream().peek((p) -> p.append("_new")).collect(Collectors.toList());
Детальные примеры
Так же этот пример можно найти на github'e
private static void testForEach() {
System.out.println();
System.out.println("For each start");
Collection<String> collection = Arrays.asList("a1", "a2", "a3", "a1");
System.out.print("forEach = ");
collection.stream().map(String::toUpperCase).forEach((e) -> System.out.print(e + ","));
System.out.println();
Collection<StringBuilder> list = Arrays.asList(new StringBuilder("a1"), new StringBuilder("a2"), new StringBuilder("a3"));
list.stream().forEachOrdered((p) -> p.append("_new"));
System.out.println("forEachOrdered = " + list);
}
private static void testPeek() {
System.out.println();
System.out.println("Test peek start");
Collection<String> collection = Arrays.asList("a1", "a2", "a3", "a1");
System.out.print("peak1 = ");
List<String> peek = collection.stream().map(String::toUpperCase).peek((e) -> System.out.print(e + ",")).
collect(Collectors.toList());
System.out.println();
System.out.println("peek2 = " + peek);
Collection<StringBuilder> list = Arrays.asList(new StringBuilder("a1"), new StringBuilder("a2"), new StringBuilder("a3"));
List<StringBuilder> newList = list.stream().peek((p) -> p.append("_new")).collect(Collectors.toList());
System.out.println("newList = " + newList);
}
3.8 Примеры использования Reduce функции
Метод reduce позволяет выполнять агрегатные функции на всей коллекцией (такие как сумма, нахождение минимального или максимального значение и т.п.), он возвращает одно значение для стрима, функция получает два аргумента — значение полученное на прошлых шагах и текущее значение.
Условие: Дана коллекция чисел Arrays.asList(1, 2, 3, 4, 2) выполним над ними несколько действий используя reduce.
Задача | Код примера | Результат |
---|---|---|
Получить сумму чисел или вернуть 0 | collection.stream().reduce((s1, s2) -> s1 + s2).orElse(0) | 12 |
Вернуть максимум или -1 | collection.stream().reduce(Integer::max).orElse(-1) | 4 |
Вернуть сумму нечетных чисел или 0 | collection.stream().filter(o -> o % 2 != 0).reduce((s1, s2) -> s1 + s2).orElse(0) | 4 |
Детальные примеры
Также этот пример можно найти на github'e
private static void testReduce() {
System.out.println();
System.out.println("Test reduce start");
Collection<Integer> collection = Arrays.asList(1, 2, 3, 4, 2);
Integer sum = collection.stream().reduce((s1, s2) -> s1 + s2).orElse(0);
Integer sumOld = 0;
for(Integer i: collection) {
sumOld += i;
}
System.out.println("sum = " + sum + " : " + sumOld);
Integer max1 = collection.stream().reduce((s1, s2) -> s1 > s2 ? s1 : s2).orElse(0);
Integer max2 = collection.stream().reduce(Integer::max).orElse(0);
Integer maxOld = null;
for(Integer i: collection) {
maxOld = maxOld != null && maxOld > i? maxOld: i;
}
maxOld = maxOld == null? 0 : maxOld;
System.out.println("max = " + max1 + " : " + max2 + " : " + maxOld);
Integer min = collection.stream().reduce((s1, s2) -> s1 < s2 ? s1 : s2).orElse(0);
Integer minOld = null;
for(Integer i: collection) {
minOld = minOld != null && minOld < i? minOld: i;
}
minOld = minOld == null? 0 : minOld;
System.out.println("min = " + min+ " : " + minOld);
Integer last = collection.stream().reduce((s1, s2) -> s2).orElse(0);
Integer lastOld = null;
for(Integer i: collection) {
lastOld = i;
}
lastOld = lastOld == null? 0 : lastOld;
System.out.println("last = " + last + " : " + lastOld);
Integer sumMore2 = collection.stream().filter(o -> o > 2).reduce((s1, s2) -> s1 + s2).orElse(0);
Integer sumMore2Old = 0;
for(Integer i: collection) {
if(i > 2) {
sumMore2Old += i;
}
}
System.out.println("sumMore2 = " + sumMore2 + " : " + sumMore2Old);
Integer sumOdd = collection.stream().filter(o -> o % 2 != 0).reduce((s1, s2) -> s1 + s2).orElse(0);
Integer sumOddOld = 0;
for(Integer i: collection) {
if(i % 2 != 0) {
sumOddOld += i;
}
}
System.out.println("sumOdd = " + sumOdd + " : " + sumOddOld);
Collection<People> peoples = Arrays.asList(
new People("Вася", 16, Sex.MAN),
new People("Петя", 23, Sex.MAN),
new People("Елена", 42, Sex.WOMEN),
new People("Иван Иванович", 69, Sex.MAN)
);
int oldMan = peoples.stream().filter((p) -> p.getSex() == Sex.MAN).map(People::getAge).reduce((s1, s2) -> s1 > s2 ? s1 : s2).get();
System.out.println("oldMan = " + oldMan);
int younger = peoples.stream().filter((p) -> p.getName().contains("е")).mapToInt(People::getAge).reduce((s1, s2) -> s1 < s2 ? s1 : s2).orElse(0);
System.out.println("younger = " + younger);
}
private enum Sex {
MAN,
WOMEN
}
private static class People {
private final String name;
private final Integer age;
private final Sex sex;
public People(String name, Integer age, Sex sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
public Sex getSex() {
return sex;
}
@Override
public String toString() {
return "{" +
"name='" + name + '\'' +
", age=" + age +
", sex=" + sex +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof People)) return false;
People people = (People) o;
return Objects.equals(name, people.name) &&
Objects.equals(age, people.age) &&
Objects.equals(sex, people.sex);
}
@Override
public int hashCode() {
return Objects.hash(name, age, sex);
}
}
3.9 Примеры использования toArray и collect функции
Если с toArray все просто, можно либо вызвать toArray() получить Object[], либо toArray(T[]::new) — получив массив типа T, то collect позволяет много возможностей преобразовать значение в коллекцию, map'у или любой другой тип. Для этого используются статические методы из Collectors, например преобразование в List будет stream.collect(Collectors.toList()).
Давайте рассмотрим статические методы из Collectors:
Метод | Описание |
---|---|
toList, toCollection, toSet | представляют стрим в виде списка, коллекции или множества |
toConcurrentMap, toMap | позволяют преобразовать стрим в map |
averagingInt, averagingDouble, averagingLong | возвращают среднее значение |
summingInt, summingDouble, summingLong | возвращает сумму |
summarizingInt, summarizingDouble, summarizingLong | возвращают SummaryStatistics с разными агрегатными значениями |
partitioningBy | разделяет коллекцию на две части по соответствию условию и возвращает их как Map<Boolean, List> |
groupingBy | разделяет коллекцию на несколько частей и возвращает Map<N, List |
mapping | дополнительные преобразования значений для сложных Collector'ов |
Теперь давайте рассмотрим работу с collect и toArray на примерах: Условие: Дана коллекция чисел Arrays.asList(1, 2, 3, 4), рассмотрим работу collect и toArray с ней
Задача | Код примера | Результат |
---|---|---|
Получить сумму нечетных чисел | numbers.stream().collect(Collectors.summingInt(((p) -> p % 2 == 1? p: 0))) | 4 |
Вычесть от каждого элемента 1 и получить среднее | numbers.stream().collect(Collectors.averagingInt((p) -> p — 1)) | 1.5 |
Прибавить к числам 3 и получить статистику | numbers.stream().collect(Collectors.summarizingInt((p) -> p + 3)) | IntSummaryStatistics{count=4, sum=22, min=4, average=5.5, max=7} |
Разделить числа на четные и нечетные | numbers.stream().collect(Collectors.partitioningBy((p) -> p % 2 == 0)) | {false=[1, 3], true=[2, 4]} |
Условие: Дана коллекция строк Arrays.asList(«a1», «b2», «c3», «a1»), рассмотрим работу collect и toArray с ней
Задача | Код примера | Результат |
---|---|---|
Получение списка без дубликатов | strings.stream().distinct().collect(Collectors.toList()) | [a1, b2, c3] |
Получить массив строк без дубликатов и в верхнем регистре | strings.stream().distinct().map(String::toUpperCase).toArray(String[]::new) | {A1, B2, C3} |
Объединить все элементы в одну строку через разделитель: и обернуть тегами <b>… </b> | strings.stream().collect(Collectors.joining(": ", "<b> ", " </b>")) | <b> a1: b2: c3: a1 </b> |
Преобразовать в map, где первый символ ключ, второй символ значение | strings.stream().distinct().collect(Collectors.toMap((p) -> p.substring(0, 1), (p) -> p.substring(1, 2))) | {a=1, b=2, c=3} |
Преобразовать в map, сгруппировав по первому символу строки | strings.stream().collect(Collectors.groupingBy((p) -> p.substring(0, 1))) | {a=[a1, a1], b=[b2], c=[c3]} |
Преобразовать в map, сгруппировав по первому символу строки и объединим вторые символы через : | strings.stream().collect(Collectors.groupingBy((p) -> p.substring(0, 1), Collectors.mapping((p) -> p.substring(1, 2), Collectors.joining(":")))) | {a=1:1, b=2, c=3} |
Детальные примеры
Также примеры можно найти на github'e
private static void testCollect() {
System.out.println();
System.out.println("Test distinct start");
Collection<String> strings = Arrays.asList("a1", "b2", "c3", "a1");
List<String> distinct = strings.stream().distinct().collect(Collectors.toList());
System.out.println("distinct = " + distinct);
String[] array = strings.stream().distinct().map(String::toUpperCase).toArray(String[]::new);
System.out.println("array = " + Arrays.asList(array));
String join = strings.stream().collect(Collectors.joining(" : ", "<b> ", " </b>"));
System.out.println("join = " + join);
Map<String, String> map = strings.stream().distinct().collect(Collectors.toMap((p) -> p.substring(0, 1), (p) -> p.substring(1, 2)));
System.out.println("map = " + map);
Map<String, List<String>> groups = strings.stream().collect(Collectors.groupingBy((p) -> p.substring(0, 1)));
System.out.println("groups = " + groups);
Map<String, String> groupJoin = strings.stream().collect(Collectors.groupingBy((p) -> p.substring(0, 1), Collectors.mapping((p) -> p.substring(1, 2), Collectors.joining(":"))));
System.out.println("groupJoin = " + groupJoin);
Collection<Integer> numbers = Arrays.asList(1, 2, 3, 4);
long sumOdd = numbers.stream().collect(Collectors.summingInt(((p) -> p % 2 == 1 ? p : 0)));
System.out.println("sumOdd = " + sumOdd);
double average = numbers.stream().collect(Collectors.averagingInt((p) -> p - 1));
System.out.println("average = " + average);
IntSummaryStatistics statistics = numbers.stream().collect(Collectors.summarizingInt((p) -> p + 3));
System.out.println("statistics = " + statistics);
long sumEven = numbers.stream().collect(Collectors.summarizingInt((p) -> p % 2 == 0 ? p : 0)).getSum();
System.out.println("sumEven = " + sumEven);
Map<Boolean, List<Integer>> parts = numbers.stream().collect(Collectors.partitioningBy((p) -> p % 2 == 0));
System.out.println("parts = " + parts);
}
3.10 Пример создания собственного Collector'a
Кроме Collector'ов уже определенных в Collectors можно так же создать собственный Collector, Давайте рассмотрим пример как его можно создать.
Метод определения пользовательского Collector'a:
Collector<Тип_источника, Тип_аккумулятора, Тип_результата> сollector = Collector.of(
метод_инициализации_аккумулятора,
метод_обработки_каждого_элемента,
метод_соединения_двух_аккумуляторов,
[метод_последней_обработки_аккумулятора]
);
Как видно из кода выше, для реализации своего Collector'a нужно определить три или четыре метода (метод_последней_обработки_аккумулятора не обязателен). Рассмотрим следующий кода, который мы писали до Java 8, чтобы объединить все строки коллекции:
StringBuilder b = new StringBuilder();
for(String s: strings) {
b.append(s).append(" , ");
}
String joinBuilderOld = b.toString();
И аналогичный код, который будет написан в Java 8
String joinBuilder = strings.stream().collect(
Collector.of(
StringBuilder::new,
(b ,s) -> b.append(s).append(" , "),
(b1, b2) -> b1.append(b2).append(" , "),
StringBuilder::toString
)
);
В общем-то, три метода легко понять из кода выше, их мы писали практически при каждой обработки коллекций, но вот что такое метод_соединения_двух_аккумуляторов? Это метод который нужен для параллельной обработки Collector'a, в данном случае при параллельном стриме коллекция может быть разделенной на две части (или больше частей), в каждой из которых будет свой аккумулятор StringBuilder и потом необходимо будет их объединить, то код до Java 8 при 2 потоках будет таким:
StringBuilder b1 = new StringBuilder();
for(String s: stringsPart1) {
b1.append(s).append(" , ");
}
StringBuilder b2 = new StringBuilder();
for(String s: stringsPart2) {
b2.append(s).append(" , ");
}
StringBuilder b = b1.append(b2).append(" , "),
String joinBuilderOld = b.toString();
Напишем свой аналог Collectors.toList() для работы со строковым стримом:
Collector<String, List<String>, List<String>> toList = Collector.of(
ArrayList::new,
List::add,
(l1, l2) -> { l1.addAll(l2); return l1; }
);
List<String> distinct1 = strings.stream().distinct().collect(toList);
Детальные примеры
Так же примеры можно найти на github'e
Collector<String,StringBuilder, String> stringBuilderCollector = Collector.of(
StringBuilder::new,
(b ,s) -> b.append(s).append(" , "),
(b1, b2) -> b1.append(b2).append(" , "),
StringBuilder::toString
);
String joinBuilder = strings.stream().collect(stringBuilderCollector);
System.out.println("joinBuilder = " + joinBuilder);
StringBuilder b = new StringBuilder();
for(String s: strings) {
b.append(s).append(" , ");
}
String joinBuilderOld = b.toString();
System.out.println("joinBuilderOld = " + joinBuilderOld);
Collector<String, List<String>, List<String>> toList = Collector.of(
ArrayList::new,
List::add,
(l1, l2) -> { l1.addAll(l2); return l1; }
);
List<String> distinct1 = strings.stream().distinct().collect(toList);
System.out.println("distinct1 = " + distinct1);
IV. Заключение
Вот и все. Надеюсь, моя небольшая шпаргалка по работе со stream api была для вас полезной. Все исходники есть на github'е, удачи в написании хорошего кода.
P.S. Список других статей, где можно прочитать дополнительно про Stream Api: 1. Processing Data with Java SE 8 Streams, Part 1 от Oracle, 2. Processing Data with Java SE 8 Streams, Part 2 от Oracle, 3. Полное руководство по Java 8 Stream
P.P.S. Так же советую посмотреть мой opensource проект useful-java-links — возможно, наиболее полная коллекция полезных Java библиотек, фреймворков и русскоязычного обучающего видео. Так же есть аналогичная английская версия этого проекта и начинаю opensource подпроект Hello world по подготовке коллекции простых примеров для разных Java библиотек в одном maven проекте (буду благодарен за любую помощь).