StreamAPI
[https://t.me/source_coding]Stream - это последовательность элементов, поддерживающих последовательные и параллельные операции над ними.
Назначение. Упростить работу с наборами данных - фильтрация, сортировка, замена элементов, и т. д.
Терминальные методы заканчивают работу с потоком (t)- terminal Промежуточные методы возвращают поток (i) - intermediate
Метод map() - преобразование элементов (i)
Метод map берет по очереди каждый элемент из нашего стрима и сопоставляет ему элемент, который из него получается после применения на нем тех действий, которые мы описываем внутри map с помощью лямбда-выражения.
Без использования стрима:
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("Alex", "Oleh", "Andrey", "Rostislav", "Veronika", "Dmitrii"));
System.out.println(names);
strToLength(names);
System.out.println(names);
}
public static void strToLength(List<String> strings) {
for (int i = 0; i < strings.size(); i++) {
strings.set(i, String.valueOf(strings.get(i).length()));
}
}
}

Со стримом:
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("Alex", "Oleh", "Andrey", "Rostislav", "Veronika", "Dmitrii"));
System.out.println(names);
names = names.stream()
.map(element -> String.valueOf(element.length()))
.toList();
System.out.println(names);
}
}
names = names.stream() .map(element -> String.valueOf(element.length())) .toList();
вместо
public static void strToLength(List<String> strings) {
for (int i = 0; i < strings.size(); i++) {
strings.set(i, String.valueOf(strings.get(i).length()));
}
}
Еще один пример, нужно создать новый список, который будет содержать в себе значения старого списка умноженные на 5:
List<Integer> digits = new ArrayList<>(List.of(3, 6, 2, 43, 76,34,76, 1, 76, 23,76, 98, 34,13)); List<Integer> multipliedBy5 = digits.stream().map(x->x*5).toList(); System.out.println(multipliedBy5);

Метод filter() - фильтрация элементов за какими-то параметрами (i)
Объект стрима предоставляет метод filter() для фильтрации элементов потока на основе заданного предиката.
Предположим, вы хотите получить только четные элементы вашего списка, тогда вы можете легко сделать это с помощью метода фильтрации. Этот метод принимает предикат в качестве аргумента и возвращает поток, состоящий из результирующих элементов:
List<Integer> digits = new ArrayList<>(List.of(3, 6, 23, 43, 71, 34, 39, 1, 76, 23, 29, 98, 33,13));
digits.stream()
.filter(x->x%2==0)
.forEach(x-> System.out.print(x + " "));

Кстати, зачастую используют method chaining, это когда несколько методов идут(вызываются) друг за другом. Выше мы отфильтровали наши элементы, то есть получили поток "новых" элементов, поскольку это все еще поток, мы можем использовать другие его методы - map(), forEach() и т. д.
Следующий пример. Есть список номеров, нам нужно найти номера начинающееся на А, и чтобы номер содержал буквы KH.
List<String> codes = new ArrayList<>(List.of("BC3564MV", "BE9890EM", "BK5616HA", "AX1792KH", "BC0708MP", "AT3100CP"));
codes.stream()
.filter(x->x.startsWith("A"))
.filter(x->x.contains("KH"))
.forEach(System.out::println);

Метод forEach() - выполняет действие для каждого элемента потока (t)
Как вы могли заметить, я использовал его в предыдущих примерах, когда нам нужно было вывести каждый элемент коллекции.
С названия думаю все ясно - просто проделываем какие-то операции над каждым элементом. Вывод, вывод с измененным состоянием элемента, вывод его свойств.
List<String> names = new ArrayList<>(List.of("Alex", "Oleh", "Andrey", "Rostislav", "Veronika", "Dmitrii"));
names.stream().forEach(x -> System.out.println(x + ". Length is " + x.length()));
К каждому элементу мы прибавили его длину и вывело это в консоль.

Метод reduce() - сокращение до одного элемента (t)
Этот метод потока позволяет нам получить один единственный результат из последовательности элементов, многократно применяя операцию комбинирования к элементам в последовательности.
Проще говоря, мы можем находить сумму, произведение, разность и так далее. Если это строки, то сконкатенировать их в одну строку.
reduce() может принимать 2 аргумента. Если используется только один аргумент, то принимается функциональный интерфейс, который в свою очередь принимает аккумулятор и "текущий" элемент потока. Если у нас 2 аргумента, то первый это начальное состояние аккумулятора, а второй я только что описал - функциональный интерфейс.
Аккумулятор накапливает (делает что-то с текущим элементом, прибавляет, умножает или делит) значение и его можно получить методом get().
Метод get() является методом класса Optional. Если вкратце, это контейнер для для объектов, который может хранить null и notNull значения.
Если мы не указали начального значения аккумулятора, по умолчанию, reduce возвращает Optional, и чтобы вытащить из этого "бокса" значение какого-то типа, мы используем метод get().
Напротив, если мы присвоили начальное состояние, нет варианта что мы получим null значение, потому что, как минимум, нач. состояние уже присутствует.
Нахождение суммы элементов коллекции:
List<Integer> digits = new ArrayList<>(List.of(50, 20, 10));
int resultSum = digits.stream().reduce((sum,x)->sum+x).get();
System.out.println("Our sum found by reduce method: " + resultSum);

