Введение

API Stream в Java предоставляет мощный способ работы с коллекциями данных в функциональном стиле. Эта статья посвящена одному из его методов — map(), который является важнейшим инструментом преобразования данных. Рассмотрим метод map() из класса java.util.stream.Stream, выясним, как он работает, постараемся понять, в чем его польза, и понаблюдаем на практических примерах, как его использовать в реальном мире.

Объяснение метода Stream.map()

Метод map() — ключевая часть API Java Stream. Он позволяет применить заданную функцию к каждому элементу потока, создавая новый поток с преобразованными элементами. Этот метод особенно полезен в скриптах, когда нужно преобразовать данные из одного типа в другой или изменить содержимое коллекции.

Что такое поток?

Поток (stream) в Java — последовательность элементов, поддерживающая последовательные и параллельные операции агрегирования. В отличие от коллекций, потоки не хранят элементы. Вместо этого они оперируют исходными данными и выдают результат. Потоки могут создаваться из коллекций, массивов и каналов ввода-вывода.

Объяснение метода map()

Метод map() является нетерминальной операцией, то есть возвращает новый поток, а не конечный результат. В качестве аргумента он принимает Function — функцию, которая определяет, как будет преобразован каждый элемент в потоке. Преобразование может включать изменение типа элементов, изменение их состояния и даже фильтрацию.

Синтаксис:

<R> Stream<R> map(Function<? super T, ? extends R> mapper)
  • <R>: тип элементов в новом потоке;
  • mapper: функция для применения к каждому элементу.

Использование метода map() для преобразования данных

Метод map() лежит в основе преобразования данных в API Java Stream. Независимо от того, имеете ли вы дело с простыми типами данных, такими как строки и целые числа, или сложными объектами, map() позволяет преобразовывать данные и манипулировать ими в чистом, функциональном стиле. Разберемся, как работает этот метод, и рассмотрим различные скрипты, в которых он может быть эффективно использован.

Функциональный интерфейс, лежащий в основе map()

В своей основе метод map() опирается на функциональный интерфейс Function<T, R>. Этот интерфейс представляет функцию, которая принимает один аргумент типа T и выдает результат типа R. Метод map() использует эту функцию для применения преобразования к каждому элементу в потоке. Такой подход не только упрощает код, но и использует возможности лямбда-выражений и ссылок методов в Java, что приводит к более краткому и читаемому коду.

Пример:

Function<String, Integer> stringLengthFunction = String::length;

В этом примере stringLengthFunction — функция, которая принимает String (строку) и возвращает Integer (целое число), представляющее длину этой строки. При использовании в сочетании с map() эта функция может преобразовать поток строк в поток целых чисел.

Преобразование данных с помощью примитивных типов данных

Одним из наиболее распространенных случаев использования метода map() является преобразование примитивных типов данных. Например, рассмотрим скрипт со списком числовых строк, которые необходимо преобразовать в целые числа. С помощью map() это преобразование можно выполнить без особых усилий:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MapExample {
public static void main(String[] args) {
List<String> numberStrings = Arrays.asList("15", "25", "35", "45");
List<Integer> numbers = numberStrings.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
System.out.println(numbers); // Вывод: [15, 25, 35, 45]
}
}

Здесь метод map() применяет функцию Integer::parseInt к каждой строке в списке, преобразуя ее в целое число. Затем полученный поток собирается в список целых чисел. Этот простой пример демонстрирует, как метод map() можно использовать для преобразования данных из одного примитивного типа в другой.

Преобразование сложных объектов

Метод map() по-настоящему проявляет себя при работе со сложными объектами. Допустим, у вас есть список объектов Employee, из которого нужно извлечь определенное поле, например имя или возраст сотрудника. С помощью map() можно создать новый поток, содержащий только извлеченные данные.

Рассмотрим следующий пример, в котором извлечем имена сотрудников из списка объектов Employee:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

class Employee {
private String name;
private int age;

public Employee(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}

public class MapExample {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Ethan", 30),
new Employee("Lily", 25),
new Employee("Mason", 35)
);

List<String> employeeNames = employees.stream()
.map(Employee::getName)
.collect(Collectors.toList());

System.out.println(employeeNames); // Вывод: [Ethan, Lily, Mason]
}
}

В данном примере метод map() используется для преобразования потока объектов Employee в поток имен. Эта операция особенно полезна, когда нужно обработать или проанализировать определенные поля сложного объекта.