Нахождение произведения элементов коллекции:
List<Integer> digits = new ArrayList<>(List.of(50, 20, 10));
int resultProduct = digits.stream().reduce((product,x)->product*x).get();
System.out.println("Our product found by reduce method: " + resultProduct);

Важно то, что мы можем задать начальное значения "результирующего" элемента.
List<Integer> digits = new ArrayList<>(List.of(50, 20, 10));
int resultSum = digits.stream().reduce(20, (sum,x)->sum+x);
System.out.println("Our sum found by reduce method (+20): " + resultSum);


После того, как мы задали начальное состояние, мы в конце не прописываем метод get(), поскольку мы УЖЕ установили какое-то значение. То есть оно не null.
Нахождение результирующей строки в коллекции:
List<String> names = new ArrayList<>(List.of("Alex", "Oleh", "Andrey", "Rostislav", "Veronika", "Dmitrii"));
String allStringFromCollection = names.stream().reduce("Names: ",(concat,elem)->concat+elem+" ");
System.out.println(allStringFromCollection);

Здесь мы тоже определили начальное состояние - "Names: " и сконкатенировали все строки в нашей коллекции.
Но это чисто для примера, конкатенировать строки плохо) В плане если их будет очень много.
Метод concat() - объединение потоков (i)
Stream<Integer> stream1 = Stream.of(1, 2, 3, 4, 5); Stream<Integer> stream2 = Stream.of(6, 7, 8, 9, 10); Stream<Integer> stream3 = Stream.concat(stream1, stream2); stream3.forEach(System.out::print);

Метод distinct() - удаление повторяющихся элементов (i)
Метод удаляет из потока все дубликаты и возвращает поток уникальных элементов.
List<Integer> ints = new ArrayList<>(List.of(1, 2, 2, 1, 4, 6, 9, 3, 5, 2)); ints.stream().distinct().forEach(System.out::println);

Метод count() - подсчет элементов (t)
Возвращает количество элементов - тип long.
List<Integer> ints = new ArrayList<>(List.of(1, 2, 2, 1, 4, 6, 9, 3, 5, 2));
long allCount = ints.stream().count();
long distinctCount = ints.stream().distinct().count();
long distinctOver3Count = ints.stream().distinct().filter(x->x>3).count();
System.out.println("Кол. всех элементов: " + allCount);
System.out.println("Кол. уникальных элементов: " + distinctCount);
System.out.println("Кол. уникальных элем. больше за три: " + distinctOver3Count);

Метод peek (i)
Данный метод, как и метод forEach так же принимает Consumer, то есть делает что-то над элементом, И, возвращает поток, в свою очередь, forEach возвращал void, по этому, в случае с forEach, на этом method chaining заканчивался.
Обычно, он нужен для того, чтобы посмотреть как проходит поэтапно наш method chaining. К примеру, мы отфильтровали наши элементы и хотим дальше что-то делать, но прежде чем что-то делать, мы можем вывести элементы с помощью peek, а после, продолжить выполнять иные операции над потоком.
Ex:
List<Integer> ints = new ArrayList<>(List.of(1, 2, 2, 1, 4, 6, 9, 3, 5));
List<Integer> newList = ints.stream()
.filter(x->x%2==0)
.peek(System.out::println)
.distinct()
.toList();
System.out.println("New list: " + newList);

Метод flatMap() - непрямая работа с элементами (i) (*)
(сначала пример, потом объяснение)
Example:
Класс Faculty:
class Faculty {
private String name;
public List<Student> studentList;
public Faculty(String name) {
this.name = name;
this.studentList = new ArrayList<>();
}
public List<Student> getStudentList() {
return studentList;
}
public void addStudentToFaculty(Student student) {
this.studentList.add(student);
}
}
Класс Student:
class Student {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Main:
Student student1 = new Student("Alex");
Student student2 = new Student("Rude");
Student student3 = new Student("Dmitrii");
Student student4 = new Student("Max");
Student student5 = new Student("John");
Faculty faculty1 = new Faculty("IT");
Faculty faculty2 = new Faculty("Economics");
faculty1.addStudentToFaculty(student1);
faculty1.addStudentToFaculty(student2);
faculty2.addStudentToFaculty(student3);
faculty2.addStudentToFaculty(student4);
faculty2.addStudentToFaculty(student5);
List<Faculty> facultyList = new ArrayList<>(List.of(faculty1, faculty2));
facultyList.stream()
.flatMap(faculty -> faculty.getStudentList().stream())
.forEach(student -> System.out.println(student.getName()));

Это похоже на вложенные цикл, сначала мы проходимся по 1 факультету, получая поток факультетов, потом получаем список студентов этого факультета и работаем с ними, ПОТОМ, переходим к внешнему потоку, меняется факультет, мы получаем новый поток студентов другого факультета и работаем уже с ними.
Метод flatMap() мы используем тогда, когда нам нужно поработать не с самими элементами нашей коллекции, а с элементами элементов нашей коллекции.
// Если будет много лайков, допишу и про остальные методы, немного устал от написания этого большого поста.