Преобразование в другой тип объекта

Еще одна мощная возможность метода map() — преобразование объектов в совершенно другие типы. Например, вам нужно преобразовать список объектов Order в список объектов Invoice на основе некоторой бизнес-логики.

Рассмотрим упрощенный пример, в котором преобразуем объекты Order в объекты Invoice:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

class Order {
private String orderId;
private double amount;

public Order(String orderId, double amount) {
this.orderId = orderId;
this.amount = amount;
}

public String getOrderId() {
return orderId;
}

public double getAmount() {
return amount;
}
}

class Invoice {
private String invoiceId;
private double totalAmount;

public Invoice(String invoiceId, double totalAmount) {
this.invoiceId = invoiceId;
this.totalAmount = totalAmount;
}

@Override
public String toString() {
return "Invoice{" +
"invoiceId='" + invoiceId + '\'' +
", totalAmount=" + totalAmount +
'}';
}
}

public class MapExample {
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("ORD001", 250.00),
new Order("ORD002", 150.75),
new Order("ORD003", 300.50)
);

List<Invoice> invoices = orders.stream()
.map(order -> new Invoice(order.getOrderId(), order.getAmount() * 1.10)) // Добавление налога в 10 %
.collect(Collectors.toList());

invoices.forEach(System.out::println);
}
}

В этом случае каждый объект Order преобразуется в объект Invoice, а метод map() применяет функцию, включающую расчеты бизнес-логики (например, добавление налога в размере 10 %). В результате получается список объектов Invoice, который можно использовать для дальнейшей обработки или создания отчетов.

Создание цепочки из нескольких операций map()

Одной из сильных сторон метода map() является его способность объединяться с другими потоковыми операциями. Можно применять несколько последовательных вызовов map() для достижения более сложных преобразований.

Рассмотрим сценарий, в котором список полных имен (личных имен и фамилий) надо преобразовать в список имен пользователей, извлекая первую букву личного имени и объединяя ее с фамилией:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MapExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Olivia Davis", "Noah Johnson", "Sophia Clark");

List<String> usernames = names.stream()
.map(name -> name.split(" ")) // Разделение полного имени на имя и фамилию
.map(parts -> parts[0].charAt(0) + parts[1]) // Создание имени пользователя путем объединения инициалов и фамилии
.map(String::toLowerCase) // Преобразование в нижний регистр
.collect(Collectors.toList());

System.out.println(usernames); // Вывод: [odavis, njohnson, sclark]
}
}

В этом примере используется несколько операций map(), чтобы сначала разделить полные имена, затем создать имена пользователей и, наконец, преобразовать их в нижний регистр. Возможность объединения методов map() в цепочку таким образом демонстрирует гибкость и выразительность API Stream.

Работа с null и классом Optional с помощью map()

При работе с потоками, особенно при обработке данных, можно столкнуться с ситуацией, когда некоторые элементы являются null. Метод map() может изящно обрабатывать такие случаи, особенно в сочетании с классом Optional.

Допустим, вы обрабатываете список строк, некоторые из которых могут быть null. Можете использовать map() для преобразования данных, безопасно обрабатывая значения null:

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class MapExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("grape", null, "melon", "peach", null);

List<Optional<String>> transformedWords = words.stream()
.map(word -> Optional.ofNullable(word).map(String::toUpperCase))
.collect(Collectors.toList());

System.out.println(transformedWords); // Вывод: [Optional[GRAPE], Optional.empty, Optional[MELON], Optional[PEACH], Optional.empty]
}
}

В этом случае метод map() используется в сочетании с Optional.ofNullable() для безопасной обработки потенциальных значений null. Эта техника гарантирует, что преобразование будет применено только к элементам, которые не являются null, предотвращая NullPointerException и делая код более надежным.

Практические примеры map() в реальной практике

Рассмотрим несколько практических примеров использования метода map() в реальных сценариях. Во всех примерах разберем пошагово механизм работы метода map(), чтобы четко понимать, что происходит на каждом этапе.

Извлечение и изменение данных из списка объектов

Рассмотрим ситуацию, когда из списка объектов Book нужно извлечь названия книг и преобразовать их в верхний регистр. Метод map() может эффективно справиться с этой задачей.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

class Book {
private String title;
private String author;

public Book(String title, String author) {
this.title = title;
this.author = author;
}

public String getTitle() {
return title;
}
}

public class MapExample {
public static void main(String[] args) {
List<Book> books = Arrays.asList(
new Book("The Great Gatsby", "F. Scott Fitzgerald"),
new Book("1984", "George Orwell"),
new Book("To Kill a Mockingbird", "Harper Lee")
);

List<String> upperCaseTitles = books.stream()
.map(Book::getTitle)
.map(String::toUpperCase)
.collect(Collectors.toList());

System.out.println(upperCaseTitles); // Вывод: [THE GREAT GATSBY, 1984, TO KILL A MOCKINGBIRD]
}
}

Пошаговый разбор:

  1. Исходный список. Начинаем со списка объектов Book, каждый из которых содержит title (название) и author (автора) книги.
  1. Создание потока. Список books преобразуется в поток с помощью stream(), что позволяет выполнять операции над каждым объектом Book.
  1. Первое преобразование — извлечение названий. Первый вызов map() применяет функцию Book::getTitle, извлекая название каждой книги. Теперь поток преобразуется из потока объектов Book в поток объектов String (названий).
  1. Второе преобразование — преобразование в верхний регистр. Второй вызов map() применяет функцию String::toUpperCase к каждому названию, преобразуя его в верхний регистр. Поток остается потоком объектов String, но теперь каждая строка переводится в верхний регистр.
  1. Сбор результата. Конечный результат собирается в список с помощью функции collect(Collectors.toList()). Этот список содержит названия книг в верхнем регистре.

Этот пример демонстрирует возможности цепочки из нескольких операций map() для выполнения сложных преобразований данных чистым и лаконичным способом.

Выравнивание и преобразование вложенных структур данных

Предположим, у вас есть список объектов Department (отделов), каждый из которых содержит список объектов Employee  (сотрудников). Необходимо извлечь все имена сотрудников из всех отделов и перевести их в нижний регистр. Метод map() в сочетании с flatMap() может решить эту задачу.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

class Employee {
private String name;

public Employee(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

class Department {
private String name;
private List<Employee> employees;

public Department(String name, List<Employee> employees) {
this.name = name;
this.employees = employees;
}

public List<Employee> getEmployees() {
return employees;
}
}

public class MapExample {
public static void main(String[] args) {
List<Department> departments = Arrays.asList(
new Department("HR", Arrays.asList(new Employee("Emma"), new Employee("Liam"))),
new Department("IT", Arrays.asList(new Employee("Ava"), new Employee("Noah"))),
new Department("Finance", Arrays.asList(new Employee("Olivia"), new Employee("Mason")))
);

List<String> employeeNames = departments.stream()
.flatMap(department -> department.getEmployees().stream()) // Flatten the nested lists
.map(Employee::getName)
.map(String::toLowerCase)
.collect(Collectors.toList());

System.out.println(employeeNames); // Вывод: [emma, liam, ava, noah, olivia, mason]
}
}

Пошаговый разбор:

  1. Исходная структура. Есть список объектов Department, каждый из которых содержит список объектов Employee.
  1. Создание потока. Список departments преобразуется в поток с помощью stream(), подготавливая вложенные данные к преобразованию.
  1. Выравнивание (преобразование в плоскую структуру) вложенной структуры. Метод flatMap() используется для выравнивания вложенного списка объектов Employee. Это означает, что вместо потока отделов теперь есть единый поток сотрудников всех отделов.
  1. Преобразование — извлечение и изменение имен. Метод map() применяется дважды: во-первых, для извлечения name (имени) каждого сотрудника, а во-вторых — для преобразования каждого имени в нижний регистр.
  1. Сбор результата. Окончательный список имен сотрудников в нижнем регистре собирается с помощью collect(Collectors.toList()).

Заключение

Метод map() в Java Stream API — мощный инструмент для функционального и эффективного преобразования данных. Поняв механизм работы этого метода и применяя его в реальных сценариях, вы упростите свой код, повысив его читаемость и удобство при сопровождении. Для многих задач — будь то преобразование типов данных, извлечение полей или применение сложной бизнес-логики —  map() предоставляет гибкие и лаконичные решения, которые улучшат навыки программирования на Java.

  1. Документация Java Stream API.
  1. Функциональный интерфейс Java.
  1. Java Collectors.toList().

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Alexander Obregon: Java’s Stream.map() Method Explained

Предыдущая статьяНовые способы оптимизации стабильности в Jetpack Compose
Следующая статья17 промежуточных программ для эффективной FastAPI-разработки